diff --git a/bitwarden-server.sln b/bitwarden-server.sln index 410afae94..50095d725 100644 --- a/bitwarden-server.sln +++ b/bitwarden-server.sln @@ -74,6 +74,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MySqlMigrations", "util\MyS EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PostgresMigrations", "util\PostgresMigrations\PostgresMigrations.csproj", "{F72E0229-2EF7-49B3-9004-FF4C0043816E}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "test\Common\Common.csproj", "{17DA09D7-0212-4009-879E-6B9CFDE5FA60}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -158,7 +160,6 @@ Global {F72E0229-2EF7-49B3-9004-FF4C0043816E}.Debug|Any CPU.Build.0 = Debug|Any CPU {F72E0229-2EF7-49B3-9004-FF4C0043816E}.Release|Any CPU.ActiveCfg = Release|Any CPU {F72E0229-2EF7-49B3-9004-FF4C0043816E}.Release|Any CPU.Build.0 = Release|Any CPU - {EDC0D688-D58C-4CE1-AA07-3606AC6874B8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EDC0D688-D58C-4CE1-AA07-3606AC6874B8}.Debug|Any CPU.Build.0 = Debug|Any CPU {EDC0D688-D58C-4CE1-AA07-3606AC6874B8}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -167,6 +168,10 @@ Global {0E99A21B-684B-4C59-9831-90F775CAB6F7}.Debug|Any CPU.Build.0 = Debug|Any CPU {0E99A21B-684B-4C59-9831-90F775CAB6F7}.Release|Any CPU.ActiveCfg = Release|Any CPU {0E99A21B-684B-4C59-9831-90F775CAB6F7}.Release|Any CPU.Build.0 = Release|Any CPU + {17DA09D7-0212-4009-879E-6B9CFDE5FA60}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17DA09D7-0212-4009-879E-6B9CFDE5FA60}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17DA09D7-0212-4009-879E-6B9CFDE5FA60}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17DA09D7-0212-4009-879E-6B9CFDE5FA60}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -193,6 +198,7 @@ Global {F72E0229-2EF7-49B3-9004-FF4C0043816E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} {EDC0D688-D58C-4CE1-AA07-3606AC6874B8} = {4FDB6543-F68B-4202-9EA6-7FEA984D2D0A} {0E99A21B-684B-4C59-9831-90F775CAB6F7} = {287CFF34-BBDB-4BC4-AF88-1E19A5A4679B} + {17DA09D7-0212-4009-879E-6B9CFDE5FA60} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F} diff --git a/bitwarden_license/test/CmmCore.Test/Services/ProviderServiceTests.cs b/bitwarden_license/test/CmmCore.Test/Services/ProviderServiceTests.cs index a46d85443..b10c24926 100644 --- a/bitwarden_license/test/CmmCore.Test/Services/ProviderServiceTests.cs +++ b/bitwarden_license/test/CmmCore.Test/Services/ProviderServiceTests.cs @@ -13,8 +13,6 @@ using Bit.Core.Models.Table; using Bit.Core.Models.Table.Provider; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Test.AutoFixture; -using Bit.Core.Test.AutoFixture.Attributes; using Bit.Core.Utilities; using Microsoft.AspNetCore.DataProtection; using NSubstitute; @@ -22,6 +20,8 @@ using NSubstitute.ReturnsExtensions; using Xunit; using ProviderUser = Bit.Core.Models.Table.Provider.ProviderUser; using Bit.Core.Context; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; namespace Bit.CommCore.Test.Services { diff --git a/src/Api/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Controllers/OrganizationSponsorshipsController.cs new file mode 100644 index 000000000..40fd4e6fa --- /dev/null +++ b/src/Api/Controllers/OrganizationSponsorshipsController.cs @@ -0,0 +1,134 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Context; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Api; +using Bit.Core.Models.Api.Request; +using Bit.Core.Models.Table; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.Controllers +{ + [Route("organization/sponsorship")] + [Authorize("Application")] + public class OrganizationSponsorshipsController : Controller + { + private readonly IOrganizationSponsorshipService _organizationsSponsorshipService; + private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository; + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly ICurrentContext _currentContext; + private readonly IUserService _userService; + + public OrganizationSponsorshipsController(IOrganizationSponsorshipService organizationSponsorshipService, + IOrganizationSponsorshipRepository organizationSponsorshipRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IUserService userService, + ICurrentContext currentContext) + { + _organizationsSponsorshipService = organizationSponsorshipService; + _organizationSponsorshipRepository = organizationSponsorshipRepository; + _organizationRepository = organizationRepository; + _organizationUserRepository = organizationUserRepository; + _userService = userService; + _currentContext = currentContext; + } + + [HttpPost("{sponsoringOrgId}/families-for-enterprise")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] OrganizationSponsorshipRequestModel model) + { + await _organizationsSponsorshipService.OfferSponsorshipAsync( + await _organizationRepository.GetByIdAsync(sponsoringOrgId), + await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default), + model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName, + (await CurrentUser).Email); + } + + [HttpPost("{sponsoringOrgId}/families-for-enterprise/resend")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task ResendSponsorshipOffer(Guid sponsoringOrgId) + { + var sponsoringOrgUser = await _organizationUserRepository + .GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default); + + await _organizationsSponsorshipService.ResendSponsorshipOfferAsync( + await _organizationRepository.GetByIdAsync(sponsoringOrgId), + sponsoringOrgUser, + await _organizationSponsorshipRepository + .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id), + (await CurrentUser).Email); + } + + [HttpPost("redeem")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task RedeemSponsorship([FromQuery] string sponsorshipToken, [FromBody] OrganizationSponsorshipRedeemRequestModel model) + { + if (!await _organizationsSponsorshipService.ValidateRedemptionTokenAsync(sponsorshipToken)) + { + throw new BadRequestException("Failed to parse sponsorship token."); + } + + if (!await _currentContext.OrganizationOwner(model.SponsoredOrganizationId)) + { + throw new BadRequestException("Can only redeem sponsorship for an organization you own."); + } + + await _organizationsSponsorshipService.SetUpSponsorshipAsync( + await _organizationSponsorshipRepository + .GetByOfferedToEmailAsync((await CurrentUser).Email), + // Check org to sponsor's product type + await _organizationRepository.GetByIdAsync(model.SponsoredOrganizationId)); + } + + [HttpDelete("{sponsoringOrganizationId}")] + [HttpPost("{sponsoringOrganizationId}/delete")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task RevokeSponsorship(Guid sponsoringOrganizationId) + { + + var orgUser = await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrganizationId, _currentContext.UserId ?? default); + if (_currentContext.UserId != orgUser?.UserId) + { + throw new BadRequestException("Can only revoke a sponsorship you granted."); + } + + var existingOrgSponsorship = await _organizationSponsorshipRepository + .GetBySponsoringOrganizationUserIdAsync(orgUser.Id); + + await _organizationsSponsorshipService.RevokeSponsorshipAsync( + await _organizationRepository + .GetByIdAsync(existingOrgSponsorship.SponsoredOrganizationId ?? default), + existingOrgSponsorship); + } + + [HttpDelete("sponsored/{sponsoredOrgId}")] + [HttpPost("sponsored/{sponsoredOrgId}/remove")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task RemoveSponsorship(Guid sponsoredOrgId) + { + + if (!await _currentContext.OrganizationOwner(sponsoredOrgId)) + { + throw new BadRequestException("Only the owner of an organization can remove sponsorship."); + } + + var existingOrgSponsorship = await _organizationSponsorshipRepository + .GetBySponsoredOrganizationIdAsync(sponsoredOrgId); + + await _organizationsSponsorshipService.RemoveSponsorshipAsync( + await _organizationRepository + .GetByIdAsync(existingOrgSponsorship.SponsoredOrganizationId.Value), + existingOrgSponsorship); + } + + private Task CurrentUser => _userService.GetUserByIdAsync(_currentContext.UserId.Value); + } +} diff --git a/src/Billing/Controllers/StripeController.cs b/src/Billing/Controllers/StripeController.cs index e3c66b2a7..f6035e6d5 100644 --- a/src/Billing/Controllers/StripeController.cs +++ b/src/Billing/Controllers/StripeController.cs @@ -29,6 +29,7 @@ namespace Bit.Billing.Controllers private readonly BillingSettings _billingSettings; private readonly IWebHostEnvironment _hostingEnvironment; private readonly IOrganizationService _organizationService; + private readonly IOrganizationSponsorshipService _organizationSponsorshipService; private readonly IOrganizationRepository _organizationRepository; private readonly ITransactionRepository _transactionRepository; private readonly IUserService _userService; @@ -45,6 +46,7 @@ namespace Bit.Billing.Controllers IOptions billingSettings, IWebHostEnvironment hostingEnvironment, IOrganizationService organizationService, + IOrganizationSponsorshipService organizationSponsorshipService, IOrganizationRepository organizationRepository, ITransactionRepository transactionRepository, IUserService userService, @@ -58,6 +60,7 @@ namespace Bit.Billing.Controllers _billingSettings = billingSettings?.Value; _hostingEnvironment = hostingEnvironment; _organizationService = organizationService; + _organizationSponsorshipService = organizationSponsorshipService; _organizationRepository = organizationRepository; _transactionRepository = transactionRepository; _userService = userService; @@ -164,6 +167,13 @@ namespace Bit.Billing.Controllers // org if (ids.Item1.HasValue) { + // sponsored org + if (CheckSponsoredSubscription(subscription)) + { + await _organizationSponsorshipService + .ValidateSponsorshipAsync(ids.Item1.Value); + } + var org = await _organizationRepository.GetByIdAsync(ids.Item1.Value); if (org != null && OrgPlanForInvoiceNotifications(org)) { @@ -783,5 +793,8 @@ namespace Bit.Billing.Controllers } return subscription; } + + private static bool CheckSponsoredSubscription(Subscription subscription) => + StaticStore.SponsoredPlans.Any(p => p.StripePlanId == subscription.Id); } } diff --git a/src/Core/Enums/PaymentMethodType.cs b/src/Core/Enums/PaymentMethodType.cs index 81b536bd7..b0290f92b 100644 --- a/src/Core/Enums/PaymentMethodType.cs +++ b/src/Core/Enums/PaymentMethodType.cs @@ -22,5 +22,7 @@ namespace Bit.Core.Enums GoogleInApp = 7, [Display(Name = "Check")] Check = 8, + [Display(Name = "None")] + None = 255, } } diff --git a/src/Core/Enums/PlanSponsorshipType.cs b/src/Core/Enums/PlanSponsorshipType.cs new file mode 100644 index 000000000..79145bf1e --- /dev/null +++ b/src/Core/Enums/PlanSponsorshipType.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Enums +{ + public enum PlanSponsorshipType : byte + { + [Display(Name = "Families For Enterprise")] + FamiliesForEnterprise = 0, + } +} diff --git a/src/Core/Enums/PlanType.cs b/src/Core/Enums/PlanType.cs index 572c40fea..037f1f893 100644 --- a/src/Core/Enums/PlanType.cs +++ b/src/Core/Enums/PlanType.cs @@ -27,6 +27,6 @@ namespace Bit.Core.Enums [Display(Name = "Enterprise (Monthly)")] EnterpriseMonthly = 10, [Display(Name = "Enterprise (Annually)")] - EnterpriseAnnually= 11, + EnterpriseAnnually = 11, } } diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferExistingAccount.html.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferExistingAccount.html.hbs new file mode 100644 index 000000000..2af8fd42a --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferExistingAccount.html.hbs @@ -0,0 +1,21 @@ +{{#>FullHtmlLayout}} + + + + + + + + + + +
+ A Bitwarden account, {{SponsorEmail}}, has sponsored a free Families subscription for you! To activate your complimentary subscription, click the link below. +
+ + Accept Offer + +
+ If you do not recognize this account, please ignore this message. +
+{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferExistingAccount.text.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferExistingAccount.text.hbs new file mode 100644 index 000000000..f11b31596 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferExistingAccount.text.hbs @@ -0,0 +1,5 @@ +{{#>BasicTextLayout}} +A Bitwarden account, {{SponsorEmail}}, has sponsored a free Families subscription for you! To activate your complimentary subscription, click the link below. + +{{Url}} +{{/BasicTextLayout}} diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferNewAccount.html.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferNewAccount.html.hbs new file mode 100644 index 000000000..78bfdb742 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferNewAccount.html.hbs @@ -0,0 +1,21 @@ +{{#>FullHtmlLayout}} + + + + + + + + + + +
+ A Bitwarden account, {{SponsorEmail}}, has sponsored a free Families subscription for you! To accept your complimentary subscription, you will need to create an account with this email address. +
+ + Create Account + +
+ If you do not recognize this account, please ignore this message. +
+{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferNewAccount.text.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferNewAccount.text.hbs new file mode 100644 index 000000000..512c0c13f --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferNewAccount.text.hbs @@ -0,0 +1,7 @@ +{{#>BasicTextLayout}} +A Bitwarden account, {{SponsorEmail}}, has sponsored a free Families subscription for you! To accept your complimentary subscription, you will need to create an account with this email address. Click the link below. + +{{Url}} + +If you do not recognize this account, please ignore this message. +{{/BasicTextLayout}} diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToEnterpriseUser.html.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToEnterpriseUser.html.hbs new file mode 100644 index 000000000..6a6c47abe --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToEnterpriseUser.html.hbs @@ -0,0 +1,9 @@ +{{#>FullHtmlLayout}} + + + + +
+ Your Families subscription has been successfully redeemed. +
+{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToEnterpriseUser.text.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToEnterpriseUser.text.hbs new file mode 100644 index 000000000..d02063ea3 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToEnterpriseUser.text.hbs @@ -0,0 +1,3 @@ +{{#>BasicTextLayout}} +Your Families subscription has been successfully redeemed. +{{/BasicTextLayout}} diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToFamilyUser.html.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToFamilyUser.html.hbs new file mode 100644 index 000000000..c2ed47156 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToFamilyUser.html.hbs @@ -0,0 +1,9 @@ +{{#>FullHtmlLayout}} + + + + +
+ Your Free Families subscription has been successfully accepted. Your subscription is free as long as the sponsoring member continues to have a qualifying Bitwarden subscription. +
+{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToFamilyUser.text.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToFamilyUser.text.hbs new file mode 100644 index 000000000..96da5b17f --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToFamilyUser.text.hbs @@ -0,0 +1,3 @@ +{{#>BasicTextLayout}} +Your Families subscription has been successfully activated. Your subscription is free as long as the sponsoring member continues to have a qualifying Bitwarden subscription. +{{/BasicTextLayout}} diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipReverting.html.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipReverting.html.hbs new file mode 100644 index 000000000..51741015c --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipReverting.html.hbs @@ -0,0 +1,9 @@ +{{#>FullHtmlLayout}} + + + + +
+ Your Families for Enterprise sponsorship will revert back to your existing payment method at the end of the current billing cycle. +
+{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipReverting.text.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipReverting.text.hbs new file mode 100644 index 000000000..d711cf9e3 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipReverting.text.hbs @@ -0,0 +1,3 @@ +{{#>BasicTextLayout}} +Your Families for Enterprise sponsorship will revert back to your existing payment method at the end of the current billing cycle. +{{/BasicTextLayout}} diff --git a/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.html.hbs b/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.html.hbs index 99c1ae93e..249104059 100644 --- a/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.html.hbs +++ b/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.html.hbs @@ -15,6 +15,10 @@ If you do not wish to join this organization, you can safely ignore this email. + {{#if OrganizationCanSponsor}} +
+ Did you know? Members of {{OrganizationName}} receive a complimentary Families subscription. Learn more at the following link: https://bitwarden.com/help/article/families-for-enterprise/ + {{/if}} diff --git a/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.text.hbs b/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.text.hbs index ed0ec4261..4c61adbe5 100644 --- a/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.text.hbs +++ b/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.text.hbs @@ -6,4 +6,7 @@ You have been invited to join the {{OrganizationName}} organization. To accept t This link expires on {{ExpirationDate}}. If you do not wish to join this organization, you can safely ignore this email. +{{#if OrganizationCanSponsor}} +Did you know? Members of {{OrganizationName}} receive a complimentary Families subscription. Learn more here: https://bitwarden.com/help/article/families-for-enterprise/ +{{/if}} {{/BasicTextLayout}} diff --git a/src/Core/Models/Api/Request/Organizations/OrganizationSponsorshipRedeemRequestModel.cs b/src/Core/Models/Api/Request/Organizations/OrganizationSponsorshipRedeemRequestModel.cs new file mode 100644 index 000000000..08012746e --- /dev/null +++ b/src/Core/Models/Api/Request/Organizations/OrganizationSponsorshipRedeemRequestModel.cs @@ -0,0 +1,14 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Bit.Core.Enums; + +namespace Bit.Core.Models.Api +{ + public class OrganizationSponsorshipRedeemRequestModel + { + [Required] + public PlanSponsorshipType PlanSponsorshipType { get; set; } + [Required] + public Guid SponsoredOrganizationId { get; set; } + } +} diff --git a/src/Core/Models/Api/Request/Organizations/OrganizationSponsorshipRequestModel.cs b/src/Core/Models/Api/Request/Organizations/OrganizationSponsorshipRequestModel.cs new file mode 100644 index 000000000..deb8d07f3 --- /dev/null +++ b/src/Core/Models/Api/Request/Organizations/OrganizationSponsorshipRequestModel.cs @@ -0,0 +1,21 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Bit.Core.Enums; +using Bit.Core.Utilities; + +namespace Bit.Core.Models.Api.Request +{ + public class OrganizationSponsorshipRequestModel + { + [Required] + public PlanSponsorshipType PlanSponsorshipType { get; set; } + + [Required] + [StringLength(256)] + [StrictEmailAddress] + public string SponsoredEmail { get; set; } + + [StringLength(256)] + public string FriendlyName { get; set; } + } +} diff --git a/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs b/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs index b4476c3af..00df76c4e 100644 --- a/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs +++ b/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs @@ -39,6 +39,12 @@ namespace Bit.Core.Models.Api UserId = organization.UserId?.ToString(); ProviderId = organization.ProviderId?.ToString(); ProviderName = organization.ProviderName; + FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName; + FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null && + Utilities.StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise) + .UsersCanSponsor(organization); + PlanProductType = Utilities.StaticStore.GetPlan(organization.PlanType).Product; + if (organization.SsoConfig != null) { var ssoConfigData = SsoConfigurationData.Deserialize(organization.SsoConfig); @@ -76,6 +82,9 @@ namespace Bit.Core.Models.Api public bool HasPublicAndPrivateKeys { get; set; } public string ProviderId { get; set; } public string ProviderName { get; set; } + public string FamilySponsorshipFriendlyName { get; set; } + public bool FamilySponsorshipAvailable { get; set; } + public ProductType PlanProductType { get; set; } public bool KeyConnectorEnabled { get; set; } public string KeyConnectorUrl { get; set; } } diff --git a/src/Core/Models/Api/Response/SubscriptionResponseModel.cs b/src/Core/Models/Api/Response/SubscriptionResponseModel.cs index d18b3c1cb..dcc592c7a 100644 --- a/src/Core/Models/Api/Response/SubscriptionResponseModel.cs +++ b/src/Core/Models/Api/Response/SubscriptionResponseModel.cs @@ -82,12 +82,14 @@ namespace Bit.Core.Models.Api Amount = item.Amount; Interval = item.Interval; Quantity = item.Quantity; + SponsoredSubscriptionItem = item.SponsoredSubscriptionItem; } public string Name { get; set; } public decimal Amount { get; set; } public int Quantity { get; set; } public string Interval { get; set; } + public bool SponsoredSubscriptionItem { get; set; } } } diff --git a/src/Core/Models/Business/SubscriptionCreateOptions.cs b/src/Core/Models/Business/SubscriptionCreateOptions.cs index 43f5cc367..75086c654 100644 --- a/src/Core/Models/Business/SubscriptionCreateOptions.cs +++ b/src/Core/Models/Business/SubscriptionCreateOptions.cs @@ -52,12 +52,12 @@ namespace Bit.Core.Models.Business if (!string.IsNullOrWhiteSpace(taxInfo?.StripeTaxRateId)) { - DefaultTaxRates = new List{ taxInfo.StripeTaxRateId }; + DefaultTaxRates = new List { taxInfo.StripeTaxRateId }; } } } - public class OrganizationPurchaseSubscriptionOptions : OrganizationSubscriptionOptionsBase + public class OrganizationPurchaseSubscriptionOptions : OrganizationSubscriptionOptionsBase { public OrganizationPurchaseSubscriptionOptions( Organization org, StaticStore.Plan plan, @@ -76,7 +76,7 @@ namespace Bit.Core.Models.Business string customerId, Organization org, StaticStore.Plan plan, TaxInfo taxInfo, int additionalSeats = 0, int additionalStorageGb = 0, - bool premiumAccessAddon = false) : + bool premiumAccessAddon = false) : base(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon) { Customer = customerId; diff --git a/src/Core/Models/Business/SubscriptionInfo.cs b/src/Core/Models/Business/SubscriptionInfo.cs index 2d3989450..57b102ed6 100644 --- a/src/Core/Models/Business/SubscriptionInfo.cs +++ b/src/Core/Models/Business/SubscriptionInfo.cs @@ -52,12 +52,14 @@ namespace Bit.Core.Models.Business } Quantity = (int)item.Quantity; + SponsoredSubscriptionItem = Utilities.StaticStore.SponsoredPlans.Any(p => p.StripePlanId == item.Plan.Id); } public string Name { get; set; } public decimal Amount { get; set; } public int Quantity { get; set; } public string Interval { get; set; } + public bool SponsoredSubscriptionItem { get; set; } } } diff --git a/src/Core/Models/Business/SubscriptionUpdate.cs b/src/Core/Models/Business/SubscriptionUpdate.cs index f4aaedf8b..f4c682fd2 100644 --- a/src/Core/Models/Business/SubscriptionUpdate.cs +++ b/src/Core/Models/Business/SubscriptionUpdate.cs @@ -1,3 +1,4 @@ +using System.Collections.Generic; using System.Linq; using Bit.Core.Models.Table; using Stripe; @@ -6,16 +7,28 @@ namespace Bit.Core.Models.Business { public abstract class SubscriptionUpdate { - protected abstract string PlanId { get; } + protected abstract List PlanIds { get; } - public abstract SubscriptionItemOptions RevertItemOptions(Subscription subscription); - public abstract SubscriptionItemOptions UpgradeItemOptions(Subscription subscription); + public abstract List RevertItemsOptions(Subscription subscription); + public abstract List UpgradeItemsOptions(Subscription subscription); - public bool UpdateNeeded(Subscription subscription) => - (SubscriptionItem(subscription)?.Quantity ?? 0) != (UpgradeItemOptions(subscription).Quantity ?? 0); + public bool UpdateNeeded(Subscription subscription) + { + var upgradeItemsOptions = UpgradeItemsOptions(subscription); + foreach (var upgradeItemOptions in upgradeItemsOptions) + { + var upgradeQuantity = upgradeItemOptions.Quantity ?? 0; + var existingQuantity = SubscriptionItem(subscription, upgradeItemOptions.Plan)?.Quantity ?? 0; + if (upgradeQuantity != existingQuantity) + { + return true; + } + } + return false; + } - protected SubscriptionItem SubscriptionItem(Subscription subscription) => - subscription.Items?.Data?.FirstOrDefault(i => i.Plan.Id == PlanId); + protected static SubscriptionItem SubscriptionItem(Subscription subscription, string planId) => + planId == null ? null : subscription.Items?.Data?.FirstOrDefault(i => i.Plan.Id == planId); } @@ -24,7 +37,7 @@ namespace Bit.Core.Models.Business private readonly Organization _organization; private readonly StaticStore.Plan _plan; private readonly long? _additionalSeats; - protected override string PlanId => _plan.StripeSeatPlanId; + protected override List PlanIds => new() { _plan.StripeSeatPlanId }; public SeatSubscriptionUpdate(Organization organization, StaticStore.Plan plan, long? additionalSeats) { @@ -33,27 +46,33 @@ namespace Bit.Core.Models.Business _additionalSeats = additionalSeats; } - public override SubscriptionItemOptions UpgradeItemOptions(Subscription subscription) + public override List UpgradeItemsOptions(Subscription subscription) { - var item = SubscriptionItem(subscription); - return new SubscriptionItemOptions + var item = SubscriptionItem(subscription, PlanIds.Single()); + return new() { - Id = item?.Id, - Plan = PlanId, - Quantity = _additionalSeats, - Deleted = (item?.Id != null && _additionalSeats == 0) ? true : (bool?)null, + new SubscriptionItemOptions + { + Id = item?.Id, + Plan = PlanIds.Single(), + Quantity = _additionalSeats, + Deleted = (item?.Id != null && _additionalSeats == 0) ? true : (bool?)null, + } }; } - public override SubscriptionItemOptions RevertItemOptions(Subscription subscription) + public override List RevertItemsOptions(Subscription subscription) { - var item = SubscriptionItem(subscription); - return new SubscriptionItemOptions + var item = SubscriptionItem(subscription, PlanIds.Single()); + return new() { - Id = item?.Id, - Plan = PlanId, - Quantity = _organization.Seats, - Deleted = item?.Id != null ? true : (bool?)null, + new SubscriptionItemOptions + { + Id = item?.Id, + Plan = PlanIds.Single(), + Quantity = _organization.Seats, + Deleted = item?.Id != null ? true : (bool?)null, + } }; } } @@ -62,7 +81,7 @@ namespace Bit.Core.Models.Business { private readonly string _plan; private readonly long? _additionalStorage; - protected override string PlanId => _plan; + protected override List PlanIds => new() { _plan }; public StorageSubscriptionUpdate(string plan, long? additionalStorage) { @@ -70,28 +89,115 @@ namespace Bit.Core.Models.Business _additionalStorage = additionalStorage; } - public override SubscriptionItemOptions UpgradeItemOptions(Subscription subscription) + public override List UpgradeItemsOptions(Subscription subscription) { - var item = SubscriptionItem(subscription); - return new SubscriptionItemOptions + var item = SubscriptionItem(subscription, PlanIds.Single()); + return new() { - Id = item?.Id, - Plan = _plan, - Quantity = _additionalStorage, - Deleted = (item?.Id != null && _additionalStorage == 0) ? true : (bool?)null, + new SubscriptionItemOptions + { + Id = item?.Id, + Plan = _plan, + Quantity = _additionalStorage, + Deleted = (item?.Id != null && _additionalStorage == 0) ? true : (bool?)null, + } }; } - public override SubscriptionItemOptions RevertItemOptions(Subscription subscription) + public override List RevertItemsOptions(Subscription subscription) { - var item = SubscriptionItem(subscription); - return new SubscriptionItemOptions + var item = SubscriptionItem(subscription, PlanIds.Single()); + return new() { - Id = item?.Id, - Plan = _plan, - Quantity = item?.Quantity ?? 0, - Deleted = item?.Id != null ? true : (bool?)null, + new SubscriptionItemOptions + { + Id = item?.Id, + Plan = _plan, + Quantity = item?.Quantity ?? 0, + Deleted = item?.Id != null ? true : (bool?)null, + } }; } } + + public class SponsorOrganizationSubscriptionUpdate : SubscriptionUpdate + { + private readonly string _existingPlanStripeId; + private readonly string _sponsoredPlanStripeId; + private readonly bool _applySponsorship; + protected override List PlanIds => new() { _existingPlanStripeId, _sponsoredPlanStripeId }; + + public SponsorOrganizationSubscriptionUpdate(StaticStore.Plan existingPlan, StaticStore.SponsoredPlan sponsoredPlan, bool applySponsorship) + { + _existingPlanStripeId = existingPlan.StripePlanId; + _sponsoredPlanStripeId = sponsoredPlan?.StripePlanId; + _applySponsorship = applySponsorship; + } + + public override List RevertItemsOptions(Subscription subscription) + { + var result = new List(); + if (!string.IsNullOrWhiteSpace(AddStripePlanId)) + { + result.Add(new SubscriptionItemOptions + { + Id = AddStripeItem(subscription)?.Id, + Plan = AddStripePlanId, + Quantity = 0, + Deleted = true, + }); + } + + if (!string.IsNullOrWhiteSpace(RemoveStripePlanId)) + { + result.Add(new SubscriptionItemOptions + { + Id = RemoveStripeItem(subscription)?.Id, + Plan = RemoveStripePlanId, + Quantity = 1, + Deleted = false, + }); + } + return result; + } + + public override List UpgradeItemsOptions(Subscription subscription) + { + var result = new List(); + if (RemoveStripeItem(subscription) != null) + { + result.Add(new SubscriptionItemOptions + { + Id = RemoveStripeItem(subscription)?.Id, + Plan = RemoveStripePlanId, + Quantity = 0, + Deleted = true, + }); + } + + if (!string.IsNullOrWhiteSpace(AddStripePlanId)) + { + result.Add(new SubscriptionItemOptions + { + Id = AddStripeItem(subscription)?.Id, + Plan = AddStripePlanId, + Quantity = 1, + Deleted = false, + }); + } + return result; + } + + private string RemoveStripePlanId => _applySponsorship ? _existingPlanStripeId : _sponsoredPlanStripeId; + private string AddStripePlanId => _applySponsorship ? _sponsoredPlanStripeId : _existingPlanStripeId; + private Stripe.SubscriptionItem RemoveStripeItem(Subscription subscription) => + _applySponsorship ? + SubscriptionItem(subscription, _existingPlanStripeId) : + SubscriptionItem(subscription, _sponsoredPlanStripeId); + private Stripe.SubscriptionItem AddStripeItem(Subscription subscription) => + _applySponsorship ? + SubscriptionItem(subscription, _sponsoredPlanStripeId) : + SubscriptionItem(subscription, _existingPlanStripeId); + + } } diff --git a/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs b/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs index 06a3ba512..78313c2c2 100644 --- a/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs +++ b/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs @@ -26,6 +26,7 @@ namespace Bit.Core.Models.Data public Enums.OrganizationUserStatusType Status { get; set; } public Enums.OrganizationUserType Type { get; set; } public bool Enabled { get; set; } + public Enums.PlanType PlanType { get; set; } public string SsoExternalId { get; set; } public string Identifier { get; set; } public string Permissions { get; set; } @@ -34,6 +35,7 @@ namespace Bit.Core.Models.Data public string PrivateKey { get; set; } public Guid? ProviderId { get; set; } public string ProviderName { get; set; } + public string FamilySponsorshipFriendlyName { get; set; } public string SsoConfig { get; set; } } } diff --git a/src/Core/Models/EntityFramework/OrganizationSponsorship.cs b/src/Core/Models/EntityFramework/OrganizationSponsorship.cs new file mode 100644 index 000000000..53dcd3e9e --- /dev/null +++ b/src/Core/Models/EntityFramework/OrganizationSponsorship.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using AutoMapper; + +namespace Bit.Core.Models.EntityFramework +{ + public class OrganizationSponsorship : Table.OrganizationSponsorship + { + public virtual Installation Installation { get; set; } + public virtual Organization SponsoringOrganization { get; set; } + public virtual Organization SponsoredOrganization { get; set; } + } + + public class OrganizationSponsorshipMapperProfile : Profile + { + public OrganizationSponsorshipMapperProfile() + { + CreateMap().ReverseMap(); + } + } +} diff --git a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseOfferExistingAccountViewModel.cs b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseOfferExistingAccountViewModel.cs new file mode 100644 index 000000000..8d25a3377 --- /dev/null +++ b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseOfferExistingAccountViewModel.cs @@ -0,0 +1,10 @@ +namespace Bit.Core.Models.Mail.FamiliesForEnterprise +{ + public class FamiliesForEnterpriseOfferExistingAccountViewModel : BaseMailModel + { + public string SponsorEmail { get; set; } + public string SponsoredEmail { get; set; } + public string SponsorshipToken { get; set; } + public string Url => $"{WebVaultUrl}/?sponsorshipToken={SponsorshipToken}&email={SponsoredEmail}"; + } +} diff --git a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseOfferNewAccountViewModel.cs b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseOfferNewAccountViewModel.cs new file mode 100644 index 000000000..72999fefd --- /dev/null +++ b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseOfferNewAccountViewModel.cs @@ -0,0 +1,10 @@ +namespace Bit.Core.Models.Mail.FamiliesForEnterprise +{ + public class FamiliesForEnterpriseOfferNewAccountViewModel : BaseMailModel + { + public string SponsorEmail { get; set; } + public string SponsoredEmail { get; set; } + public string SponsorshipToken { get; set; } + public string Url => $"{WebVaultUrl}/register?sponsorshipToken={SponsorshipToken}&email={SponsoredEmail}"; + } +} diff --git a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipRevertingViewModel.cs b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipRevertingViewModel.cs new file mode 100644 index 000000000..e8185000b --- /dev/null +++ b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipRevertingViewModel.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Models.Mail.FamiliesForEnterprise +{ + public class FamiliesForEnterpriseSponsorshipRevertingViewModel : BaseMailModel + { + public string OrganizationName { get; set; } + } +} diff --git a/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs b/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs index 36509f5bb..b281e96d3 100644 --- a/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs +++ b/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs @@ -11,6 +11,7 @@ namespace Bit.Core.Models.Mail public string OrganizationNameUrlEncoded { get; set; } public string Token { get; set; } public string ExpirationDate { get; set; } + public bool OrganizationCanSponsor { get; set; } public string Url => string.Format("{0}/accept-organization?organizationId={1}&" + "organizationUserId={2}&email={3}&organizationName={4}&token={5}", WebVaultUrl, diff --git a/src/Core/Models/StaticStore/SponsoredPlan.cs b/src/Core/Models/StaticStore/SponsoredPlan.cs new file mode 100644 index 000000000..f9c7b2b6b --- /dev/null +++ b/src/Core/Models/StaticStore/SponsoredPlan.cs @@ -0,0 +1,15 @@ +using System; +using Bit.Core.Enums; +using Bit.Core.Models.Data; + +namespace Bit.Core.Models.StaticStore +{ + public class SponsoredPlan + { + public PlanSponsorshipType PlanSponsorshipType { get; set; } + public ProductType SponsoredProductType { get; set; } + public ProductType SponsoringProductType { get; set; } + public string StripePlanId { get; set; } + public Func UsersCanSponsor { get; set; } + } +} diff --git a/src/Core/Models/Table/OrganizationSponsorship.cs b/src/Core/Models/Table/OrganizationSponsorship.cs new file mode 100644 index 000000000..11be3c66c --- /dev/null +++ b/src/Core/Models/Table/OrganizationSponsorship.cs @@ -0,0 +1,31 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Bit.Core.Enums; +using Bit.Core.Utilities; + +namespace Bit.Core.Models.Table +{ + public class OrganizationSponsorship : ITableObject + { + public Guid Id { get; set; } + public Guid? InstallationId { get; set; } + public Guid? SponsoringOrganizationId { get; set; } + public Guid? SponsoringOrganizationUserId { get; set; } + public Guid? SponsoredOrganizationId { get; set; } + [MaxLength(256)] + public string FriendlyName { get; set; } + [MaxLength(256)] + public string OfferedToEmail { get; set; } + public PlanSponsorshipType? PlanSponsorshipType { get; set; } + [Required] + public bool CloudSponsor { get; set; } + public DateTime? LastSyncDate { get; set; } + public byte TimesRenewedWithoutValidation { get; set; } + public DateTime? SponsorshipLapsedDate { get; set; } + + public void SetNewId() + { + Id = CoreHelpers.GenerateComb(); + } + } +} diff --git a/src/Core/Repositories/EntityFramework/DatabaseContext.cs b/src/Core/Repositories/EntityFramework/DatabaseContext.cs index fcc8275f9..0131ab1c8 100644 --- a/src/Core/Repositories/EntityFramework/DatabaseContext.cs +++ b/src/Core/Repositories/EntityFramework/DatabaseContext.cs @@ -26,6 +26,7 @@ namespace Bit.Core.Repositories.EntityFramework public DbSet GroupUsers { get; set; } public DbSet Installations { get; set; } public DbSet Organizations { get; set; } + public DbSet OrganizationSponsorships { get; set; } public DbSet OrganizationUsers { get; set; } public DbSet Policies { get; set; } public DbSet Providers { get; set; } @@ -55,6 +56,7 @@ namespace Bit.Core.Repositories.EntityFramework var eGroupUser = builder.Entity(); var eInstallation = builder.Entity(); var eOrganization = builder.Entity(); + var eOrganizationSponsorship = builder.Entity(); var eOrganizationUser = builder.Entity(); var ePolicy = builder.Entity(); var eProvider = builder.Entity(); @@ -76,6 +78,7 @@ namespace Bit.Core.Repositories.EntityFramework eGroup.Property(c => c.Id).ValueGeneratedNever(); eInstallation.Property(c => c.Id).ValueGeneratedNever(); eOrganization.Property(c => c.Id).ValueGeneratedNever(); + eOrganizationSponsorship.Property(c => c.Id).ValueGeneratedNever(); eOrganizationUser.Property(c => c.Id).ValueGeneratedNever(); ePolicy.Property(c => c.Id).ValueGeneratedNever(); eProvider.Property(c => c.Id).ValueGeneratedNever(); @@ -115,6 +118,7 @@ namespace Bit.Core.Repositories.EntityFramework eGroupUser.ToTable(nameof(GroupUser)); eInstallation.ToTable(nameof(Installation)); eOrganization.ToTable(nameof(Organization)); + eOrganizationSponsorship.ToTable(nameof(OrganizationSponsorship)); eOrganizationUser.ToTable(nameof(OrganizationUser)); ePolicy.ToTable(nameof(Policy)); eProvider.ToTable(nameof(Provider)); diff --git a/src/Core/Repositories/EntityFramework/OrganizationRepository.cs b/src/Core/Repositories/EntityFramework/OrganizationRepository.cs index d6eb20cc0..3c73c646d 100644 --- a/src/Core/Repositories/EntityFramework/OrganizationRepository.cs +++ b/src/Core/Repositories/EntityFramework/OrganizationRepository.cs @@ -96,5 +96,29 @@ namespace Bit.Core.Repositories.EntityFramework { await OrganizationUpdateStorage(id); } + + public override async Task DeleteAsync(Organization organization) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var orgEntity = await dbContext.FindAsync(organization.Id); + var sponsorships = dbContext.OrganizationSponsorships + .Where(os => + os.SponsoringOrganizationId == organization.Id || + os.SponsoredOrganizationId == organization.Id); + dbContext.RemoveRange(sponsorships.Where(os => os.CloudSponsor)); + + Guid? UpdatedOrgId(Guid? orgId) => orgId == organization.Id ? null : organization.Id; + foreach (var sponsorship in sponsorships.Where(os => !os.CloudSponsor)) + { + sponsorship.SponsoredOrganizationId = UpdatedOrgId(sponsorship.SponsoredOrganizationId); + sponsorship.SponsoringOrganizationId = UpdatedOrgId(sponsorship.SponsoringOrganizationId); + } + + dbContext.Remove(orgEntity); + await dbContext.SaveChangesAsync(); + } + } } } diff --git a/src/Core/Repositories/EntityFramework/OrganizationSponsorshipRepository.cs b/src/Core/Repositories/EntityFramework/OrganizationSponsorshipRepository.cs new file mode 100644 index 000000000..9fed4b0c5 --- /dev/null +++ b/src/Core/Repositories/EntityFramework/OrganizationSponsorshipRepository.cs @@ -0,0 +1,52 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoMapper; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using EFModel = Bit.Core.Models.EntityFramework; +using TableModel = Bit.Core.Models.Table; + +namespace Bit.Core.Repositories.EntityFramework +{ + public class OrganizationSponsorshipRepository : Repository, IOrganizationSponsorshipRepository + { + public OrganizationSponsorshipRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) + : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.OrganizationSponsorships) + { } + + public async Task GetByOfferedToEmailAsync(string email) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var orgSponsorship = await GetDbSet(dbContext).Where(e => e.OfferedToEmail == email) + .FirstOrDefaultAsync(); + return orgSponsorship; + } + } + + public async Task GetBySponsoredOrganizationIdAsync(Guid sponsoredOrganizationId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var orgSponsorship = await GetDbSet(dbContext).Where(e => e.SponsoredOrganizationId == sponsoredOrganizationId) + .FirstOrDefaultAsync(); + return orgSponsorship; + } + } + + public async Task GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var orgSponsorship = await GetDbSet(dbContext).Where(e => e.SponsoringOrganizationUserId == sponsoringOrganizationUserId) + .FirstOrDefaultAsync(); + return orgSponsorship; + } + } + } +} diff --git a/src/Core/Repositories/EntityFramework/OrganizationUserRepository.cs b/src/Core/Repositories/EntityFramework/OrganizationUserRepository.cs index 17ea260b4..8cc3f4f2e 100644 --- a/src/Core/Repositories/EntityFramework/OrganizationUserRepository.cs +++ b/src/Core/Repositories/EntityFramework/OrganizationUserRepository.cs @@ -67,12 +67,32 @@ namespace Bit.Core.Repositories.EntityFramework return organizationUsers.Select(u => u.Id).ToList(); } + public override async Task DeleteAsync(OrganizationUser organizationUser) => await DeleteAsync(organizationUser.Id); + public async Task DeleteAsync(Guid organizationUserId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var orgUser = await dbContext.FindAsync(organizationUserId); + var sponsorships = dbContext.OrganizationSponsorships + .Where(os => os.SponsoringOrganizationUserId != default && + os.SponsoringOrganizationUserId.Value == organizationUserId); + dbContext.RemoveRange(sponsorships); + dbContext.Remove(orgUser); + await dbContext.SaveChangesAsync(); + } + } + public async Task DeleteManyAsync(IEnumerable organizationUserIds) { using (var scope = ServiceScopeFactory.CreateScope()) { var dbContext = GetDatabaseContext(scope); var entities = dbContext.FindAsync(organizationUserIds); + var sponsorships = dbContext.OrganizationSponsorships + .Where(os => os.SponsoringOrganizationUserId != default && + organizationUserIds.Contains(os.SponsoringOrganizationUserId ?? default)); + dbContext.RemoveRange(sponsorships); dbContext.RemoveRange(entities); await dbContext.SaveChangesAsync(); } diff --git a/src/Core/Repositories/EntityFramework/Queries/OrganizationUserOrganizationDetailsViewQuery.cs b/src/Core/Repositories/EntityFramework/Queries/OrganizationUserOrganizationDetailsViewQuery.cs index d48f0cfc6..2442191d3 100644 --- a/src/Core/Repositories/EntityFramework/Queries/OrganizationUserOrganizationDetailsViewQuery.cs +++ b/src/Core/Repositories/EntityFramework/Queries/OrganizationUserOrganizationDetailsViewQuery.cs @@ -17,16 +17,20 @@ namespace Bit.Core.Repositories.EntityFramework.Queries from po in po_g.DefaultIfEmpty() join p in dbContext.Providers on po.ProviderId equals p.Id into p_g from p in p_g.DefaultIfEmpty() + join os in dbContext.OrganizationSponsorships on ou.Id equals os.SponsoringOrganizationUserId into os_g + from os in os_g.DefaultIfEmpty() join ss in dbContext.SsoConfigs on ou.OrganizationId equals ss.OrganizationId into ss_g from ss in ss_g.DefaultIfEmpty() where ((su == null || !su.OrganizationId.HasValue) || su.OrganizationId == ou.OrganizationId) - select new { ou, o, su, p, ss }; + select new { ou, o, su, p, ss, os }; + return query.Select(x => new OrganizationUserOrganizationDetails { OrganizationId = x.ou.OrganizationId, UserId = x.ou.UserId, Name = x.o.Name, Enabled = x.o.Enabled, + PlanType = x.o.PlanType, UsePolicies = x.o.UsePolicies, UseSso = x.o.UseSso, UseKeyConnector = x.o.UseKeyConnector, @@ -52,6 +56,7 @@ namespace Bit.Core.Repositories.EntityFramework.Queries PrivateKey = x.o.PrivateKey, ProviderId = x.p.Id, ProviderName = x.p.Name, + FamilySponsorshipFriendlyName = x.os.FriendlyName, SsoConfig = x.ss.Data, }); } diff --git a/src/Core/Repositories/IOrganizationSponsorshipRepository.cs b/src/Core/Repositories/IOrganizationSponsorshipRepository.cs new file mode 100644 index 000000000..9d81cae91 --- /dev/null +++ b/src/Core/Repositories/IOrganizationSponsorshipRepository.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Models.Table; + +namespace Bit.Core.Repositories +{ + public interface IOrganizationSponsorshipRepository : IRepository + { + Task GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId); + Task GetBySponsoredOrganizationIdAsync(Guid sponsoredOrganizationId); + Task GetByOfferedToEmailAsync(string email); + } +} diff --git a/src/Core/Repositories/SqlServer/OrganizationSponsorshipRepository.cs b/src/Core/Repositories/SqlServer/OrganizationSponsorshipRepository.cs new file mode 100644 index 000000000..c759e906b --- /dev/null +++ b/src/Core/Repositories/SqlServer/OrganizationSponsorshipRepository.cs @@ -0,0 +1,67 @@ +using System; +using System.Data; +using System.Data.SqlClient; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Models.Table; +using Bit.Core.Settings; +using Dapper; + +namespace Bit.Core.Repositories.SqlServer +{ + public class OrganizationSponsorshipRepository : Repository, IOrganizationSponsorshipRepository + { + public OrganizationSponsorshipRepository(GlobalSettings globalSettings) + : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + { } + + public OrganizationSponsorshipRepository(string connectionString, string readOnlyConnectionString) + : base(connectionString, readOnlyConnectionString) + { } + + public async Task GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationUserId]", + new + { + SponsoringOrganizationUserId = sponsoringOrganizationUserId + }, + commandType: CommandType.StoredProcedure); + + return results.SingleOrDefault(); + } + } + + public async Task GetBySponsoredOrganizationIdAsync(Guid sponsoredOrganizationId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[OrganizationSponsorship_ReadBySponsoredOrganizationId]", + new { SponsoredOrganizationId = sponsoredOrganizationId }, + commandType: CommandType.StoredProcedure); + + return results.SingleOrDefault(); + } + } + + public async Task GetByOfferedToEmailAsync(string offeredToEmail) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[OrganizationSponsorship_ReadByOfferedToEmail]", + new + { + OfferedToEmail = offeredToEmail + }, + commandType: CommandType.StoredProcedure); + + return results.SingleOrDefault(); + } + } + } +} diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 433a21bf1..50f954049 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -18,8 +18,8 @@ namespace Bit.Core.Services Task SendTwoFactorEmailAsync(string email, string token); Task SendNoMasterPasswordHintEmailAsync(string email); Task SendMasterPasswordHintEmailAsync(string email, string hint); - Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token); - Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites); + Task SendOrganizationInviteEmailAsync(string organizationName, bool orgCanSponsor, OrganizationUser orgUser, ExpiringToken token); + Task BulkSendOrganizationInviteEmailAsync(string organizationName, bool orgCanSponsor, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites); Task SendOrganizationMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable ownerEmails); Task SendOrganizationAutoscaledEmailAsync(Organization organization, int initialSeatCount, IEnumerable ownerEmails); Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier, IEnumerable adminEmails); @@ -49,6 +49,9 @@ namespace Bit.Core.Services Task SendProviderConfirmedEmailAsync(string providerName, string email); Task SendProviderUserRemoved(string providerName, string email); Task SendUpdatedTempPasswordEmailAsync(string email, string userName); + Task SendFamiliesForEnterpriseOfferEmailAsync(string email, string sponsorEmail, bool existingAccount, string token); + Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail); + Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, string familyOrgName); Task SendOTPEmailAsync(string email, string token); } } diff --git a/src/Core/Services/IOrganizationSponsorshipService.cs b/src/Core/Services/IOrganizationSponsorshipService.cs new file mode 100644 index 000000000..ec86688d2 --- /dev/null +++ b/src/Core/Services/IOrganizationSponsorshipService.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Enums; +using Bit.Core.Models.Table; + +namespace Bit.Core.Services +{ + public interface IOrganizationSponsorshipService + { + Task ValidateRedemptionTokenAsync(string encryptedToken); + Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, + PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName, string sponsoringUserEmail); + Task ResendSponsorshipOfferAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, + OrganizationSponsorship sponsorship, string sponsoringUserEmail); + Task SendSponsorshipOfferAsync(OrganizationSponsorship sponsorship, string sponsoringOrgUserEmail); + Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship, + Organization sponsoredOrganization); + Task ValidateSponsorshipAsync(Guid sponsoredOrganizationId); + Task RevokeSponsorshipAsync(Organization sponsoredOrganization, OrganizationSponsorship sponsorship); + Task RemoveSponsorshipAsync(Organization sponsoredOrganization, OrganizationSponsorship sponsorship); + } +} diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index 5f4a5ecc3..2bb515a21 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -2,6 +2,7 @@ using System.Threading.Tasks; using Bit.Core.Models.Table; using Bit.Core.Models.Business; +using Bit.Core.Models.StaticStore; using Bit.Core.Enums; namespace Bit.Core.Services @@ -10,13 +11,15 @@ namespace Bit.Core.Services { Task CancelAndRecoverChargesAsync(ISubscriber subscriber); Task PurchaseOrganizationAsync(Organization org, PaymentMethodType paymentMethodType, - string paymentToken, Models.StaticStore.Plan plan, short additionalStorageGb, int additionalSeats, + string paymentToken, Plan plan, short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo); - Task UpgradeFreeOrganizationAsync(Organization org, Models.StaticStore.Plan plan, + Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship); + Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship); + Task UpgradeFreeOrganizationAsync(Organization org, Plan plan, short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo); Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken, short additionalStorageGb, TaxInfo taxInfo); - Task AdjustSeatsAsync(Organization organization, Models.StaticStore.Plan plan, int additionalSeats, DateTime? prorationDate = null); + Task AdjustSeatsAsync(Organization organization, Plan plan, int additionalSeats, DateTime? prorationDate = null); Task AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId, DateTime? prorationDate = null); Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false, bool skipInAppPurchaseCheck = false); diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 4c6d1ec8f..d8264431b 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -9,6 +9,7 @@ using System.Net; using Bit.Core.Utilities; using System.Linq; using System.Reflection; +using Bit.Core.Models.Mail.FamiliesForEnterprise; using Bit.Core.Models.Mail.Provider; using Bit.Core.Models.Table.Provider; using HandlebarsDotNet; @@ -204,10 +205,10 @@ namespace Bit.Core.Services await _mailDeliveryService.SendEmailAsync(message); } - public Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token) => - BulkSendOrganizationInviteEmailAsync(organizationName, new[] { (orgUser, token) }); + public Task SendOrganizationInviteEmailAsync(string organizationName, bool orgCanSponsor, OrganizationUser orgUser, ExpiringToken token) => + BulkSendOrganizationInviteEmailAsync(organizationName, orgCanSponsor, new[] { (orgUser, token) }); - public async Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites) + public async Task BulkSendOrganizationInviteEmailAsync(string organizationName, bool organizationCanSponsor, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites) { MailQueueMessage CreateMessage(string email, object model) { @@ -227,6 +228,7 @@ namespace Bit.Core.Services OrganizationNameUrlEncoded = WebUtility.UrlEncode(organizationName), WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, SiteName = _globalSettings.SiteName, + OrganizationCanSponsor = organizationCanSponsor, } )); @@ -756,6 +758,78 @@ namespace Bit.Core.Services await _mailDeliveryService.SendEmailAsync(message); } + public async Task SendFamiliesForEnterpriseOfferEmailAsync(string email, string sponsorEmail, bool existingAccount, string token) + { + var message = CreateDefaultMessage("Accept Your Free Families Subscription", email); + + if (existingAccount) + { + var model = new FamiliesForEnterpriseOfferExistingAccountViewModel + { + SponsorEmail = CoreHelpers.ObfuscateEmail(sponsorEmail), + SponsoredEmail = WebUtility.UrlEncode(email), + WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, + SiteName = _globalSettings.SiteName, + SponsorshipToken = token, + }; + + await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseOfferExistingAccount", model); + } + else + { + var model = new FamiliesForEnterpriseOfferNewAccountViewModel + { + SponsorEmail = sponsorEmail, + SponsoredEmail = WebUtility.UrlEncode(email), + WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, + SiteName = _globalSettings.SiteName, + SponsorshipToken = token, + }; + + await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseOfferNewAccount", model); + } + + message.Category = "FamiliesForEnterpriseOffer"; + await _mailDeliveryService.SendEmailAsync(message); + } + + public async Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail) + { + // Email family user + await SendFamiliesForEnterpriseInviteRedeemedToFamilyUserEmailAsync(familyUserEmail); + + // Email enterprise org user + await SendFamiliesForEnterpriseInviteRedeemedToEnterpriseUserEmailAsync(sponsorEmail); + } + + private async Task SendFamiliesForEnterpriseInviteRedeemedToFamilyUserEmailAsync(string email) + { + var message = CreateDefaultMessage("Success! Families Subscription Accepted", email); + await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseRedeemedToFamilyUser", new BaseMailModel()); + message.Category = "FamilyForEnterpriseRedeemedToFamilyUser"; + await _mailDeliveryService.SendEmailAsync(message); + } + + private async Task SendFamiliesForEnterpriseInviteRedeemedToEnterpriseUserEmailAsync(string email) + { + var message = CreateDefaultMessage("Success! Families Subscription Accepted", email); + await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseRedeemedToEnterpriseUser", new BaseMailModel()); + message.Category = "FamilyForEnterpriseRedeemedToEnterpriseUser"; + await _mailDeliveryService.SendEmailAsync(message); + } + + public async Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, string familyOrgName) + { + var message = CreateDefaultMessage($"{familyOrgName} Organization Sponsorship Is No Longer Valid", email); + var model = new FamiliesForEnterpriseSponsorshipRevertingViewModel + { + OrganizationName = CoreHelpers.SanitizeForEmail(familyOrgName, false), + }; + await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseSponsorshipReverting", model); + message.Category = "FamiliesForEnterpriseSponsorshipReverting"; + await _mailDeliveryService.SendEmailAsync(message); + } + public async Task SendOTPEmailAsync(string email, string token) { var message = CreateDefaultMessage("Your Bitwarden Verification Code", email); diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index b222fe724..246263360 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -1262,7 +1262,8 @@ namespace Bit.Core.Services { string MakeToken(OrganizationUser orgUser) => _dataProtector.Protect($"OrganizationUserInvite {orgUser.Id} {orgUser.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}"); - await _mailService.BulkSendOrganizationInviteEmailAsync(organization.Name, + + await _mailService.BulkSendOrganizationInviteEmailAsync(organization.Name, CheckOrganizationCanSponsor(organization), orgUsers.Select(o => (o, new ExpiringToken(MakeToken(o), DateTime.UtcNow.AddDays(5))))); } @@ -1272,7 +1273,15 @@ namespace Bit.Core.Services var nowMillis = CoreHelpers.ToEpocMilliseconds(now); var token = _dataProtector.Protect( $"OrganizationUserInvite {orgUser.Id} {orgUser.Email} {nowMillis}"); - await _mailService.SendOrganizationInviteEmailAsync(organization.Name, orgUser, new ExpiringToken(token, now.AddDays(5))); + + await _mailService.SendOrganizationInviteEmailAsync(organization.Name, CheckOrganizationCanSponsor(organization), orgUser, new ExpiringToken(token, now.AddDays(5))); + } + + + private static bool CheckOrganizationCanSponsor(Organization organization) + { + return StaticStore.GetPlan(organization.PlanType).Product == ProductType.Enterprise + && !organization.SelfHost; } public async Task AcceptUserAsync(Guid organizationUserId, User user, string token, diff --git a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs new file mode 100644 index 000000000..20bcb8aff --- /dev/null +++ b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs @@ -0,0 +1,310 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Table; +using Bit.Core.Repositories; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.DataProtection; + +namespace Bit.Core.Services +{ + public class OrganizationSponsorshipService : IOrganizationSponsorshipService + { + private const string FamiliesForEnterpriseTokenName = "FamiliesForEnterpriseToken"; + private const string TokenClearTextPrefix = "BWOrganizationSponsorship_"; + + private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository; + private readonly IOrganizationRepository _organizationRepository; + private readonly IUserRepository _userRepository; + private readonly IPaymentService _paymentService; + private readonly IMailService _mailService; + + private readonly IDataProtector _dataProtector; + + public OrganizationSponsorshipService(IOrganizationSponsorshipRepository organizationSponsorshipRepository, + IOrganizationRepository organizationRepository, + IUserRepository userRepository, + IPaymentService paymentService, + IMailService mailService, + IDataProtectionProvider dataProtectionProvider) + { + _organizationSponsorshipRepository = organizationSponsorshipRepository; + _organizationRepository = organizationRepository; + _userRepository = userRepository; + _paymentService = paymentService; + _mailService = mailService; + _dataProtector = dataProtectionProvider.CreateProtector("OrganizationSponsorshipServiceDataProtector"); + } + + public async Task ValidateRedemptionTokenAsync(string encryptedToken) + { + if (!encryptedToken.StartsWith(TokenClearTextPrefix)) + { + return false; + } + + var decryptedToken = _dataProtector.Unprotect(encryptedToken[TokenClearTextPrefix.Length..]); + var dataParts = decryptedToken.Split(' '); + + if (dataParts.Length != 3) + { + return false; + } + + if (dataParts[0].Equals(FamiliesForEnterpriseTokenName)) + { + if (!Guid.TryParse(dataParts[1], out Guid sponsorshipId) || + !Enum.TryParse(dataParts[2], true, out var sponsorshipType)) + { + return false; + } + + var sponsorship = await _organizationSponsorshipRepository.GetByIdAsync(sponsorshipId); + if (sponsorship == null || sponsorship.PlanSponsorshipType != sponsorshipType) + { + return false; + } + + return true; + } + + return false; + } + + private string RedemptionToken(Guid sponsorshipId, PlanSponsorshipType sponsorshipType) => + string.Concat( + TokenClearTextPrefix, + _dataProtector.Protect($"{FamiliesForEnterpriseTokenName} {sponsorshipId} {sponsorshipType}") + ); + + public async Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, + PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName, string sponsoringUserEmail) + { + var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(sponsorshipType)?.SponsoringProductType; + if (requiredSponsoringProductType == null || + sponsoringOrg == null || + StaticStore.GetPlan(sponsoringOrg.PlanType).Product != requiredSponsoringProductType.Value) + { + throw new BadRequestException("Specified Organization cannot sponsor other organizations."); + } + + if (sponsoringOrgUser == null || sponsoringOrgUser.Status != OrganizationUserStatusType.Confirmed) + { + throw new BadRequestException("Only confirmed users can sponsor other organizations."); + } + + var existingOrgSponsorship = await _organizationSponsorshipRepository + .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id); + if (existingOrgSponsorship != null) + { + throw new BadRequestException("Can only sponsor one organization per Organization User."); + } + + var sponsorship = new OrganizationSponsorship + { + SponsoringOrganizationId = sponsoringOrg.Id, + SponsoringOrganizationUserId = sponsoringOrgUser.Id, + FriendlyName = friendlyName, + OfferedToEmail = sponsoredEmail, + PlanSponsorshipType = sponsorshipType, + CloudSponsor = true, + }; + + try + { + sponsorship = await _organizationSponsorshipRepository.CreateAsync(sponsorship); + + await SendSponsorshipOfferAsync(sponsorship, sponsoringUserEmail); + } + catch + { + if (sponsorship.Id != default) + { + await _organizationSponsorshipRepository.DeleteAsync(sponsorship); + } + throw; + } + } + + public async Task ResendSponsorshipOfferAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, + OrganizationSponsorship sponsorship, string sponsoringUserEmail) + { + if (sponsoringOrg == null) + { + throw new BadRequestException("Cannot find the requested sponsoring organization."); + } + + if (sponsoringOrgUser == null || sponsoringOrgUser.Status != OrganizationUserStatusType.Confirmed) + { + throw new BadRequestException("Only confirmed users can sponsor other organizations."); + } + + if (sponsorship == null || sponsorship.OfferedToEmail == null) + { + throw new BadRequestException("Cannot find an outstanding sponsorship offer for this organization."); + } + + await SendSponsorshipOfferAsync(sponsorship, sponsoringUserEmail); + } + + public async Task SendSponsorshipOfferAsync(OrganizationSponsorship sponsorship, string sponsoringEmail) + { + var user = await _userRepository.GetByEmailAsync(sponsorship.OfferedToEmail); + var isExistingAccount = user != null; + + await _mailService.SendFamiliesForEnterpriseOfferEmailAsync(sponsorship.OfferedToEmail, sponsoringEmail, + isExistingAccount, RedemptionToken(sponsorship.Id, sponsorship.PlanSponsorshipType.Value)); + } + + public async Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship, + Organization sponsoredOrganization) + { + if (sponsorship == null) + { + throw new BadRequestException("No unredeemed sponsorship offer exists for you."); + } + + var existingOrgSponsorship = await _organizationSponsorshipRepository + .GetBySponsoredOrganizationIdAsync(sponsoredOrganization.Id); + if (existingOrgSponsorship != null) + { + throw new BadRequestException("Cannot redeem a sponsorship offer for an organization that is already sponsored. Revoke existing sponsorship first."); + } + + if (sponsorship.PlanSponsorshipType == null) + { + throw new BadRequestException("Cannot set up sponsorship without a known sponsorship type."); + } + + // Check org to sponsor's product type + var requiredSponsoredProductType = StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value)?.SponsoredProductType; + if (requiredSponsoredProductType == null || + sponsoredOrganization == null || + StaticStore.GetPlan(sponsoredOrganization.PlanType).Product != requiredSponsoredProductType.Value) + { + throw new BadRequestException("Can only redeem sponsorship offer on families organizations."); + } + + await _paymentService.SponsorOrganizationAsync(sponsoredOrganization, sponsorship); + await _organizationRepository.UpsertAsync(sponsoredOrganization); + + sponsorship.SponsoredOrganizationId = sponsoredOrganization.Id; + sponsorship.OfferedToEmail = null; + await _organizationSponsorshipRepository.UpsertAsync(sponsorship); + } + + public async Task ValidateSponsorshipAsync(Guid sponsoredOrganizationId) + { + var sponsoredOrganization = await _organizationRepository.GetByIdAsync(sponsoredOrganizationId); + if (sponsoredOrganization == null) + { + return false; + } + + var existingSponsorship = await _organizationSponsorshipRepository + .GetBySponsoredOrganizationIdAsync(sponsoredOrganizationId); + + if (existingSponsorship == null) + { + await DoRemoveSponsorshipAsync(sponsoredOrganization, null); + return false; + } + + if (existingSponsorship.SponsoringOrganizationId == null || existingSponsorship.SponsoringOrganizationUserId == null || existingSponsorship.PlanSponsorshipType == null) + { + await DoRemoveSponsorshipAsync(sponsoredOrganization, existingSponsorship); + return false; + } + var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(existingSponsorship.PlanSponsorshipType.Value); + + var sponsoringOrganization = await _organizationRepository + .GetByIdAsync(existingSponsorship.SponsoringOrganizationId.Value); + if (sponsoringOrganization == null) + { + await DoRemoveSponsorshipAsync(sponsoredOrganization, existingSponsorship); + return false; + } + + var sponsoringOrgPlan = Utilities.StaticStore.GetPlan(sponsoringOrganization.PlanType); + if (!sponsoringOrganization.Enabled || sponsoredPlan.SponsoringProductType != sponsoringOrgPlan.Product) + { + await DoRemoveSponsorshipAsync(sponsoredOrganization, existingSponsorship); + return false; + } + + return true; + } + + public async Task RevokeSponsorshipAsync(Organization sponsoredOrg, OrganizationSponsorship sponsorship) + { + if (sponsorship == null) + { + throw new BadRequestException("You are not currently sponsoring an organization."); + } + + if (sponsorship.SponsoredOrganizationId == null) + { + await DoRemoveSponsorshipAsync(null, sponsorship); + return; + } + + if (sponsoredOrg == null) + { + throw new BadRequestException("Unable to find the sponsored Organization."); + } + + await DoRemoveSponsorshipAsync(sponsoredOrg, sponsorship); + } + + public async Task RemoveSponsorshipAsync(Organization sponsoredOrg, OrganizationSponsorship sponsorship) + { + if (sponsorship == null || sponsorship.SponsoredOrganizationId == null) + { + throw new BadRequestException("The requested organization is not currently being sponsored."); + } + + if (sponsoredOrg == null) + { + throw new BadRequestException("Unable to find the sponsored Organization."); + } + + await DoRemoveSponsorshipAsync(sponsoredOrg, sponsorship); + } + + internal async Task DoRemoveSponsorshipAsync(Organization sponsoredOrganization, OrganizationSponsorship sponsorship = null) + { + if (sponsoredOrganization != null) + { + await _paymentService.RemoveOrganizationSponsorshipAsync(sponsoredOrganization, sponsorship); + await _organizationRepository.UpsertAsync(sponsoredOrganization); + + await _mailService.SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync( + sponsoredOrganization.BillingEmailAddress(), + sponsoredOrganization.Name); + } + + if (sponsorship == null) + { + return; + } + + // Initialize the record as available + sponsorship.SponsoredOrganizationId = null; + sponsorship.FriendlyName = null; + sponsorship.OfferedToEmail = null; + sponsorship.PlanSponsorshipType = null; + sponsorship.TimesRenewedWithoutValidation = 0; + sponsorship.SponsorshipLapsedDate = null; + + if (sponsorship.CloudSponsor || sponsorship.SponsorshipLapsedDate.HasValue) + { + await _organizationSponsorshipRepository.DeleteAsync(sponsorship); + } + else + { + await _organizationSponsorshipRepository.UpsertAsync(sponsorship); + } + } + } +} diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 690391d78..7ca89b201 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -192,6 +192,27 @@ namespace Bit.Core.Services } } + private async Task ChangeOrganizationSponsorship(Organization org, OrganizationSponsorship sponsorship, bool applySponsorship) + { + var existingPlan = Utilities.StaticStore.GetPlan(org.PlanType); + var sponsoredPlan = sponsorship != null ? + Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value) : + null; + var subscriptionUpdate = new SponsorOrganizationSubscriptionUpdate(existingPlan, sponsoredPlan, applySponsorship); + + await FinalizeSubscriptionChangeAsync(org, subscriptionUpdate, DateTime.UtcNow); + + var sub = await _stripeAdapter.SubscriptionGetAsync(org.GatewaySubscriptionId); + org.ExpirationDate = sub.CurrentPeriodEnd; + + } + + public Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship) => + ChangeOrganizationSponsorship(org, sponsorship, true); + + public Task RemoveOrganizationSponsorshipAsync(Organization org, OrganizationSponsorship sponsorship) => + ChangeOrganizationSponsorship(org, sponsorship, false); + public async Task UpgradeFreeOrganizationAsync(Organization org, StaticStore.Plan plan, short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo) { @@ -227,6 +248,29 @@ namespace Bit.Core.Services } var subCreateOptions = new OrganizationUpgradeSubscriptionOptions(customer.Id, org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon); + var (stripePaymentMethod, paymentMethodType) = IdentifyPaymentMethod(customer, subCreateOptions); + + var subscription = await ChargeForNewSubscriptionAsync(org, customer, false, + stripePaymentMethod, paymentMethodType, subCreateOptions, null); + org.GatewaySubscriptionId = subscription.Id; + + if (subscription.Status == "incomplete" && + subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action") + { + org.Enabled = false; + return subscription.LatestInvoice.PaymentIntent.ClientSecret; + } + else + { + org.Enabled = true; + org.ExpirationDate = subscription.CurrentPeriodEnd; + return null; + } + } + + private (bool stripePaymentMethod, PaymentMethodType PaymentMethodType) IdentifyPaymentMethod( + Stripe.Customer customer, Stripe.SubscriptionCreateOptions subCreateOptions) + { var stripePaymentMethod = false; var paymentMethodType = PaymentMethodType.Credit; var hasBtCustomerId = customer.Metadata.ContainsKey("btCustomerId"); @@ -265,23 +309,7 @@ namespace Bit.Core.Services } } } - - var subscription = await ChargeForNewSubscriptionAsync(org, customer, false, - stripePaymentMethod, paymentMethodType, subCreateOptions, null); - org.GatewaySubscriptionId = subscription.Id; - - if (subscription.Status == "incomplete" && - subscription.LatestInvoice?.PaymentIntent?.Status == "requires_action") - { - org.Enabled = false; - return subscription.LatestInvoice.PaymentIntent.ClientSecret; - } - else - { - org.Enabled = true; - org.ExpirationDate = subscription.CurrentPeriodEnd; - return null; - } + return (stripePaymentMethod, paymentMethodType); } public async Task PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, @@ -691,11 +719,11 @@ namespace Bit.Core.Services var collectionMethod = sub.CollectionMethod; var daysUntilDue = sub.DaysUntilDue; var chargeNow = collectionMethod == "charge_automatically"; - var updatedItemOptions = subscriptionUpdate.UpgradeItemOptions(sub); + var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub); var subUpdateOptions = new Stripe.SubscriptionUpdateOptions { - Items = new List { updatedItemOptions }, + Items = updatedItemOptions, ProrationBehavior = "always_invoice", DaysUntilDue = daysUntilDue ?? 1, CollectionMethod = "send_invoice", @@ -738,14 +766,8 @@ namespace Bit.Core.Services throw new BadRequestException("Unable to locate draft invoice for subscription update."); } - // If no amount due, invoice is autofinalized, we're done - if (invoice.AmountDue <= 0) - { - return null; - } - string paymentIntentClientSecret = null; - if (updatedItemOptions.Quantity > 0) + if (invoice.AmountDue > 0 && updatedItemOptions.Any(i => i.Quantity > 0)) { try { @@ -769,7 +791,7 @@ namespace Bit.Core.Services // Need to revert the subscription await _stripeAdapter.SubscriptionUpdateAsync(sub.Id, new Stripe.SubscriptionUpdateOptions { - Items = new List { subscriptionUpdate.RevertItemOptions(sub) }, + Items = subscriptionUpdate.RevertItemsOptions(sub), // This proration behavior prevents a false "credit" from // being applied forward to the next month's invoice ProrationBehavior = "none", diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index 230469931..62d1d4a1c 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -55,12 +55,12 @@ namespace Bit.Core.Services return Task.FromResult(0); } - public Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token) + public Task SendOrganizationInviteEmailAsync(string organizationName, bool orgCanSponsor, OrganizationUser orgUser, ExpiringToken token) { return Task.FromResult(0); } - public Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites) + public Task BulkSendOrganizationInviteEmailAsync(string organizationName, bool orgCanSponsor, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites) { return Task.FromResult(0); } @@ -201,6 +201,21 @@ namespace Bit.Core.Services return Task.FromResult(0); } + public Task SendFamiliesForEnterpriseOfferEmailAsync(string email, string sponsorEmail, bool existingAccount, string token) + { + return Task.FromResult(0); + } + + public Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail) + { + return Task.FromResult(0); + } + + public Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, string familyOrgName) + { + return Task.FromResult(0); + } + public Task SendOTPEmailAsync(string email, string token) { return Task.FromResult(0); diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index a9f6b7765..a6aed7150 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -937,5 +937,39 @@ namespace Bit.Core.Utilities return CryptographicOperations.FixedTimeEquals( Encoding.UTF8.GetBytes(input1), Encoding.UTF8.GetBytes(input2)); } + + public static string ObfuscateEmail(string email) + { + if (email == null) + { + return email; + } + + var emailParts = email.Split('@', StringSplitOptions.RemoveEmptyEntries); + + if (emailParts.Length != 2) + { + return email; + } + + var username = emailParts[0]; + + if (username.Length < 2) + { + return email; + } + + var sb = new StringBuilder(); + sb.Append(emailParts[0][..2]); + for (var i = 2; i < emailParts[0].Length; i++) + { + sb.Append('*'); + } + + return sb.Append('@') + .Append(emailParts[1]) + .ToString(); + + } } } diff --git a/src/Core/Utilities/ServiceCollectionExtensions.cs b/src/Core/Utilities/ServiceCollectionExtensions.cs index 1c4718111..f37412963 100644 --- a/src/Core/Utilities/ServiceCollectionExtensions.cs +++ b/src/Core/Utilities/ServiceCollectionExtensions.cs @@ -101,6 +101,7 @@ namespace Bit.Core.Utilities services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -127,6 +128,7 @@ namespace Bit.Core.Utilities services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); @@ -168,6 +170,7 @@ namespace Bit.Core.Utilities services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Core/Utilities/StaticStore.cs b/src/Core/Utilities/StaticStore.cs index c6fb25c6d..3de47c1c8 100644 --- a/src/Core/Utilities/StaticStore.cs +++ b/src/Core/Utilities/StaticStore.cs @@ -1,6 +1,8 @@ using Bit.Core.Enums; +using Bit.Core.Models.Data; using Bit.Core.Models.StaticStore; using System.Collections.Generic; +using System.Linq; namespace Bit.Core.Utilities { @@ -477,5 +479,21 @@ namespace Bit.Core.Utilities public static IDictionary> GlobalDomains { get; set; } public static IEnumerable Plans { get; set; } + public static IEnumerable SponsoredPlans { get; set; } = new[] + { + new SponsoredPlan + { + PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise, + SponsoredProductType = ProductType.Families, + SponsoringProductType = ProductType.Enterprise, + StripePlanId = "2021-family-for-enterprise-annually", + UsersCanSponsor = (OrganizationUserOrganizationDetails org) => + GetPlan(org.PlanType).Product == ProductType.Enterprise, + } + }; + public static Plan GetPlan(PlanType planType) => + Plans.FirstOrDefault(p => p.Type == planType); + public static SponsoredPlan GetSponsoredPlan(PlanSponsorshipType planSponsorshipType) => + SponsoredPlans.FirstOrDefault(p => p.PlanSponsorshipType == planSponsorshipType); } } diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index 4843e6b28..d912a9bd1 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -95,6 +95,7 @@ + @@ -224,6 +225,16 @@ + + + + + + + + + + diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Create.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Create.sql new file mode 100644 index 000000000..f26e9a35b --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Create.sql @@ -0,0 +1,46 @@ +CREATE PROCEDURE [dbo].[OrganizationSponsorship_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @InstallationId UNIQUEIDENTIFIER, + @SponsoringOrganizationId UNIQUEIDENTIFIER, + @SponsoringOrganizationUserID UNIQUEIDENTIFIER, + @SponsoredOrganizationId UNIQUEIDENTIFIER, + @OfferedToEmail NVARCHAR(256), + @PlanSponsorshipType TINYINT, + @CloudSponsor BIT, + @LastSyncDate DATETIME2 (7), + @TimesRenewedWithoutValidation TINYINT, + @SponsorshipLapsedDate DATETIME2 (7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[OrganizationSponsorship] + ( + [Id], + [InstallationId], + [SponsoringOrganizationId], + [SponsoringOrganizationUserID], + [SponsoredOrganizationId], + [OfferedToEmail], + [PlanSponsorshipType], + [CloudSponsor], + [LastSyncDate], + [TimesRenewedWithoutValidation], + [SponsorshipLapsedDate] + ) + VALUES + ( + @Id, + @InstallationId, + @SponsoringOrganizationId, + @SponsoringOrganizationUserID, + @SponsoredOrganizationId, + @OfferedToEmail, + @PlanSponsorshipType, + @CloudSponsor, + @LastSyncDate, + @TimesRenewedWithoutValidation, + @SponsorshipLapsedDate + ) +END +GO diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_DeleteById.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_DeleteById.sql new file mode 100644 index 000000000..914707154 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_DeleteById.sql @@ -0,0 +1,17 @@ +CREATE PROCEDURE [dbo].[OrganizationSponsorship_DeleteById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + BEGIN TRANSACTION OrgSponsorship_DeleteById + + DELETE + FROM + [dbo].[OrganizationSponsorship] + WHERE + [Id] = @Id + + COMMIT TRANSACTION OrgSponsorship_DeleteById +END +GO diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationDeleted.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationDeleted.sql new file mode 100644 index 000000000..f7685b7e1 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationDeleted.sql @@ -0,0 +1,31 @@ +CREATE PROCEDURE [dbo].[OrganizationSponsorship_OrganizationDeleted] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[OrganizationSponsorship] + SET + [SponsoringOrganizationId] = NULL + WHERE + [SponsoringOrganizationId] = @OrganizationId AND + [CloudSponsor] = 0 + + UPDATE + [dbo].[OrganizationSponsorship] + SET + [SponsoredOrganizationId] = NULL + WHERE + [SponsoredOrganizationId] = @OrganizationId AND + [CloudSponsor] = 0 + + DELETE + FROM + [dbo].[OrganizationSponsorship] + WHERE + [CloudSponsor] = 1 AND + ([SponsoredOrganizationId] = @OrganizationId OR + [SponsoringOrganizationId] = @OrganizationId) +END +GO diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationUserDeleted.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationUserDeleted.sql new file mode 100644 index 000000000..a324b76d3 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationUserDeleted.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[OrganizationSponsorship_OrganizationUserDeleted] + @OrganizationUserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DELETE + FROM + [dbo].[OrganizationSponsorship] + WHERE + [SponsoringOrganizationUserId] = @OrganizationUserId +END +GO diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationUsersDeleted.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationUsersDeleted.sql new file mode 100644 index 000000000..97f1e3043 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationUsersDeleted.sql @@ -0,0 +1,25 @@ +CREATE PROCEDURE [dbo].[OrganizationSponsorship_OrganizationUsersDeleted] + @SponsoringOrganizationUserIds [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + DECLARE @BatchSize AS INT; + SET @BatchSize = 100; + + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION OrganizationSponsorship_DeleteOUs + + DELETE TOP(@BatchSize) OS + FROM + [dbo].[OrganizationSponsorship] OS + INNER JOIN + @SponsoringOrganizationUserIds I ON I.Id = OS.SponsoringOrganizationUserId + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION OrganizationSponsorship_DeleteOUs + END +END +GO diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadById.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadById.sql new file mode 100644 index 000000000..630200a32 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadById.sql @@ -0,0 +1,14 @@ +CREATE PROCEDURE [dbo].[OrganizationSponsorship_ReadById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationSponsorshipView] + WHERE + [Id] = @Id +END +GO diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadByOfferedToEmail.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadByOfferedToEmail.sql new file mode 100644 index 000000000..22fac3f98 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadByOfferedToEmail.sql @@ -0,0 +1,14 @@ +CREATE PROCEDURE [dbo].[OrganizationSponsorship_ReadByOfferedToEmail] + @OfferedToEmail NVARCHAR (256) -- Should not be null +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationSponsorshipView] + WHERE + [OfferedToEmail] = @OfferedToEmail +END +GO diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoredOrganizationId.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoredOrganizationId.sql new file mode 100644 index 000000000..203249cf8 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoredOrganizationId.sql @@ -0,0 +1,14 @@ +CREATE PROCEDURE [dbo].[OrganizationSponsorship_ReadBySponsoredOrganizationId] + @SponsoredOrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationSponsorshipView] + WHERE + [SponsoredOrganizationId] = @SponsoredOrganizationId +END +GO diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoringOrganiationUserId.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoringOrganiationUserId.sql new file mode 100644 index 000000000..817a95cbc --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoringOrganiationUserId.sql @@ -0,0 +1,14 @@ +CREATE PROCEDURE [dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationUserId] + @SponsoringOrganizationUserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationSponsorshipView] + WHERE + [SponsoringOrganizationUserId] = @SponsoringOrganizationUserId +END +GO diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Update.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Update.sql new file mode 100644 index 000000000..88ac04856 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Update.sql @@ -0,0 +1,33 @@ +CREATE PROCEDURE [dbo].[OrganizationSponsorship_Update] + @Id UNIQUEIDENTIFIER, + @InstallationId UNIQUEIDENTIFIER, + @SponsoringOrganizationId UNIQUEIDENTIFIER, + @SponsoringOrganizationUserID UNIQUEIDENTIFIER, + @SponsoredOrganizationId UNIQUEIDENTIFIER, + @OfferedToEmail NVARCHAR(256), + @PlanSponsorshipType TINYINT, + @CloudSponsor BIT, + @LastSyncDate DATETIME2 (7), + @TimesRenewedWithoutValidation TINYINT, + @SponsorshipLapsedDate DATETIME2 (7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[OrganizationSponsorship] + SET + [InstallationId] = @InstallationId, + [SponsoringOrganizationId] = @SponsoringOrganizationId, + [SponsoringOrganizationUserID] = @SponsoringOrganizationUserID, + [SponsoredOrganizationId] = @SponsoredOrganizationId, + [OfferedToEmail] = @OfferedToEmail, + [PlanSponsorshipType] = @PlanSponsorshipType, + [CloudSponsor] = @CloudSponsor, + [LastSyncDate] = @LastSyncDate, + [TimesRenewedWithoutValidation] = @TimesRenewedWithoutValidation, + [SponsorshipLapsedDate] = @SponsorshipLapsedDate + WHERE + [Id] = @Id +END +GO diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteById.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteById.sql index be2a12eeb..9b7f8187e 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteById.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteById.sql @@ -34,9 +34,11 @@ BEGIN WHERE [OrganizationUserId] = @Id + EXEC [dbo].[OrganizationSponsorship_OrganizationUserDeleted] @Id + DELETE FROM [dbo].[OrganizationUser] WHERE [Id] = @Id -END \ No newline at end of file +END diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteByIds.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteByIds.sql index 049a2c5c0..4930a8fbe 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteByIds.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteByIds.sql @@ -61,6 +61,7 @@ BEGIN COMMIT TRANSACTION GoupUser_DeleteMany_GroupUsers END + EXEC [dbo].[OrganizationSponsorship_OrganizationUsersDeleted] @Ids SET @BatchSize = 100; diff --git a/src/Sql/dbo/Stored Procedures/Organization_DeleteById.sql b/src/Sql/dbo/Stored Procedures/Organization_DeleteById.sql index f19b050e7..d32f7d360 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_DeleteById.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_DeleteById.sql @@ -57,6 +57,8 @@ BEGIN WHERE [OrganizationId] = @Id + EXEC[dbo].[OrganizationSponsorship_OrganizationDeleted] @Id + DELETE FROM [dbo].[Organization] diff --git a/src/Sql/dbo/Tables/OrganizationSponsorship.sql b/src/Sql/dbo/Tables/OrganizationSponsorship.sql new file mode 100644 index 000000000..391ab599b --- /dev/null +++ b/src/Sql/dbo/Tables/OrganizationSponsorship.sql @@ -0,0 +1,43 @@ +CREATE TABLE [dbo].[OrganizationSponsorship] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [InstallationId] UNIQUEIDENTIFIER NULL, + [SponsoringOrganizationId] UNIQUEIDENTIFIER NULL, + [SponsoringOrganizationUserID] UNIQUEIDENTIFIER NULL, + [SponsoredOrganizationId] UNIQUEIDENTIFIER NULL, + [OfferedToEmail] NVARCHAR (256) NULL, + [PlanSponsorshipType] TINYINT NULL, + [CloudSponsor] BIT NULL, + [LastSyncDate] DATETIME2 (7) NULL, + [TimesRenewedWithoutValidation] TINYINT DEFAULT 0, + [SponsorshipLapsedDate] DATETIME2 (7) NULL, + CONSTRAINT [PK_OrganizationSponsorship] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_OrganizationSponsorship_InstallationId] FOREIGN KEY ([InstallationId]) REFERENCES [dbo].[Installation] ([Id]), + CONSTRAINT [FK_OrganizationSponsorship_SponsoringOrg] FOREIGN KEY ([SponsoringOrganizationId]) REFERENCES [dbo].[Organization] ([Id]), + CONSTRAINT [FK_OrganizationSponsorship_SponsoredOrg] FOREIGN KEY ([SponsoredOrganizationId]) REFERENCES [dbo].[Organization] ([Id]), +); + + +GO +CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_InstallationId] + ON [dbo].[OrganizationSponsorship]([InstallationId] ASC) + WHERE [InstallationId] IS NOT NULL; + +GO +CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationId] + ON [dbo].[OrganizationSponsorship]([SponsoringOrganizationId] ASC) + WHERE [SponsoringOrganizationId] IS NOT NULL; + +GO +CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationUserId] + ON [dbo].[OrganizationSponsorship]([SponsoringOrganizationUserID] ASC) + WHERE [SponsoringOrganizationUserID] IS NOT NULL; + +GO +CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_OfferedToEmail] + ON [dbo].[OrganizationSponsorship]([OfferedToEmail] ASC) + WHERE [OfferedToEmail] IS NOT NULL; + +GO +CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoredOrganizationID] + ON [dbo].[OrganizationSponsorship]([SponsoredOrganizationId] ASC) + WHERE [SponsoredOrganizationId] IS NOT NULL; diff --git a/test/Api.Test/Api.Test.csproj b/test/Api.Test/Api.Test.csproj index e8c71e9f2..dd902a33d 100644 --- a/test/Api.Test/Api.Test.csproj +++ b/test/Api.Test/Api.Test.csproj @@ -22,6 +22,7 @@ + diff --git a/test/Api.Test/AutoFixture/Attributes/ControllerCustomizeAttribute.cs b/test/Api.Test/AutoFixture/Attributes/ControllerCustomizeAttribute.cs new file mode 100644 index 000000000..fa5f764b9 --- /dev/null +++ b/test/Api.Test/AutoFixture/Attributes/ControllerCustomizeAttribute.cs @@ -0,0 +1,17 @@ +using System; +using AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; + +namespace Bit.Api.Test.AutoFixture.Attributes +{ + public class ControllerCustomizeAttribute : BitCustomizeAttribute + { + private readonly Type _controllerType; + public ControllerCustomizeAttribute(Type controllerType) + { + _controllerType = controllerType; + } + + public override ICustomization GetCustomization() => new ControllerCustomization(_controllerType); + } +} diff --git a/test/Api.Test/AutoFixture/ControllerCustomization.cs b/test/Api.Test/AutoFixture/ControllerCustomization.cs new file mode 100644 index 000000000..137cadb89 --- /dev/null +++ b/test/Api.Test/AutoFixture/ControllerCustomization.cs @@ -0,0 +1,38 @@ +using AutoFixture; +using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc; +using Bit.Api.Controllers; +using AutoFixture.Kernel; +using System; +using Bit.Test.Common.AutoFixture; +using Org.BouncyCastle.Security; + +namespace Bit.Api.Test.AutoFixture +{ + /// + /// Disables setting of Auto Properties on the Controller to avoid ASP.net initialization errors. Still sets constructor dependencies. + /// + /// + public class ControllerCustomization : ICustomization + { + private readonly Type _controllerType; + public ControllerCustomization(Type controllerType) + { + if (!controllerType.IsAssignableTo(typeof(Controller))) + { + throw new InvalidParameterException($"{nameof(controllerType)} must derive from {typeof(Controller).Name}"); + } + + _controllerType = controllerType; + } + + public void Customize(IFixture fixture) + { + fixture.Customizations.Add(new BuilderWithoutAutoProperties(_controllerType)); + } + } + public class ControllerCustomization : ICustomization where T : Controller + { + public void Customize(IFixture fixture) => new ControllerCustomization(typeof(T)).Customize(fixture); + } +} diff --git a/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs new file mode 100644 index 000000000..949b6d48b --- /dev/null +++ b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs @@ -0,0 +1,109 @@ +using Xunit; +using Bit.Test.Common.AutoFixture.Attributes; +using System.Threading.Tasks; +using System; +using Bit.Core.Enums; +using System.Linq; +using System.Collections.Generic; +using Bit.Core.Models.Table; +using Bit.Test.Common.AutoFixture; +using Bit.Api.Controllers; +using Bit.Core.Context; +using NSubstitute; +using Bit.Core.Exceptions; +using Bit.Api.Test.AutoFixture.Attributes; +using Bit.Core.Repositories; +using Bit.Core.Models.Api.Request; +using Bit.Core.Services; +using Bit.Core.Models.Api; +using Bit.Core.Utilities; + +namespace Bit.Api.Test.Controllers +{ + [ControllerCustomize(typeof(OrganizationSponsorshipsController))] + [SutProviderCustomize] + public class OrganizationSponsorshipsControllerTests + { + public static IEnumerable EnterprisePlanTypes => + Enum.GetValues().Where(p => StaticStore.GetPlan(p).Product == ProductType.Enterprise).Select(p => new object[] { p }); + public static IEnumerable NonEnterprisePlanTypes => + Enum.GetValues().Where(p => StaticStore.GetPlan(p).Product != ProductType.Enterprise).Select(p => new object[] { p }); + public static IEnumerable NonFamiliesPlanTypes => + Enum.GetValues().Where(p => StaticStore.GetPlan(p).Product != ProductType.Families).Select(p => new object[] { p }); + + public static IEnumerable NonConfirmedOrganizationUsersStatuses => + Enum.GetValues() + .Where(s => s != OrganizationUserStatusType.Confirmed) + .Select(s => new object[] { s }); + + + [Theory] + [BitAutoData] + public async Task RedeemSponsorship_BadToken_ThrowsBadRequest(string sponsorshipToken, + OrganizationSponsorshipRedeemRequestModel model, SutProvider sutProvider) + { + sutProvider.GetDependency().ValidateRedemptionTokenAsync(sponsorshipToken) + .Returns(false); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RedeemSponsorship(sponsorshipToken, model)); + + Assert.Contains("Failed to parse sponsorship token.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SetUpSponsorshipAsync(default, default); + } + + [Theory] + [BitAutoData] + public async Task RedeemSponsorship_NotSponsoredOrgOwner_ThrowsBadRequest(string sponsorshipToken, + OrganizationSponsorshipRedeemRequestModel model, SutProvider sutProvider) + { + sutProvider.GetDependency().ValidateRedemptionTokenAsync(sponsorshipToken) + .Returns(true); + sutProvider.GetDependency().OrganizationOwner(model.SponsoredOrganizationId).Returns(false); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RedeemSponsorship(sponsorshipToken, model)); + + Assert.Contains("Can only redeem sponsorship for an organization you own.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SetUpSponsorshipAsync(default, default); + } + + [Theory] + [BitAutoData] + public async Task RevokeSponsorship_WrongSponsoringUser_ThrowsBadRequest(OrganizationUser sponsoringOrgUser, + Guid currentUserId, SutProvider sutProvider) + { + sutProvider.GetDependency().UserId.Returns(currentUserId); + sutProvider.GetDependency().GetByIdAsync(sponsoringOrgUser.Id) + .Returns(sponsoringOrgUser); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RevokeSponsorship(sponsoringOrgUser.Id)); + + Assert.Contains("Can only revoke a sponsorship you granted.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .RemoveSponsorshipAsync(default, default); + } + + [Theory] + [BitAutoData] + public async Task RemoveSponsorship_WrongOrgUserType_ThrowsBadRequest(Organization sponsoredOrg, + SutProvider sutProvider) + { + sutProvider.GetDependency().OrganizationOwner(Arg.Any()).Returns(false); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RemoveSponsorship(sponsoredOrg.Id)); + + Assert.Contains("Only the owner of an organization can remove sponsorship.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .RemoveSponsorshipAsync(default, default); + } + } +} diff --git a/test/Common/AutoFixture/Attributes/BitAutoDataAttribute.cs b/test/Common/AutoFixture/Attributes/BitAutoDataAttribute.cs new file mode 100644 index 000000000..dfb06a254 --- /dev/null +++ b/test/Common/AutoFixture/Attributes/BitAutoDataAttribute.cs @@ -0,0 +1,29 @@ +using System; +using Xunit.Sdk; +using AutoFixture; +using System.Reflection; +using System.Collections.Generic; +using Bit.Test.Common.Helpers; + +namespace Bit.Test.Common.AutoFixture.Attributes +{ + public class BitAutoDataAttribute : DataAttribute + { + private readonly Func _createFixture; + private readonly object[] _fixedTestParameters; + + public BitAutoDataAttribute(params object[] fixedTestParameters) : + this(() => new Fixture(), fixedTestParameters) + { } + + public BitAutoDataAttribute(Func createFixture, params object[] fixedTestParameters) : + base() + { + _createFixture = createFixture; + _fixedTestParameters = fixedTestParameters; + } + + public override IEnumerable GetData(MethodInfo testMethod) + => BitAutoDataAttributeHelpers.GetData(testMethod, _createFixture(), _fixedTestParameters); + } +} diff --git a/test/Common/AutoFixture/Attributes/BitCustomizeAttribute.cs b/test/Common/AutoFixture/Attributes/BitCustomizeAttribute.cs new file mode 100644 index 000000000..32910ef53 --- /dev/null +++ b/test/Common/AutoFixture/Attributes/BitCustomizeAttribute.cs @@ -0,0 +1,22 @@ +using System; +using AutoFixture; + +namespace Bit.Test.Common.AutoFixture.Attributes +{ + /// + /// + /// Base class for customizing parameters in methods decorated with the + /// Bit.Test.Common.AutoFixture.Attributes.MemberAutoDataAttribute. + /// + /// ⚠ Warning ⚠ Will not insert customizations into AutoFixture's AutoDataAttribute build chain + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method | AttributeTargets.Parameter, AllowMultiple = true)] + public abstract class BitCustomizeAttribute : Attribute + { + /// + /// /// Gets a customization for the method's parameters. + /// + /// A customization for the method's paramters. + public abstract ICustomization GetCustomization(); + } +} diff --git a/test/Core.Test/AutoFixture/Attributes/CustomAutoDataAttribute.cs b/test/Common/AutoFixture/Attributes/CustomAutoDataAttribute.cs similarity index 93% rename from test/Core.Test/AutoFixture/Attributes/CustomAutoDataAttribute.cs rename to test/Common/AutoFixture/Attributes/CustomAutoDataAttribute.cs index ac2e81c1a..cd2feebca 100644 --- a/test/Core.Test/AutoFixture/Attributes/CustomAutoDataAttribute.cs +++ b/test/Common/AutoFixture/Attributes/CustomAutoDataAttribute.cs @@ -3,7 +3,7 @@ using System.Linq; using AutoFixture; using AutoFixture.Xunit2; -namespace Bit.Core.Test.AutoFixture.Attributes +namespace Bit.Test.Common.AutoFixture.Attributes { public class CustomAutoDataAttribute : AutoDataAttribute { diff --git a/test/Core.Test/AutoFixture/Attributes/InlineCustomAutoDataAttribute.cs b/test/Common/AutoFixture/Attributes/InlineCustomAutoDataAttribute.cs similarity index 83% rename from test/Core.Test/AutoFixture/Attributes/InlineCustomAutoDataAttribute.cs rename to test/Common/AutoFixture/Attributes/InlineCustomAutoDataAttribute.cs index ac1c22a65..d36f963a4 100644 --- a/test/Core.Test/AutoFixture/Attributes/InlineCustomAutoDataAttribute.cs +++ b/test/Common/AutoFixture/Attributes/InlineCustomAutoDataAttribute.cs @@ -4,9 +4,9 @@ using Xunit.Sdk; using AutoFixture.Xunit2; using AutoFixture; -namespace Bit.Core.Test.AutoFixture.Attributes +namespace Bit.Test.Common.AutoFixture.Attributes { - internal class InlineCustomAutoDataAttribute : CompositeDataAttribute + public class InlineCustomAutoDataAttribute : CompositeDataAttribute { public InlineCustomAutoDataAttribute(Type[] iCustomizationTypes, params object[] values) : base(new DataAttribute[] { new InlineDataAttribute(values), diff --git a/test/Common/AutoFixture/Attributes/InlineSutAutoDataAttribute.cs b/test/Common/AutoFixture/Attributes/InlineSutAutoDataAttribute.cs new file mode 100644 index 000000000..89eebad8c --- /dev/null +++ b/test/Common/AutoFixture/Attributes/InlineSutAutoDataAttribute.cs @@ -0,0 +1,20 @@ +using System; +using System.Linq; +using AutoFixture; + +namespace Bit.Test.Common.AutoFixture.Attributes +{ + public class InlineSutAutoDataAttribute : InlineCustomAutoDataAttribute + { + public InlineSutAutoDataAttribute(params object[] values) : base( + new Type[] { typeof(SutProviderCustomization) }, values) + { } + public InlineSutAutoDataAttribute(Type[] iCustomizationTypes, params object[] values) : base( + iCustomizationTypes.Append(typeof(SutProviderCustomization)).ToArray(), values) + { } + + public InlineSutAutoDataAttribute(ICustomization[] customizations, params object[] values) : base( + customizations.Append(new SutProviderCustomization()).ToArray(), values) + { } + } +} diff --git a/test/Common/AutoFixture/Attributes/MemberAutoDataAttribute.cs b/test/Common/AutoFixture/Attributes/MemberAutoDataAttribute.cs new file mode 100644 index 000000000..a154128ae --- /dev/null +++ b/test/Common/AutoFixture/Attributes/MemberAutoDataAttribute.cs @@ -0,0 +1,27 @@ +using System; +using Xunit; +using AutoFixture; +using System.Reflection; +using System.Linq; +using Bit.Test.Common.Helpers; + +namespace Bit.Test.Common.AutoFixture.Attributes +{ + public class BitMemberAutoDataAttribute : MemberDataAttributeBase + { + private readonly Func _createFixture; + + public BitMemberAutoDataAttribute(string memberName, params object[] parameters) : + this(() => new Fixture(), memberName, parameters) + { } + + public BitMemberAutoDataAttribute(Func createFixture, string memberName, params object[] parameters) : + base(memberName, parameters) + { + _createFixture = createFixture; + } + + protected override object[] ConvertDataItem(MethodInfo testMethod, object item) => + BitAutoDataAttributeHelpers.GetData(testMethod, _createFixture(), item as object[]).First(); + } +} diff --git a/test/Common/AutoFixture/Attributes/SutAutoDataAttribute.cs b/test/Common/AutoFixture/Attributes/SutAutoDataAttribute.cs new file mode 100644 index 000000000..1c8bd089b --- /dev/null +++ b/test/Common/AutoFixture/Attributes/SutAutoDataAttribute.cs @@ -0,0 +1,20 @@ +using System; +using System.Linq; +using System.Reflection; +using AutoFixture; +using AutoFixture.Xunit2; + +namespace Bit.Test.Common.AutoFixture.Attributes +{ + public class SutProviderCustomizeAttribute : BitCustomizeAttribute + { + public override ICustomization GetCustomization() => new SutProviderCustomization(); + } + + public class SutAutoDataAttribute : CustomAutoDataAttribute + { + public SutAutoDataAttribute(params Type[] iCustomizationTypes) : base( + iCustomizationTypes.Append(typeof(SutProviderCustomization)).ToArray()) + { } + } +} diff --git a/test/Common/AutoFixture/BuilderWithoutAutoProperties.cs b/test/Common/AutoFixture/BuilderWithoutAutoProperties.cs new file mode 100644 index 000000000..81df3206a --- /dev/null +++ b/test/Common/AutoFixture/BuilderWithoutAutoProperties.cs @@ -0,0 +1,41 @@ +using System; +using AutoFixture; +using AutoFixture.Dsl; +using AutoFixture.Kernel; + +namespace Bit.Test.Common.AutoFixture +{ + public class BuilderWithoutAutoProperties : ISpecimenBuilder + { + private readonly Type _type; + public BuilderWithoutAutoProperties(Type type) + { + _type = type; + } + + public object Create(object request, ISpecimenContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var type = request as Type; + if (type == null || type != _type) + { + return new NoSpecimen(); + } + + var fixture = new Fixture(); + // This is the equivalent of _fixture.Build<_type>().OmitAutoProperties().Create(request, context), but no overload for + // Build(Type type) exists. + dynamic reflectedComposer = typeof(Fixture).GetMethod("Build").MakeGenericMethod(_type).Invoke(fixture, null); + return reflectedComposer.OmitAutoProperties().Create(request, context); + } + } + public class BuilderWithoutAutoProperties : ISpecimenBuilder + { + public object Create(object request, ISpecimenContext context) => + new BuilderWithoutAutoProperties(typeof(T)).Create(request, context); + } +} diff --git a/test/Core.Test/AutoFixture/FixtureExtensions.cs b/test/Common/AutoFixture/FixtureExtensions.cs similarity index 87% rename from test/Core.Test/AutoFixture/FixtureExtensions.cs rename to test/Common/AutoFixture/FixtureExtensions.cs index 10021fbee..7249e8e41 100644 --- a/test/Core.Test/AutoFixture/FixtureExtensions.cs +++ b/test/Common/AutoFixture/FixtureExtensions.cs @@ -1,7 +1,7 @@ using AutoFixture; using AutoFixture.AutoNSubstitute; -namespace Bit.Core.Test.AutoFixture +namespace Bit.Test.Common.AutoFixture { public static class FixtureExtensions { diff --git a/test/Core.Test/AutoFixture/GlobalSettingsFixtures.cs b/test/Common/AutoFixture/GlobalSettingsFixtures.cs similarity index 57% rename from test/Core.Test/AutoFixture/GlobalSettingsFixtures.cs rename to test/Common/AutoFixture/GlobalSettingsFixtures.cs index 9eea0b063..8df15b107 100644 --- a/test/Core.Test/AutoFixture/GlobalSettingsFixtures.cs +++ b/test/Common/AutoFixture/GlobalSettingsFixtures.cs @@ -1,19 +1,13 @@ using System; -using System.Collections; -using System.Collections.Generic; using System.Reflection; using AutoFixture; using AutoFixture.Kernel; -using AutoMapper; -using Bit.Core.Enums; -using Bit.Core.Models; -using Bit.Core.Models.Table; -using Bit.Core.Settings; -using Bit.Core.Test.Helpers.Factories; +using AutoFixture.Xunit2; +using Bit.Test.Common.Helpers.Factories; -namespace Bit.Core.Test.AutoFixture.GlobalSettingsFixtures +namespace Bit.Test.Common.AutoFixture { - internal class GlobalSettingsBuilder: ISpecimenBuilder + public class GlobalSettingsBuilder : ISpecimenBuilder { public object Create(object request, ISpecimenContext context) { @@ -25,22 +19,27 @@ namespace Bit.Core.Test.AutoFixture.GlobalSettingsFixtures var pi = request as ParameterInfo; var fixture = new Fixture(); - if (pi == null || pi.ParameterType != typeof(Settings.GlobalSettings)) + if (pi == null || pi.ParameterType != typeof(Bit.Core.Settings.GlobalSettings)) return new NoSpecimen(); return GlobalSettingsFactory.GlobalSettings; } } - internal class GlobalSettings : ICustomization + public class GlobalSettings : ICustomization { public void Customize(IFixture fixture) { - fixture.Customize(composer => composer + fixture.Customize(composer => composer .Without(s => s.BaseServiceUri) .Without(s => s.Attachment) .Without(s => s.Send) .Without(s => s.DataProtection)); } } + + public class GlobalSettingsCustomizeAttribute : CustomizeAttribute + { + public override ICustomization GetCustomization(ParameterInfo parameter) => new GlobalSettings(); + } } diff --git a/test/Core.Test/AutoFixture/ISutProvider.cs b/test/Common/AutoFixture/ISutProvider.cs similarity index 76% rename from test/Core.Test/AutoFixture/ISutProvider.cs rename to test/Common/AutoFixture/ISutProvider.cs index 3a22bf489..c72dc4a27 100644 --- a/test/Core.Test/AutoFixture/ISutProvider.cs +++ b/test/Common/AutoFixture/ISutProvider.cs @@ -1,6 +1,6 @@ using System; -namespace Bit.Core.Test.AutoFixture +namespace Bit.Test.Common.AutoFixture { public interface ISutProvider { diff --git a/test/Core.Test/AutoFixture/SutProvider.cs b/test/Common/AutoFixture/SutProvider.cs similarity index 98% rename from test/Core.Test/AutoFixture/SutProvider.cs rename to test/Common/AutoFixture/SutProvider.cs index e8b475d24..0846655a7 100644 --- a/test/Core.Test/AutoFixture/SutProvider.cs +++ b/test/Common/AutoFixture/SutProvider.cs @@ -4,9 +4,8 @@ using AutoFixture; using AutoFixture.Kernel; using System.Reflection; using System.Linq; -using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures; -namespace Bit.Core.Test.AutoFixture +namespace Bit.Test.Common.AutoFixture { public class SutProvider : ISutProvider { diff --git a/test/Core.Test/AutoFixture/SutProviderCustomization.cs b/test/Common/AutoFixture/SutProviderCustomization.cs similarity index 94% rename from test/Core.Test/AutoFixture/SutProviderCustomization.cs rename to test/Common/AutoFixture/SutProviderCustomization.cs index 549af17e7..f6041e91e 100644 --- a/test/Core.Test/AutoFixture/SutProviderCustomization.cs +++ b/test/Common/AutoFixture/SutProviderCustomization.cs @@ -2,7 +2,7 @@ using System; using AutoFixture; using AutoFixture.Kernel; -namespace Bit.Core.Test.AutoFixture +namespace Bit.Test.Common.AutoFixture.Attributes { public class SutProviderCustomization : ICustomization, ISpecimenBuilder { diff --git a/test/Common/Common.csproj b/test/Common/Common.csproj new file mode 100644 index 000000000..16dc167d4 --- /dev/null +++ b/test/Common/Common.csproj @@ -0,0 +1,25 @@ + + + + false + Bit.Test.Common + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + diff --git a/test/Common/Helpers/AssertHelper.cs b/test/Common/Helpers/AssertHelper.cs new file mode 100644 index 000000000..5d65dc515 --- /dev/null +++ b/test/Common/Helpers/AssertHelper.cs @@ -0,0 +1,58 @@ +using System.Reflection; +using System.IO; +using System.Linq; +using Xunit; +using System; +using Newtonsoft.Json; + +namespace Bit.Test.Common.Helpers +{ + public static class AssertHelper + { + public static void AssertPropertyEqual(object expected, object actual, params string[] excludedPropertyStrings) + { + var relevantExcludedProperties = excludedPropertyStrings.Where(name => !name.Contains('.')).ToList(); + if (expected == null) + { + Assert.Null(actual); + return; + } + + if (actual == null) + { + throw new Exception("Expected object is null but actual is not"); + } + + foreach (var expectedPropInfo in expected.GetType().GetProperties().Where(pi => !relevantExcludedProperties.Contains(pi.Name))) + { + var actualPropInfo = actual.GetType().GetProperty(expectedPropInfo.Name); + + if (actualPropInfo == null) + { + var settings = new JsonSerializerSettings { Formatting = Formatting.Indented }; + throw new Exception(string.Concat($"Expected actual object to contain a property named {expectedPropInfo.Name}, but it does not\n", + $"Expected:\n{JsonConvert.SerializeObject(expected, settings)}\n", + $"Actual:\n{JsonConvert.SerializeObject(actual, new JsonSerializerSettings { Formatting = Formatting.Indented })}")); + } + + if (expectedPropInfo.PropertyType == typeof(string) || expectedPropInfo.PropertyType.IsValueType) + { + Assert.Equal(expectedPropInfo.GetValue(expected), actualPropInfo.GetValue(actual)); + } + else + { + var prefix = $"{expectedPropInfo.PropertyType.Name}."; + var nextExcludedProperties = excludedPropertyStrings.Where(name => name.StartsWith(prefix)) + .Select(name => name[prefix.Length..]).ToArray(); + AssertPropertyEqual(expectedPropInfo.GetValue(expected), actualPropInfo.GetValue(actual), nextExcludedProperties); + } + } + } + + public static Predicate AssertEqualExpectedPredicate(T expected) => (actual) => + { + Assert.Equal(expected, actual); + return true; + }; + } +} diff --git a/test/Common/Helpers/BitAutoDataAttributeHelpers.cs b/test/Common/Helpers/BitAutoDataAttributeHelpers.cs new file mode 100644 index 000000000..635d99671 --- /dev/null +++ b/test/Common/Helpers/BitAutoDataAttributeHelpers.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using AutoFixture; +using AutoFixture.Kernel; +using AutoFixture.Xunit2; +using Bit.Test.Common.AutoFixture.Attributes; + +namespace Bit.Test.Common.Helpers +{ + public static class BitAutoDataAttributeHelpers + { + public static IEnumerable GetData(MethodInfo testMethod, IFixture fixture, object[] fixedTestParameters) + { + var methodParameters = testMethod.GetParameters(); + var classCustomizations = testMethod.DeclaringType.GetCustomAttributes().Select(attr => attr.GetCustomization()); + var methodCustomizations = testMethod.GetCustomAttributes().Select(attr => attr.GetCustomization()); + + fixedTestParameters = fixedTestParameters ?? Array.Empty(); + + fixture = ApplyCustomizations(ApplyCustomizations(fixture, classCustomizations), methodCustomizations); + var missingParameters = methodParameters.Skip(fixedTestParameters.Length).Select(p => CustomizeAndCreate(p, fixture)); + + return new object[1][] { fixedTestParameters.Concat(missingParameters).ToArray() }; + } + + public static object CustomizeAndCreate(ParameterInfo p, IFixture fixture) + { + var customizations = p.GetCustomAttributes(typeof(CustomizeAttribute), false) + .OfType() + .Select(attr => attr.GetCustomization(p)); + + var context = new SpecimenContext(ApplyCustomizations(fixture, customizations)); + return context.Resolve(p); + } + + public static IFixture ApplyCustomizations(IFixture fixture, IEnumerable customizations) + { + var newFixture = new Fixture(); + + foreach (var customization in fixture.Customizations.Reverse().Select(b => b.ToCustomization())) + { + newFixture.Customize(customization); + } + + foreach (var customization in customizations) + { + newFixture.Customize(customization); + } + + return newFixture; + } + } +} diff --git a/test/Common/Helpers/Factories.cs b/test/Common/Helpers/Factories.cs new file mode 100644 index 000000000..5067662d2 --- /dev/null +++ b/test/Common/Helpers/Factories.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using Bit.Core.Repositories.EntityFramework; +using Bit.Core.Settings; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Configuration; + +namespace Bit.Test.Common.Helpers.Factories +{ + public static class GlobalSettingsFactory + { + public static GlobalSettings GlobalSettings { get; } = new GlobalSettings(); + static GlobalSettingsFactory() + { + var configBuilder = new ConfigurationBuilder().AddUserSecrets(); + var Configuration = configBuilder.Build(); + ConfigurationBinder.Bind(Configuration.GetSection("GlobalSettings"), GlobalSettings); + } + } +} diff --git a/test/Core.Test/AutoFixture/CipherFixtures.cs b/test/Core.Test/AutoFixture/CipherFixtures.cs index 30d21f132..23730fe9d 100644 --- a/test/Core.Test/AutoFixture/CipherFixtures.cs +++ b/test/Core.Test/AutoFixture/CipherFixtures.cs @@ -7,14 +7,13 @@ using AutoFixture.Kernel; using Bit.Core.Models.Data; using Bit.Core.Models.Table; using Bit.Core.Repositories.EntityFramework; -using Bit.Core.Test.AutoFixture.Attributes; using Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures; -using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures; using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; using Bit.Core.Test.AutoFixture.Relays; -using Bit.Core.Test.AutoFixture.TransactionFixtures; using Bit.Core.Test.AutoFixture.UserFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; using Core.Models.Data; namespace Bit.Core.Test.AutoFixture.CipherFixtures diff --git a/test/Core.Test/AutoFixture/CollectionCipherFixtures.cs b/test/Core.Test/AutoFixture/CollectionCipherFixtures.cs index 54bc7524c..79a3cd38a 100644 --- a/test/Core.Test/AutoFixture/CollectionCipherFixtures.cs +++ b/test/Core.Test/AutoFixture/CollectionCipherFixtures.cs @@ -1,22 +1,15 @@ using AutoFixture; using TableModel = Bit.Core.Models.Table; -using Bit.Core.Test.AutoFixture.Attributes; -using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures; -using AutoMapper; -using Bit.Core.Models.EntityFramework; -using Bit.Core.Models; -using System.Collections.Generic; -using Bit.Core.Enums; using AutoFixture.Kernel; using System; -using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Repositories.EntityFramework; using Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures; -using Bit.Core.Test.AutoFixture.TransactionFixtures; using Bit.Core.Test.AutoFixture.Relays; using Bit.Core.Test.AutoFixture.CollectionFixtures; using Bit.Core.Test.AutoFixture.CipherFixtures; using Bit.Core.Test.AutoFixture.UserFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; namespace Bit.Core.Test.AutoFixture.CollectionCipherFixtures { diff --git a/test/Core.Test/AutoFixture/CollectionFixtures.cs b/test/Core.Test/AutoFixture/CollectionFixtures.cs index adad029d8..fd1d91b5c 100644 --- a/test/Core.Test/AutoFixture/CollectionFixtures.cs +++ b/test/Core.Test/AutoFixture/CollectionFixtures.cs @@ -1,19 +1,13 @@ using AutoFixture; using TableModel = Bit.Core.Models.Table; -using Bit.Core.Test.AutoFixture.Attributes; -using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures; -using AutoMapper; -using Bit.Core.Models.EntityFramework; -using Bit.Core.Models; -using System.Collections.Generic; -using Bit.Core.Enums; using AutoFixture.Kernel; using System; using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Repositories.EntityFramework; using Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures; -using Bit.Core.Test.AutoFixture.TransactionFixtures; using Bit.Core.Test.AutoFixture.Relays; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; namespace Bit.Core.Test.AutoFixture.CollectionFixtures { diff --git a/test/Core.Test/AutoFixture/CurrentContextFixtures.cs b/test/Core.Test/AutoFixture/CurrentContextFixtures.cs index 21b82102b..8dcaeb646 100644 --- a/test/Core.Test/AutoFixture/CurrentContextFixtures.cs +++ b/test/Core.Test/AutoFixture/CurrentContextFixtures.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using AutoFixture; using AutoFixture.Kernel; using Bit.Core.Context; +using Bit.Test.Common.AutoFixture; namespace Bit.Core.Test.AutoFixture.CurrentContextFixtures { diff --git a/test/Core.Test/AutoFixture/DeviceFixtures.cs b/test/Core.Test/AutoFixture/DeviceFixtures.cs index 17472c5fb..4fac109b0 100644 --- a/test/Core.Test/AutoFixture/DeviceFixtures.cs +++ b/test/Core.Test/AutoFixture/DeviceFixtures.cs @@ -1,20 +1,13 @@ using AutoFixture; using TableModel = Bit.Core.Models.Table; -using Bit.Core.Test.AutoFixture.Attributes; -using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures; -using AutoMapper; -using Bit.Core.Models.EntityFramework; -using Bit.Core.Models; -using System.Collections.Generic; -using Bit.Core.Enums; using AutoFixture.Kernel; using System; -using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Repositories.EntityFramework; using Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures; using Bit.Core.Test.AutoFixture.UserFixtures; -using Bit.Core.Test.AutoFixture.TransactionFixtures; using Bit.Core.Test.AutoFixture.Relays; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; namespace Bit.Core.Test.AutoFixture.DeviceFixtures { diff --git a/test/Core.Test/AutoFixture/EmergencyAccessFixtures.cs b/test/Core.Test/AutoFixture/EmergencyAccessFixtures.cs index cdf240803..760a92a1e 100644 --- a/test/Core.Test/AutoFixture/EmergencyAccessFixtures.cs +++ b/test/Core.Test/AutoFixture/EmergencyAccessFixtures.cs @@ -1,21 +1,13 @@ using AutoFixture; using TableModel = Bit.Core.Models.Table; -using Bit.Core.Test.AutoFixture.Attributes; -using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures; -using AutoMapper; -using Bit.Core.Models.EntityFramework; -using Bit.Core.Models; -using System.Collections.Generic; -using Bit.Core.Enums; using AutoFixture.Kernel; using System; -using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Repositories.EntityFramework; using Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures; using Bit.Core.Test.AutoFixture.UserFixtures; -using Bit.Core.Test.AutoFixture.TransactionFixtures; -using AutoFixture.DataAnnotations; using Bit.Core.Test.AutoFixture.Relays; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; namespace Bit.Core.Test.AutoFixture.EmergencyAccessFixtures { diff --git a/test/Core.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs b/test/Core.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs index 88bf72168..0d82ef9b4 100644 --- a/test/Core.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs +++ b/test/Core.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs @@ -9,9 +9,9 @@ using Moq; using Microsoft.Extensions.DependencyInjection; using System.Reflection; using Bit.Core.Repositories.EntityFramework; -using Bit.Core.Test.Helpers.Factories; using Microsoft.EntityFrameworkCore; using Bit.Core.Settings; +using Bit.Core.Test.Helpers.Factories; namespace Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures { @@ -76,6 +76,7 @@ namespace Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures cfg.AddProfile(); cfg.AddProfile(); cfg.AddProfile(); + cfg.AddProfile(); cfg.AddProfile(); cfg.AddProfile(); cfg.AddProfile(); diff --git a/test/Core.Test/AutoFixture/EventFixtures.cs b/test/Core.Test/AutoFixture/EventFixtures.cs index b903b2639..f5674ddd4 100644 --- a/test/Core.Test/AutoFixture/EventFixtures.cs +++ b/test/Core.Test/AutoFixture/EventFixtures.cs @@ -1,18 +1,12 @@ using AutoFixture; using TableModel = Bit.Core.Models.Table; -using Bit.Core.Test.AutoFixture.Attributes; -using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures; -using AutoMapper; -using Bit.Core.Models.EntityFramework; -using Bit.Core.Models; -using System.Collections.Generic; -using Bit.Core.Enums; using AutoFixture.Kernel; using System; -using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Repositories.EntityFramework; using Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures; using Bit.Core.Test.AutoFixture.Relays; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; namespace Bit.Core.Test.AutoFixture.EventFixtures { diff --git a/test/Core.Test/AutoFixture/FolderFixtures.cs b/test/Core.Test/AutoFixture/FolderFixtures.cs index 9449ae71c..f46de2f75 100644 --- a/test/Core.Test/AutoFixture/FolderFixtures.cs +++ b/test/Core.Test/AutoFixture/FolderFixtures.cs @@ -1,19 +1,13 @@ using AutoFixture; using TableModel = Bit.Core.Models.Table; -using Bit.Core.Test.AutoFixture.Attributes; -using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures; -using AutoMapper; -using Bit.Core.Models.EntityFramework; -using Bit.Core.Models; -using System.Collections.Generic; -using Bit.Core.Enums; using AutoFixture.Kernel; using System; -using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Repositories.EntityFramework; using Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures; using Bit.Core.Test.AutoFixture.Relays; using Bit.Core.Test.AutoFixture.UserFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; namespace Bit.Core.Test.AutoFixture.FolderFixtures { diff --git a/test/Core.Test/AutoFixture/GrantFixtures.cs b/test/Core.Test/AutoFixture/GrantFixtures.cs index a0142a29a..2c837f216 100644 --- a/test/Core.Test/AutoFixture/GrantFixtures.cs +++ b/test/Core.Test/AutoFixture/GrantFixtures.cs @@ -1,18 +1,12 @@ using AutoFixture; using TableModel = Bit.Core.Models.Table; -using Bit.Core.Test.AutoFixture.Attributes; -using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures; -using AutoMapper; -using Bit.Core.Models.EntityFramework; -using Bit.Core.Models; -using System.Collections.Generic; -using Bit.Core.Enums; using AutoFixture.Kernel; using System; -using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Repositories.EntityFramework; using Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures; using Bit.Core.Test.AutoFixture.Relays; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; namespace Bit.Core.Test.AutoFixture.GrantFixtures { diff --git a/test/Core.Test/AutoFixture/GroupFixtures.cs b/test/Core.Test/AutoFixture/GroupFixtures.cs index 77190ce7b..b13a1c063 100644 --- a/test/Core.Test/AutoFixture/GroupFixtures.cs +++ b/test/Core.Test/AutoFixture/GroupFixtures.cs @@ -1,7 +1,5 @@ using AutoFixture; using TableModel = Bit.Core.Models.Table; -using Bit.Core.Test.AutoFixture.Attributes; -using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures; using AutoFixture.Kernel; using System; using Bit.Core.Test.AutoFixture.OrganizationFixtures; @@ -9,6 +7,8 @@ using Bit.Core.Repositories.EntityFramework; using Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures; using Bit.Core.Test.AutoFixture.Relays; using Fixtures = Bit.Core.Test.AutoFixture.OrganizationFixtures; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.AutoFixture; namespace Bit.Core.Test.AutoFixture.GroupFixtures { diff --git a/test/Core.Test/AutoFixture/GroupUserFixtures.cs b/test/Core.Test/AutoFixture/GroupUserFixtures.cs index 1ee09445e..6a44d7c81 100644 --- a/test/Core.Test/AutoFixture/GroupUserFixtures.cs +++ b/test/Core.Test/AutoFixture/GroupUserFixtures.cs @@ -1,17 +1,11 @@ using AutoFixture; using TableModel = Bit.Core.Models.Table; -using Bit.Core.Test.AutoFixture.Attributes; -using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures; -using AutoMapper; -using Bit.Core.Models.EntityFramework; -using Bit.Core.Models; -using System.Collections.Generic; -using Bit.Core.Enums; using AutoFixture.Kernel; using System; -using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Repositories.EntityFramework; using Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; namespace Bit.Core.Test.AutoFixture.GroupUserFixtures { diff --git a/test/Core.Test/AutoFixture/InstallationFixtures.cs b/test/Core.Test/AutoFixture/InstallationFixtures.cs index 8e1aa74ee..87980fa82 100644 --- a/test/Core.Test/AutoFixture/InstallationFixtures.cs +++ b/test/Core.Test/AutoFixture/InstallationFixtures.cs @@ -1,17 +1,11 @@ using AutoFixture; using TableModel = Bit.Core.Models.Table; -using Bit.Core.Test.AutoFixture.Attributes; -using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures; -using AutoMapper; -using Bit.Core.Models.EntityFramework; -using Bit.Core.Models; -using System.Collections.Generic; -using Bit.Core.Enums; using AutoFixture.Kernel; using System; -using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Repositories.EntityFramework; using Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; namespace Bit.Core.Test.AutoFixture.InstallationFixtures { diff --git a/test/Core.Test/AutoFixture/OrganizationFixtures.cs b/test/Core.Test/AutoFixture/OrganizationFixtures.cs index 2eb84ea6a..819ebeb85 100644 --- a/test/Core.Test/AutoFixture/OrganizationFixtures.cs +++ b/test/Core.Test/AutoFixture/OrganizationFixtures.cs @@ -7,13 +7,13 @@ using Bit.Core.Enums; using Bit.Core.Models.Business; using Bit.Core.Models.Data; using TableModel = Bit.Core.Models.Table; -using Bit.Core.Test.AutoFixture.Attributes; -using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures; using Bit.Core.Utilities; using AutoFixture.Kernel; using Bit.Core.Models; using Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures; using Bit.Core.Repositories.EntityFramework; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.AutoFixture; namespace Bit.Core.Test.AutoFixture.OrganizationFixtures { @@ -67,9 +67,9 @@ namespace Bit.Core.Test.AutoFixture.OrganizationFixtures public PlanType CheckedPlanType { get; set; } public void Customize(IFixture fixture) { - var validUpgradePlans = StaticStore.Plans.Where(p => p.Type != Enums.PlanType.Free && !p.Disabled).Select(p => p.Type).ToList(); + var validUpgradePlans = StaticStore.Plans.Where(p => p.Type != PlanType.Free && !p.Disabled).Select(p => p.Type).ToList(); var lowestActivePaidPlan = validUpgradePlans.First(); - CheckedPlanType = CheckedPlanType.Equals(Enums.PlanType.Free) ? lowestActivePaidPlan : CheckedPlanType; + CheckedPlanType = CheckedPlanType.Equals(PlanType.Free) ? lowestActivePaidPlan : CheckedPlanType; validUpgradePlans.Remove(lowestActivePaidPlan); fixture.Customize(composer => composer .With(o => o.PlanType, CheckedPlanType)); diff --git a/test/Core.Test/AutoFixture/OrganizationSponsorshipFixtures.cs b/test/Core.Test/AutoFixture/OrganizationSponsorshipFixtures.cs new file mode 100644 index 000000000..7c39ca097 --- /dev/null +++ b/test/Core.Test/AutoFixture/OrganizationSponsorshipFixtures.cs @@ -0,0 +1,60 @@ +using AutoFixture; +using TableModel = Bit.Core.Models.Table; +using AutoFixture.Kernel; +using System; +using Bit.Core.Repositories.EntityFramework; +using Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; +using Bit.Core.Test.AutoFixture.OrganizationFixtures; + +namespace Bit.Core.Test.AutoFixture.OrganizationSponsorshipFixtures +{ + internal class OrganizationSponsorshipBuilder : ISpecimenBuilder + { + public object Create(object request, ISpecimenContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + var type = request as Type; + if (type == null || type != typeof(TableModel.OrganizationSponsorship)) + { + return new NoSpecimen(); + } + + var fixture = new Fixture(); + var obj = fixture.WithAutoNSubstitutions().Create(); + return obj; + } + } + + internal class EfOrganizationSponsorship : ICustomization + { + public void Customize(IFixture fixture) + { + fixture.Customizations.Add(new IgnoreVirtualMembersCustomization()); + fixture.Customizations.Add(new GlobalSettingsBuilder()); + fixture.Customizations.Add(new OrganizationSponsorshipBuilder()); + fixture.Customizations.Add(new OrganizationUserBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + fixture.Customizations.Add(new EfRepositoryListBuilder()); + } + } + + internal class EfOrganizationSponsorshipAutoDataAttribute : CustomAutoDataAttribute + { + public EfOrganizationSponsorshipAutoDataAttribute() : base(new SutProviderCustomization(), new EfOrganizationSponsorship(), new EfOrganization()) + { } + } + + internal class InlineEfOrganizationSponsorshipAutoDataAttribute : InlineCustomAutoDataAttribute + { + public InlineEfOrganizationSponsorshipAutoDataAttribute(params object[] values) : base(new[] { typeof(SutProviderCustomization), + typeof(EfOrganizationSponsorship), typeof(EfOrganization) }, values) + { } + } +} diff --git a/test/Core.Test/AutoFixture/OrganizationUserFixtures.cs b/test/Core.Test/AutoFixture/OrganizationUserFixtures.cs index f5e5256dd..1a083c2f3 100644 --- a/test/Core.Test/AutoFixture/OrganizationUserFixtures.cs +++ b/test/Core.Test/AutoFixture/OrganizationUserFixtures.cs @@ -1,9 +1,5 @@ using AutoFixture; using TableModel = Bit.Core.Models.Table; -using Bit.Core.Test.AutoFixture.Attributes; -using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures; -using AutoMapper; -using Bit.Core.Models.EntityFramework; using Bit.Core.Models; using System.Collections.Generic; using Bit.Core.Enums; @@ -17,6 +13,8 @@ using System.Text.Json; using Bit.Core.Test.AutoFixture.UserFixtures; using AutoFixture.Xunit2; using System.Reflection; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; namespace Bit.Core.Test.AutoFixture.OrganizationUserFixtures { diff --git a/test/Core.Test/AutoFixture/PolicyFixtures.cs b/test/Core.Test/AutoFixture/PolicyFixtures.cs index 868991693..71b2e30a7 100644 --- a/test/Core.Test/AutoFixture/PolicyFixtures.cs +++ b/test/Core.Test/AutoFixture/PolicyFixtures.cs @@ -2,14 +2,14 @@ using System.Reflection; using AutoFixture; using TableModel = Bit.Core.Models.Table; -using Bit.Core.Test.AutoFixture.Attributes; -using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures; using Bit.Core.Enums; using AutoFixture.Kernel; using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Repositories.EntityFramework; using Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures; using AutoFixture.Xunit2; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; namespace Bit.Core.Test.AutoFixture.PolicyFixtures { diff --git a/test/Core.Test/AutoFixture/SendFixtures.cs b/test/Core.Test/AutoFixture/SendFixtures.cs index 1588837c6..17dee60ed 100644 --- a/test/Core.Test/AutoFixture/SendFixtures.cs +++ b/test/Core.Test/AutoFixture/SendFixtures.cs @@ -3,12 +3,12 @@ using AutoFixture; using AutoFixture.Kernel; using Bit.Core.Models.Table; using Bit.Core.Repositories.EntityFramework; -using Bit.Core.Test.AutoFixture.Attributes; using Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures; -using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures; using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Test.AutoFixture.Relays; using Bit.Core.Test.AutoFixture.UserFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; namespace Bit.Core.Test.AutoFixture.SendFixtures { diff --git a/test/Core.Test/AutoFixture/SsoConfigFixtures.cs b/test/Core.Test/AutoFixture/SsoConfigFixtures.cs index 14e681af7..5ecbc247c 100644 --- a/test/Core.Test/AutoFixture/SsoConfigFixtures.cs +++ b/test/Core.Test/AutoFixture/SsoConfigFixtures.cs @@ -1,16 +1,14 @@ using System; using AutoFixture; using AutoFixture.Kernel; -using AutoMapper; using TableModel = Bit.Core.Models.Table; -using Bit.Core.Models.EntityFramework; -using Bit.Core.Test.AutoFixture.Attributes; -using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures; using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Models.Data; using System.Text.Json; using Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures; using Bit.Core.Repositories.EntityFramework; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; namespace Bit.Core.Test.AutoFixture.SsoConfigFixtures { diff --git a/test/Core.Test/AutoFixture/SsoUserFixtures.cs b/test/Core.Test/AutoFixture/SsoUserFixtures.cs index bb756a73b..9dc452466 100644 --- a/test/Core.Test/AutoFixture/SsoUserFixtures.cs +++ b/test/Core.Test/AutoFixture/SsoUserFixtures.cs @@ -1,18 +1,16 @@ using AutoFixture; -using AutoMapper; -using Bit.Core.Models.EntityFramework; using Bit.Core.Repositories.EntityFramework; -using Bit.Core.Test.AutoFixture.Attributes; using Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures; -using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures; using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Test.AutoFixture.UserFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; using TableModel = Bit.Core.Models.Table; namespace Bit.Core.Test.AutoFixture.SsoUserFixtures { - internal class EfSsoUser: ICustomization - { + internal class EfSsoUser : ICustomization + { public void Customize(IFixture fixture) { fixture.Customizations.Add(new IgnoreVirtualMembersCustomization()); diff --git a/test/Core.Test/AutoFixture/TaxRateFixtures.cs b/test/Core.Test/AutoFixture/TaxRateFixtures.cs index b0a94eacc..d1e89fa5d 100644 --- a/test/Core.Test/AutoFixture/TaxRateFixtures.cs +++ b/test/Core.Test/AutoFixture/TaxRateFixtures.cs @@ -1,18 +1,12 @@ using AutoFixture; using TableModel = Bit.Core.Models.Table; -using Bit.Core.Test.AutoFixture.Attributes; -using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures; -using AutoMapper; -using Bit.Core.Models.EntityFramework; -using Bit.Core.Models; -using System.Collections.Generic; -using Bit.Core.Enums; using AutoFixture.Kernel; using System; -using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Repositories.EntityFramework; using Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures; using Bit.Core.Test.AutoFixture.Relays; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; namespace Bit.Core.Test.AutoFixture.TaxRateFixtures { diff --git a/test/Core.Test/AutoFixture/TransactionFixutres.cs b/test/Core.Test/AutoFixture/TransactionFixutres.cs index e2660c8c7..0610ced02 100644 --- a/test/Core.Test/AutoFixture/TransactionFixutres.cs +++ b/test/Core.Test/AutoFixture/TransactionFixutres.cs @@ -1,12 +1,6 @@ using AutoFixture; using TableModel = Bit.Core.Models.Table; -using Bit.Core.Test.AutoFixture.Attributes; -using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures; -using AutoMapper; using Bit.Core.Models.EntityFramework; -using Bit.Core.Models; -using System.Collections.Generic; -using Bit.Core.Enums; using AutoFixture.Kernel; using System; using Bit.Core.Test.AutoFixture.OrganizationFixtures; @@ -14,6 +8,8 @@ using Bit.Core.Repositories.EntityFramework; using Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures; using Bit.Core.Test.AutoFixture.UserFixtures; using Bit.Core.Test.AutoFixture.Relays; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; namespace Bit.Core.Test.AutoFixture.TransactionFixtures { diff --git a/test/Core.Test/AutoFixture/U2fFixtures.cs b/test/Core.Test/AutoFixture/U2fFixtures.cs index d813fd20e..78fac0c4d 100644 --- a/test/Core.Test/AutoFixture/U2fFixtures.cs +++ b/test/Core.Test/AutoFixture/U2fFixtures.cs @@ -1,19 +1,13 @@ using AutoFixture; using TableModel = Bit.Core.Models.Table; -using Bit.Core.Test.AutoFixture.Attributes; -using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures; -using AutoMapper; -using Bit.Core.Models.EntityFramework; -using Bit.Core.Models; -using System.Collections.Generic; -using Bit.Core.Enums; using AutoFixture.Kernel; using System; -using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Repositories.EntityFramework; using Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures; using Bit.Core.Test.AutoFixture.Relays; using Bit.Core.Test.AutoFixture.UserFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; namespace Bit.Core.Test.AutoFixture.U2fFixtures { diff --git a/test/Core.Test/AutoFixture/UserFixtures.cs b/test/Core.Test/AutoFixture/UserFixtures.cs index fb9a3e27c..1608db151 100644 --- a/test/Core.Test/AutoFixture/UserFixtures.cs +++ b/test/Core.Test/AutoFixture/UserFixtures.cs @@ -1,7 +1,5 @@ using AutoFixture; using TableModel = Bit.Core.Models.Table; -using Bit.Core.Test.AutoFixture.Attributes; -using Bit.Core.Test.AutoFixture.GlobalSettingsFixtures; using Bit.Core.Models; using System.Collections.Generic; using Bit.Core.Enums; @@ -10,6 +8,8 @@ using System; using Bit.Core.Test.AutoFixture.OrganizationFixtures; using Bit.Core.Repositories.EntityFramework; using Bit.Core.Test.AutoFixture.EntityFrameworkRepositoryFixtures; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.AutoFixture; namespace Bit.Core.Test.AutoFixture.UserFixtures { diff --git a/test/Core.Test/Core.Test.csproj b/test/Core.Test/Core.Test.csproj index 45c44fa1b..725a51d4c 100644 --- a/test/Core.Test/Core.Test.csproj +++ b/test/Core.Test/Core.Test.csproj @@ -25,6 +25,7 @@ + diff --git a/test/Core.Test/Helpers/Factories.cs b/test/Core.Test/Helpers/Factories.cs index 61469a543..56092122d 100644 --- a/test/Core.Test/Helpers/Factories.cs +++ b/test/Core.Test/Helpers/Factories.cs @@ -1,22 +1,12 @@ using System.Collections.Generic; using Bit.Core.Repositories.EntityFramework; using Bit.Core.Settings; +using Bit.Test.Common.Helpers.Factories; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; namespace Bit.Core.Test.Helpers.Factories { - public static class GlobalSettingsFactory - { - public static GlobalSettings GlobalSettings { get; } = new GlobalSettings(); - static GlobalSettingsFactory() - { - var configBuilder = new ConfigurationBuilder().AddUserSecrets(); - var Configuration = configBuilder.Build(); - ConfigurationBinder.Bind(Configuration.GetSection("GlobalSettings"), GlobalSettings); - } - } - public static class DatabaseOptionsFactory { public static List> Options { get; } = new List>(); diff --git a/test/Core.Test/Repositories/EntityFramework/EqualityComparers/OrganizationSponsorshipCompare.cs b/test/Core.Test/Repositories/EntityFramework/EqualityComparers/OrganizationSponsorshipCompare.cs new file mode 100644 index 000000000..cd1c29262 --- /dev/null +++ b/test/Core.Test/Repositories/EntityFramework/EqualityComparers/OrganizationSponsorshipCompare.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Bit.Core.Models.Table; + +namespace Bit.Core.Test.Repositories.EntityFramework.EqualityComparers +{ + public class OrganizationSponsorshipCompare : IEqualityComparer + { + public bool Equals(OrganizationSponsorship x, OrganizationSponsorship y) + { + return x.InstallationId.Equals(y.InstallationId) && + x.SponsoringOrganizationId.Equals(y.SponsoringOrganizationId) && + x.SponsoringOrganizationUserId.Equals(y.SponsoringOrganizationUserId) && + x.SponsoredOrganizationId.Equals(y.SponsoredOrganizationId) && + x.OfferedToEmail.Equals(y.OfferedToEmail) && + x.CloudSponsor.Equals(y.CloudSponsor) && + x.TimesRenewedWithoutValidation.Equals(y.TimesRenewedWithoutValidation) && + x.SponsorshipLapsedDate.ToString().Equals(y.SponsorshipLapsedDate.ToString()); + } + + public int GetHashCode([DisallowNull] OrganizationSponsorship obj) + { + return base.GetHashCode(); + } + } +} diff --git a/test/Core.Test/Repositories/EntityFramework/OrganizationSponsorshipRepositoryTests.cs b/test/Core.Test/Repositories/EntityFramework/OrganizationSponsorshipRepositoryTests.cs new file mode 100644 index 000000000..67b7ab409 --- /dev/null +++ b/test/Core.Test/Repositories/EntityFramework/OrganizationSponsorshipRepositoryTests.cs @@ -0,0 +1,138 @@ +using EfRepo = Bit.Core.Repositories.EntityFramework; +using SqlRepo = Bit.Core.Repositories.SqlServer; +using System.Collections.Generic; +using System.Linq; +using TableModel = Bit.Core.Models.Table; +using Xunit; +using Bit.Core.Test.Repositories.EntityFramework.EqualityComparers; +using Bit.Core.Test.AutoFixture.Attributes; +using Bit.Core.Test.AutoFixture.OrganizationSponsorshipFixtures; + +namespace Bit.Core.Test.Repositories.EntityFramework +{ + public class OrganizationSponsorshipRepositoryTests + { + [CiSkippedTheory, EfOrganizationSponsorshipAutoData] + public async void CreateAsync_Works_DataMatches( + TableModel.OrganizationSponsorship organizationSponsorship, TableModel.Organization sponsoringOrg, + List efOrgRepos, + SqlRepo.OrganizationRepository sqlOrganizationRepo, + SqlRepo.OrganizationSponsorshipRepository sqlOrganizationSponsorshipRepo, + OrganizationSponsorshipCompare equalityComparer, + List suts) + { + organizationSponsorship.InstallationId = null; + organizationSponsorship.SponsoredOrganizationId = null; + + var savedOrganizationSponsorships = new List(); + foreach (var (sut, orgRepo) in suts.Zip(efOrgRepos)) + { + var efSponsoringOrg = await orgRepo.CreateAsync(sponsoringOrg); + sut.ClearChangeTracking(); + organizationSponsorship.SponsoringOrganizationId = efSponsoringOrg.Id; + + await sut.CreateAsync(organizationSponsorship); + sut.ClearChangeTracking(); + + var savedOrganizationSponsorship = await sut.GetByIdAsync(organizationSponsorship.Id); + savedOrganizationSponsorships.Add(savedOrganizationSponsorship); + } + + var sqlSponsoringOrg = await sqlOrganizationRepo.CreateAsync(sponsoringOrg); + organizationSponsorship.SponsoringOrganizationId = sqlSponsoringOrg.Id; + + var sqlOrganizationSponsorship = await sqlOrganizationSponsorshipRepo.CreateAsync(organizationSponsorship); + savedOrganizationSponsorships.Add(await sqlOrganizationSponsorshipRepo.GetByIdAsync(sqlOrganizationSponsorship.Id)); + + var distinctItems = savedOrganizationSponsorships.Distinct(equalityComparer); + Assert.True(!distinctItems.Skip(1).Any()); + } + + [CiSkippedTheory, EfOrganizationSponsorshipAutoData] + public async void ReplaceAsync_Works_DataMatches(TableModel.OrganizationSponsorship postOrganizationSponsorship, + TableModel.OrganizationSponsorship replaceOrganizationSponsorship, TableModel.Organization sponsoringOrg, + List efOrgRepos, + SqlRepo.OrganizationRepository sqlOrganizationRepo, + SqlRepo.OrganizationSponsorshipRepository sqlOrganizationSponsorshipRepo, + OrganizationSponsorshipCompare equalityComparer, List suts) + { + postOrganizationSponsorship.InstallationId = null; + postOrganizationSponsorship.SponsoredOrganizationId = null; + replaceOrganizationSponsorship.InstallationId = null; + replaceOrganizationSponsorship.SponsoredOrganizationId = null; + + var savedOrganizationSponsorships = new List(); + foreach (var (sut, orgRepo) in suts.Zip(efOrgRepos)) + { + var efSponsoringOrg = await orgRepo.CreateAsync(sponsoringOrg); + sut.ClearChangeTracking(); + postOrganizationSponsorship.SponsoringOrganizationId = efSponsoringOrg.Id; + replaceOrganizationSponsorship.SponsoringOrganizationId = efSponsoringOrg.Id; + + var postEfOrganizationSponsorship = await sut.CreateAsync(postOrganizationSponsorship); + sut.ClearChangeTracking(); + + replaceOrganizationSponsorship.Id = postEfOrganizationSponsorship.Id; + await sut.ReplaceAsync(replaceOrganizationSponsorship); + sut.ClearChangeTracking(); + + var replacedOrganizationSponsorship = await sut.GetByIdAsync(replaceOrganizationSponsorship.Id); + savedOrganizationSponsorships.Add(replacedOrganizationSponsorship); + } + + var sqlSponsoringOrg = await sqlOrganizationRepo.CreateAsync(sponsoringOrg); + postOrganizationSponsorship.SponsoringOrganizationId = sqlSponsoringOrg.Id; + + var postSqlOrganization = await sqlOrganizationSponsorshipRepo.CreateAsync(postOrganizationSponsorship); + replaceOrganizationSponsorship.Id = postSqlOrganization.Id; + await sqlOrganizationSponsorshipRepo.ReplaceAsync(replaceOrganizationSponsorship); + savedOrganizationSponsorships.Add(await sqlOrganizationSponsorshipRepo.GetByIdAsync(replaceOrganizationSponsorship.Id)); + + var distinctItems = savedOrganizationSponsorships.Distinct(equalityComparer); + Assert.True(!distinctItems.Skip(1).Any()); + } + + [CiSkippedTheory, EfOrganizationSponsorshipAutoData] + public async void DeleteAsync_Works_DataMatches(TableModel.OrganizationSponsorship organizationSponsorship, + TableModel.Organization sponsoringOrg, + List efOrgRepos, + SqlRepo.OrganizationRepository sqlOrganizationRepo, + SqlRepo.OrganizationSponsorshipRepository sqlOrganizationSponsorshipRepo, + List suts) + { + organizationSponsorship.InstallationId = null; + organizationSponsorship.SponsoredOrganizationId = null; + + foreach (var (sut, orgRepo) in suts.Zip(efOrgRepos)) + { + var efSponsoringOrg = await orgRepo.CreateAsync(sponsoringOrg); + sut.ClearChangeTracking(); + organizationSponsorship.SponsoringOrganizationId = efSponsoringOrg.Id; + + var postEfOrganizationSponsorship = await sut.CreateAsync(organizationSponsorship); + sut.ClearChangeTracking(); + + var savedEfOrganizationSponsorship = await sut.GetByIdAsync(postEfOrganizationSponsorship.Id); + sut.ClearChangeTracking(); + Assert.True(savedEfOrganizationSponsorship != null); + + await sut.DeleteAsync(savedEfOrganizationSponsorship); + sut.ClearChangeTracking(); + + savedEfOrganizationSponsorship = await sut.GetByIdAsync(savedEfOrganizationSponsorship.Id); + Assert.True(savedEfOrganizationSponsorship == null); + } + + var sqlSponsoringOrg = await sqlOrganizationRepo.CreateAsync(sponsoringOrg); + organizationSponsorship.SponsoringOrganizationId = sqlSponsoringOrg.Id; + + var postSqlOrganizationSponsorship = await sqlOrganizationSponsorshipRepo.CreateAsync(organizationSponsorship); + var savedSqlOrganizationSponsorship = await sqlOrganizationSponsorshipRepo.GetByIdAsync(postSqlOrganizationSponsorship.Id); + Assert.True(savedSqlOrganizationSponsorship != null); + + await sqlOrganizationSponsorshipRepo.DeleteAsync(postSqlOrganizationSponsorship); + savedSqlOrganizationSponsorship = await sqlOrganizationSponsorshipRepo.GetByIdAsync(postSqlOrganizationSponsorship.Id); + Assert.True(savedSqlOrganizationSponsorship == null); + } + } +} diff --git a/test/Core.Test/Services/CipherServiceTests.cs b/test/Core.Test/Services/CipherServiceTests.cs index 94aa49863..45e1f189f 100644 --- a/test/Core.Test/Services/CipherServiceTests.cs +++ b/test/Core.Test/Services/CipherServiceTests.cs @@ -9,9 +9,9 @@ using Bit.Core.Models.Table; using Core.Models.Data; using Bit.Core.Test.AutoFixture.CipherFixtures; using System.Collections.Generic; -using Bit.Core.Test.AutoFixture; using System.Linq; using Castle.Core.Internal; +using Bit.Test.Common.AutoFixture; namespace Bit.Core.Test.Services { diff --git a/test/Core.Test/Services/CollectionServiceTests.cs b/test/Core.Test/Services/CollectionServiceTests.cs index b1748e858..727621690 100644 --- a/test/Core.Test/Services/CollectionServiceTests.cs +++ b/test/Core.Test/Services/CollectionServiceTests.cs @@ -7,9 +7,9 @@ using Bit.Core.Models.Data; using Bit.Core.Models.Table; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Test.AutoFixture; -using Bit.Core.Test.AutoFixture.Attributes; using Bit.Core.Test.AutoFixture.CollectionFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; diff --git a/test/Core.Test/Services/DeviceServiceTests.cs b/test/Core.Test/Services/DeviceServiceTests.cs index dd125759b..413648634 100644 --- a/test/Core.Test/Services/DeviceServiceTests.cs +++ b/test/Core.Test/Services/DeviceServiceTests.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Bit.Core.Enums; using Bit.Core.Models.Table; using Bit.Core.Repositories; using Bit.Core.Services; @@ -23,7 +24,7 @@ namespace Bit.Core.Test.Services { Id = id, Name = "test device", - Type = Enums.DeviceType.Android, + Type = DeviceType.Android, UserId = userId, PushToken = "testtoken", Identifier = "testid" @@ -32,7 +33,7 @@ namespace Bit.Core.Test.Services Assert.True(device.RevisionDate - DateTime.UtcNow < TimeSpan.FromSeconds(1)); await pushRepo.Received().CreateOrUpdateRegistrationAsync("testtoken", id.ToString(), - userId.ToString(), "testid", Enums.DeviceType.Android); + userId.ToString(), "testid", DeviceType.Android); } } } diff --git a/test/Core.Test/Services/EmergencyAccessServiceTests.cs b/test/Core.Test/Services/EmergencyAccessServiceTests.cs index f3812f20f..9493238e9 100644 --- a/test/Core.Test/Services/EmergencyAccessServiceTests.cs +++ b/test/Core.Test/Services/EmergencyAccessServiceTests.cs @@ -4,6 +4,8 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Test.AutoFixture.Attributes; using Bit.Core.Test.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using System.Threading.Tasks; using System; diff --git a/test/Core.Test/Services/GroupServiceTests.cs b/test/Core.Test/Services/GroupServiceTests.cs index 3b8467ce3..04ddf20ce 100644 --- a/test/Core.Test/Services/GroupServiceTests.cs +++ b/test/Core.Test/Services/GroupServiceTests.cs @@ -10,6 +10,8 @@ using Bit.Core.Services; using Bit.Core.Test.AutoFixture; using Bit.Core.Test.AutoFixture.Attributes; using Bit.Core.Test.AutoFixture.GroupFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; diff --git a/test/Core.Test/Services/HandlebarsMailServiceTests.cs b/test/Core.Test/Services/HandlebarsMailServiceTests.cs index 1204a0613..d5de7a684 100644 --- a/test/Core.Test/Services/HandlebarsMailServiceTests.cs +++ b/test/Core.Test/Services/HandlebarsMailServiceTests.cs @@ -1,6 +1,14 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Bit.Core.Models.Business; +using Bit.Core.Models.Table; +using Bit.Core.Models.Table.Provider; using Bit.Core.Services; using Bit.Core.Settings; +using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -27,6 +35,137 @@ namespace Bit.Core.Test.Services ); } + [Fact(Skip = "For local development")] + public async Task SendAllEmails() + { + // This test is only opt in and is more for development purposes. + // This will send all emails to the test email address so that they can be viewed. + var namedParameters = new Dictionary<(string, Type), object> + { + // TODO: Swith to use env variable + { ("email", typeof(string)), "test@bitwarden.com" }, + { ("user", typeof(User)), new User + { + Id = Guid.NewGuid(), + Email = "test@bitwarden.com", + }}, + { ("userId", typeof(Guid)), Guid.NewGuid() }, + { ("token", typeof(string)), "test_token" }, + { ("fromEmail", typeof(string)), "test@bitwarden.com" }, + { ("toEmail", typeof(string)), "test@bitwarden.com" }, + { ("newEmailAddress", typeof(string)), "test@bitwarden.com" }, + { ("hint", typeof(string)), "Test Hint" }, + { ("organizationName", typeof(string)), "Test Organization Name" }, + { ("orgUser", typeof(OrganizationUser)), new OrganizationUser + { + Id = Guid.NewGuid(), + Email = "test@bitwarden.com", + OrganizationId = Guid.NewGuid(), + + }}, + { ("token", typeof(ExpiringToken)), new ExpiringToken("test_token", DateTime.UtcNow.AddDays(1))}, + { ("organization", typeof(Organization)), new Organization + { + Id = Guid.NewGuid(), + Name = "Test Organization Name", + Seats = 5 + }}, + { ("initialSeatCount", typeof(int)), 5}, + { ("ownerEmails", typeof(IEnumerable)), new [] { "test@bitwarden.com" }}, + { ("maxSeatCount", typeof(int)), 5 }, + { ("userIdentifier", typeof(string)), "test_user" }, + { ("adminEmails", typeof(IEnumerable)), new [] { "test@bitwarden.com" }}, + { ("returnUrl", typeof(string)), "https://bitwarden.com/" }, + { ("amount", typeof(decimal)), 1.00M }, + { ("dueDate", typeof(DateTime)), DateTime.UtcNow.AddDays(1) }, + { ("items", typeof(List)), new List { "test@bitwarden.com" }}, + { ("mentionInvoices", typeof(bool)), true }, + { ("emails", typeof(IEnumerable)), new [] { "test@bitwarden.com" }}, + { ("deviceType", typeof(string)), "Mobile" }, + { ("timestamp", typeof(DateTime)), DateTime.UtcNow.AddDays(1)}, + { ("ip", typeof(string)), "127.0.0.1" }, + { ("emergencyAccess", typeof(EmergencyAccess)), new EmergencyAccess + { + Id = Guid.NewGuid(), + Email = "test@bitwarden.com", + }}, + { ("granteeEmail", typeof(string)), "test@bitwarden.com" }, + { ("grantorName", typeof(string)), "Test User" }, + { ("initiatingName", typeof(string)), "Test" }, + { ("approvingName", typeof(string)), "Test Name" }, + { ("rejectingName", typeof(string)), "Test Name" }, + { ("provider", typeof(Provider)), new Provider + { + Id = Guid.NewGuid(), + }}, + { ("name", typeof(string)), "Test Name" }, + { ("ea", typeof(EmergencyAccess)), new EmergencyAccess + { + Id = Guid.NewGuid(), + Email = "test@bitwarden.com", + }}, + { ("userName", typeof(string)), "testUser" }, + { ("orgName", typeof(string)), "Test Org Name" }, + { ("providerName", typeof(string)), "testProvider" }, + { ("providerUser", typeof(ProviderUser)), new ProviderUser + { + ProviderId = Guid.NewGuid(), + Id = Guid.NewGuid(), + }}, + { ("familyUserEmail", typeof(string)), "test@bitwarden.com" }, + { ("sponsorEmail", typeof(string)), "test@bitwarden.com" }, + { ("familyOrgName", typeof(string)), "Test Org Name" }, + { ("orgCanSponsor", typeof(bool)), true }, + { ("existingAccount", typeof(bool)), true }, + { ("sponsorshipEndDate", typeof(DateTime)), DateTime.UtcNow.AddDays(1)}, + }; + + var globalSettings = new GlobalSettings + { + Mail = new GlobalSettings.MailSettings + { + Smtp = new GlobalSettings.MailSettings.SmtpSettings + { + Host = "localhost", + TrustServer = true, + Port = 10250, + }, + ReplyToEmail = "noreply@bitwarden.com", + }, + SiteName = "Bitwarden", + }; + + var mailDeliveryService = new MailKitSmtpMailDeliveryService(globalSettings, Substitute.For>()); + + var handlebarsService = new HandlebarsMailService(globalSettings, mailDeliveryService, new BlockingMailEnqueuingService()); + + var sendMethods = typeof(IMailService).GetMethods(BindingFlags.Public | BindingFlags.Instance) + .Where(m => m.Name.StartsWith("Send") && m.Name != "SendEnqueuedMailMessageAsync"); + + foreach (var sendMethod in sendMethods) + { + await InvokeMethod(sendMethod); + } + + async Task InvokeMethod(MethodInfo method) + { + var parameters = method.GetParameters(); + var args = new object[parameters.Length]; + + for(var i = 0; i < parameters.Length; i++) + { + if (!namedParameters.TryGetValue((parameters[i].Name, parameters[i].ParameterType), out var value)) + { + throw new InvalidOperationException($"Couldn't find a parameter for name '{parameters[i].Name}' and type '{parameters[i].ParameterType.FullName}'"); + } + + args[i] = value; + } + + await (Task)method.Invoke(handlebarsService, args); + } + } + // Remove this test when we add actual tests. It only proves that // we've properly constructed the system under test. [Fact] diff --git a/test/Core.Test/Services/LocalAttachmentStorageServiceTests.cs b/test/Core.Test/Services/LocalAttachmentStorageServiceTests.cs index e70495d47..386bc2bee 100644 --- a/test/Core.Test/Services/LocalAttachmentStorageServiceTests.cs +++ b/test/Core.Test/Services/LocalAttachmentStorageServiceTests.cs @@ -4,8 +4,6 @@ using Bit.Core.Settings; using NSubstitute; using Xunit; using System.IO; -using Bit.Core.Test.AutoFixture.Attributes; -using Bit.Core.Test.AutoFixture; using Bit.Core.Test.AutoFixture.CipherFixtures; using Bit.Core.Models.Data; using System.Threading.Tasks; @@ -13,6 +11,8 @@ using Bit.Core.Models.Table; using U2F.Core.Utils; using Bit.Core.Test.AutoFixture.CipherAttachmentMetaData; using AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.AutoFixture; namespace Bit.Core.Test.Services { diff --git a/test/Core.Test/Services/OrganizationServiceTests.cs b/test/Core.Test/Services/OrganizationServiceTests.cs index a860da785..a97abf6dd 100644 --- a/test/Core.Test/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/Services/OrganizationServiceTests.cs @@ -9,10 +9,8 @@ using Bit.Core.Repositories; using Bit.Core.Services; using NSubstitute; using Xunit; -using Bit.Core.Test.AutoFixture; using Bit.Core.Exceptions; using Bit.Core.Enums; -using Bit.Core.Test.AutoFixture.Attributes; using Bit.Core.Test.AutoFixture.OrganizationFixtures; using System.Text.Json; using Bit.Core.Context; @@ -22,7 +20,8 @@ using OrganizationUser = Bit.Core.Models.Table.OrganizationUser; using Policy = Bit.Core.Models.Table.Policy; using Bit.Core.Test.AutoFixture.PolicyFixtures; using Bit.Core.Settings; -using AutoFixture.Xunit2; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; namespace Bit.Core.Test.Services { @@ -67,6 +66,7 @@ namespace Bit.Core.Test.Services .CreateManyAsync(Arg.Is>(users => users.Count() == expectedNewUsersCount)); await sutProvider.GetDependency().Received(1) .BulkSendOrganizationInviteEmailAsync(org.Name, + Arg.Any(), Arg.Is>(messages => messages.Count() == expectedNewUsersCount)); // Send events @@ -125,6 +125,7 @@ namespace Bit.Core.Test.Services .CreateManyAsync(Arg.Is>(users => users.Count() == expectedNewUsersCount)); await sutProvider.GetDependency().Received(1) .BulkSendOrganizationInviteEmailAsync(org.Name, + Arg.Any(), Arg.Is>(messages => messages.Count() == expectedNewUsersCount)); // Sent events diff --git a/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs b/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs new file mode 100644 index 000000000..c17a4d21b --- /dev/null +++ b/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs @@ -0,0 +1,680 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Table; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using Microsoft.AspNetCore.DataProtection; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Bit.Core.Test.Services +{ + [SutProviderCustomize] + public class OrganizationSponsorshipServiceTests + { + private bool SponsorshipValidator(OrganizationSponsorship sponsorship, OrganizationSponsorship expectedSponsorship) + { + try + { + AssertHelper.AssertPropertyEqual(sponsorship, expectedSponsorship, nameof(OrganizationSponsorship.Id)); + return true; + } + catch + { + return false; + } + } + + public static IEnumerable EnterprisePlanTypes => + Enum.GetValues().Where(p => StaticStore.GetPlan(p).Product == ProductType.Enterprise).Select(p => new object[] { p }); + + public static IEnumerable NonEnterprisePlanTypes => + Enum.GetValues().Where(p => StaticStore.GetPlan(p).Product != ProductType.Enterprise).Select(p => new object[] { p }); + + public static IEnumerable NonFamiliesPlanTypes => + Enum.GetValues().Where(p => StaticStore.GetPlan(p).Product != ProductType.Families).Select(p => new object[] { p }); + + public static IEnumerable NonConfirmedOrganizationUsersStatuses => + Enum.GetValues() + .Where(s => s != OrganizationUserStatusType.Confirmed) + .Select(s => new object[] { s }); + + [Theory] + [BitMemberAutoData(nameof(NonEnterprisePlanTypes))] + public async Task OfferSponsorship_BadSponsoringOrgPlan_ThrowsBadRequest(PlanType sponsoringOrgPlan, + Organization org, OrganizationUser orgUser, SutProvider sutProvider) + { + org.PlanType = sponsoringOrgPlan; + + sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.OfferSponsorshipAsync(org, orgUser, PlanSponsorshipType.FamiliesForEnterprise, default, default, "test@bitwarden.com")); + + Assert.Contains("Specified Organization cannot sponsor other organizations.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateAsync(default); + } + + [Theory] + [BitMemberAutoData(nameof(NonConfirmedOrganizationUsersStatuses))] + public async Task CreateSponsorship_BadSponsoringUserStatus_ThrowsBadRequest( + OrganizationUserStatusType statusType, Organization org, OrganizationUser orgUser, + SutProvider sutProvider) + { + org.PlanType = PlanType.EnterpriseAnnually; + orgUser.Status = statusType; + + sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.OfferSponsorshipAsync(org, orgUser, PlanSponsorshipType.FamiliesForEnterprise, default, default, "test@bitwarden.com")); + + Assert.Contains("Only confirmed users can sponsor other organizations.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateAsync(default); + } + + [Theory] + [BitAutoData] + public async Task OfferSponsorship_AlreadySponsoring_Throws(Organization org, + OrganizationUser orgUser, OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + org.PlanType = PlanType.EnterpriseAnnually; + orgUser.Status = OrganizationUserStatusType.Confirmed; + + sutProvider.GetDependency() + .GetBySponsoringOrganizationUserIdAsync(orgUser.Id).Returns(sponsorship); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.OfferSponsorshipAsync(org, orgUser, sponsorship.PlanSponsorshipType.Value, default, default, "test@bitwarden.com")); + + Assert.Contains("Can only sponsor one organization per Organization User.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateAsync(default); + } + + [Theory] + [BitAutoData] + public async Task OfferSponsorship_CreatesSponsorship(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, + string sponsoredEmail, string friendlyName, Guid sponsorshipId, + SutProvider sutProvider) + { + const string email = "test@bitwarden.com"; + + sponsoringOrg.PlanType = PlanType.EnterpriseAnnually; + sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed; + + var dataProtector = Substitute.For(); + sutProvider.GetDependency().CreateProtector(default).ReturnsForAnyArgs(dataProtector); + sutProvider.GetDependency().CreateAsync(default).ReturnsForAnyArgs(callInfo => + { + var sponsorship = callInfo.Arg(); + sponsorship.Id = sponsorshipId; + return sponsorship; + }); + + await sutProvider.Sut.OfferSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, + PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, email); + + var expectedSponsorship = new OrganizationSponsorship + { + Id = sponsorshipId, + SponsoringOrganizationId = sponsoringOrg.Id, + SponsoringOrganizationUserId = sponsoringOrgUser.Id, + FriendlyName = friendlyName, + OfferedToEmail = sponsoredEmail, + PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise, + CloudSponsor = true, + }; + + await sutProvider.GetDependency().Received(1) + .CreateAsync(Arg.Is(s => SponsorshipValidator(s, expectedSponsorship))); + + await sutProvider.GetDependency().Received(1). + SendFamiliesForEnterpriseOfferEmailAsync(sponsoredEmail, email, + false, Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task OfferSponsorship_CreateSponsorshipThrows_RevertsDatabase(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, + string sponsoredEmail, string friendlyName, SutProvider sutProvider) + { + sponsoringOrg.PlanType = PlanType.EnterpriseAnnually; + sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed; + + var expectedException = new Exception(); + OrganizationSponsorship createdSponsorship = null; + sutProvider.GetDependency().CreateAsync(default).ThrowsForAnyArgs(callInfo => + { + createdSponsorship = callInfo.ArgAt(0); + createdSponsorship.Id = Guid.NewGuid(); + return expectedException; + }); + + var actualException = await Assert.ThrowsAsync(() => + sutProvider.Sut.OfferSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, + PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, "test@bitwarden.com")); + Assert.Same(expectedException, actualException); + + await sutProvider.GetDependency().Received(1) + .DeleteAsync(createdSponsorship); + } + + [Theory] + [BitAutoData] + public async Task ResendSponsorshipOffer_SponsoringOrgNotFound_ThrowsBadRequest( + OrganizationUser orgUser, OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.ResendSponsorshipOfferAsync(null, orgUser, sponsorship, "test@bitwarden.com")); + + Assert.Contains("Cannot find the requested sponsoring organization.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendFamiliesForEnterpriseOfferEmailAsync(default, default, default, default); + } + + [Theory] + [BitAutoData] + public async Task ResendSponsorshipOffer_SponsoringOrgUserNotFound_ThrowsBadRequest(Organization org, + OrganizationSponsorship sponsorship, SutProvider sutProvider) + { + sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.ResendSponsorshipOfferAsync(org, null, sponsorship, "test@bitwarden.com")); + + Assert.Contains("Only confirmed users can sponsor other organizations.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendFamiliesForEnterpriseOfferEmailAsync(default, default, default, default); + } + + [Theory] + [BitAutoData] + [BitMemberAutoData(nameof(NonConfirmedOrganizationUsersStatuses))] + public async Task ResendSponsorshipOffer_SponsoringOrgUserNotConfirmed_ThrowsBadRequest(OrganizationUserStatusType status, + Organization org, OrganizationUser orgUser, OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + orgUser.Status = status; + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.ResendSponsorshipOfferAsync(org, orgUser, sponsorship, "test@bitwarden.com")); + + Assert.Contains("Only confirmed users can sponsor other organizations.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendFamiliesForEnterpriseOfferEmailAsync(default, default, default, default); + } + + [Theory] + [BitAutoData] + public async Task ResendSponsorshipOffer_SponsorshipNotFound_ThrowsBadRequest(Organization org, + OrganizationUser orgUser, + SutProvider sutProvider) + { + orgUser.Status = OrganizationUserStatusType.Confirmed; + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.ResendSponsorshipOfferAsync(org, orgUser, null, "test@bitwarden.com")); + + Assert.Contains("Cannot find an outstanding sponsorship offer for this organization.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendFamiliesForEnterpriseOfferEmailAsync(default, default, default, default); + } + + [Theory] + [BitAutoData] + public async Task ResendSponsorshipOffer_NoOfferToEmail_ThrowsBadRequest(Organization org, + OrganizationUser orgUser, OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + orgUser.Status = OrganizationUserStatusType.Confirmed; + sponsorship.OfferedToEmail = null; + + sutProvider.GetDependency().GetBySponsoringOrganizationUserIdAsync(orgUser.Id) + .Returns(sponsorship); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.ResendSponsorshipOfferAsync(org, orgUser, sponsorship, "test@bitwarden.com")); + + Assert.Contains("Cannot find an outstanding sponsorship offer for this organization.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendFamiliesForEnterpriseOfferEmailAsync(default, default, default, default); + } + + + [Theory] + [BitAutoData] + public async Task SendSponsorshipOfferAsync(OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + const string email = "test@bitwarden.com"; + + sutProvider.GetDependency() + .GetByEmailAsync(sponsorship.OfferedToEmail) + .Returns(Task.FromResult(new User())); + + await sutProvider.Sut.SendSponsorshipOfferAsync(sponsorship, email); + + await sutProvider.GetDependency().Received(1) + .SendFamiliesForEnterpriseOfferEmailAsync(sponsorship.OfferedToEmail, email, true, Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task SetUpSponsorship_SponsorshipNotFound_ThrowsBadRequest(Organization org, + SutProvider sutProvider) + { + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.SetUpSponsorshipAsync(null, org)); + + Assert.Contains("No unredeemed sponsorship offer exists for you.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SponsorOrganizationAsync(default, default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertAsync(default); + } + + [Theory] + [BitAutoData] + public async Task SetUpSponsorship_OrgAlreadySponsored_ThrowsBadRequest(Organization org, + OrganizationSponsorship sponsorship, OrganizationSponsorship existingSponsorship, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(org.Id).Returns(existingSponsorship); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.SetUpSponsorshipAsync(sponsorship, org)); + + Assert.Contains("Cannot redeem a sponsorship offer for an organization that is already sponsored. Revoke existing sponsorship first.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SponsorOrganizationAsync(default, default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertAsync(default); + } + + [Theory] + [BitMemberAutoData(nameof(NonFamiliesPlanTypes))] + public async Task SetUpSponsorship_OrgNotFamiles_ThrowsBadRequest(PlanType planType, + OrganizationSponsorship sponsorship, Organization org, + SutProvider sutProvider) + { + org.PlanType = planType; + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.SetUpSponsorshipAsync(sponsorship, org)); + + Assert.Contains("Can only redeem sponsorship offer on families organizations.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SponsorOrganizationAsync(default, default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertAsync(default); + } + + private async Task AssertRemovedSponsoredPaymentAsync(Organization sponsoredOrg, + OrganizationSponsorship sponsorship, SutProvider sutProvider) + { + await sutProvider.GetDependency().Received(1) + .RemoveOrganizationSponsorshipAsync(sponsoredOrg, sponsorship); + await sutProvider.GetDependency().Received(1).UpsertAsync(sponsoredOrg); + await sutProvider.GetDependency().Received(1) + .SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(sponsoredOrg.BillingEmailAddress(), sponsoredOrg.Name); + } + + private async Task AssertRemovedSponsorshipAsync(OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + if (sponsorship.CloudSponsor || sponsorship.SponsorshipLapsedDate.HasValue) + { + await sutProvider.GetDependency().Received(1) + .DeleteAsync(sponsorship); + } + else + { + await sutProvider.GetDependency().Received(1) + .UpsertAsync(sponsorship); + } + } + + private static async Task AssertDidNotRemoveSponsoredPaymentAsync(SutProvider sutProvider) + { + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .RemoveOrganizationSponsorshipAsync(default, default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .UpsertAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(default, default); + } + + private static async Task AssertDidNotRemoveSponsorshipAsync(SutProvider sutProvider) + { + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .DeleteAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .UpsertAsync(default); + } + + [Theory] + [BitAutoData] + public async Task ValidateSponsorshipAsync_NoSponsoredOrg_EarlyReturn(Guid sponsoredOrgId, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetByIdAsync(sponsoredOrgId).Returns((Organization)null); + + var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrgId); + + Assert.False(result); + await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); + await AssertDidNotRemoveSponsorshipAsync(sutProvider); + } + + [Theory] + [BitAutoData] + public async Task ValidateSponsorshipAsync_NoExistingSponsorship_UpdatesStripePlan(Organization sponsoredOrg, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); + + var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); + + Assert.False(result); + await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, null, sutProvider); + } + + [Theory] + [BitAutoData] + public async Task ValidateSponsorshipAsync_SponsoringOrgNull_UpdatesStripePlan(Organization sponsoredOrg, + OrganizationSponsorship existingSponsorship, SutProvider sutProvider) + { + existingSponsorship.SponsoringOrganizationId = null; + + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship); + sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); + + var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); + + Assert.False(result); + await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider); + await AssertRemovedSponsorshipAsync(existingSponsorship, sutProvider); + } + + [Theory] + [BitAutoData] + public async Task ValidateSponsorshipAsync_SponsoringOrgUserNull_UpdatesStripePlan(Organization sponsoredOrg, + OrganizationSponsorship existingSponsorship, SutProvider sutProvider) + { + existingSponsorship.SponsoringOrganizationUserId = null; + + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship); + sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); + + var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); + + Assert.False(result); + await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider); + await AssertRemovedSponsorshipAsync(existingSponsorship, sutProvider); + } + + [Theory] + [BitAutoData] + public async Task ValidateSponsorshipAsync_SponsorshipTypeNull_UpdatesStripePlan(Organization sponsoredOrg, + OrganizationSponsorship existingSponsorship, SutProvider sutProvider) + { + existingSponsorship.PlanSponsorshipType = null; + + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship); + sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); + + var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); + + Assert.False(result); + await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider); + await AssertRemovedSponsorshipAsync(existingSponsorship, sutProvider); + } + + [Theory] + [BitAutoData] + public async Task ValidateSponsorshipAsync_SponsoringOrgNotFound_UpdatesStripePlan(Organization sponsoredOrg, + OrganizationSponsorship existingSponsorship, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship); + sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); + + var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); + + Assert.False(result); + await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider); + await AssertRemovedSponsorshipAsync(existingSponsorship, sutProvider); + } + + [Theory] + [BitMemberAutoData(nameof(NonEnterprisePlanTypes))] + public async Task ValidateSponsorshipAsync_SponsoringOrgNotEnterprise_UpdatesStripePlan(PlanType planType, + Organization sponsoredOrg, OrganizationSponsorship existingSponsorship, Organization sponsoringOrg, + SutProvider sutProvider) + { + sponsoringOrg.PlanType = planType; + existingSponsorship.SponsoringOrganizationId = sponsoringOrg.Id; + + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship); + sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); + sutProvider.GetDependency().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg); + + var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); + + Assert.False(result); + await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider); + await AssertRemovedSponsorshipAsync(existingSponsorship, sutProvider); + } + + [Theory] + [BitMemberAutoData(nameof(EnterprisePlanTypes))] + public async Task ValidateSponsorshipAsync_SponsoringOrgDisabled_UpdatesStripePlan(PlanType planType, + Organization sponsoredOrg, OrganizationSponsorship existingSponsorship, Organization sponsoringOrg, + SutProvider sutProvider) + { + sponsoringOrg.PlanType = planType; + sponsoringOrg.Enabled = false; + existingSponsorship.SponsoringOrganizationId = sponsoringOrg.Id; + + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship); + sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); + sutProvider.GetDependency().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg); + + var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); + + Assert.False(result); + await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider); + await AssertRemovedSponsorshipAsync(existingSponsorship, sutProvider); + } + + [Theory] + [BitMemberAutoData(nameof(EnterprisePlanTypes))] + public async Task ValidateSponsorshipAsync_Valid(PlanType planType, + Organization sponsoredOrg, OrganizationSponsorship existingSponsorship, Organization sponsoringOrg, + SutProvider sutProvider) + { + sponsoringOrg.PlanType = planType; + sponsoringOrg.Enabled = true; + existingSponsorship.SponsoringOrganizationId = sponsoringOrg.Id; + + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship); + sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); + sutProvider.GetDependency().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg); + + var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); + + Assert.True(result); + + await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); + await AssertDidNotRemoveSponsorshipAsync(sutProvider); + } + + [Theory] + [BitAutoData] + public async Task RevokeSponsorship_NoExistingSponsorship_ThrowsBadRequest(Organization org, + SutProvider sutProvider) + { + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RevokeSponsorshipAsync(org, null)); + + Assert.Contains("You are not currently sponsoring an organization.", exception.Message); + await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); + await AssertDidNotRemoveSponsorshipAsync(sutProvider); + } + + [Theory] + [BitAutoData] + public async Task RevokeSponsorship_SponsorshipNotRedeemed_DeletesSponsorship(Organization org, + OrganizationSponsorship sponsorship, SutProvider sutProvider) + { + sponsorship.SponsoredOrganizationId = null; + + await sutProvider.Sut.RevokeSponsorshipAsync(org, sponsorship); + + await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); + await AssertRemovedSponsorshipAsync(sponsorship, sutProvider); + } + + [Theory] + [BitAutoData] + public async Task RevokeSponsorship_SponsoredOrgNotFound_ThrowsBadRequest(OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RevokeSponsorshipAsync(null, sponsorship)); + + Assert.Contains("Unable to find the sponsored Organization.", exception.Message); + + await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); + await AssertDidNotRemoveSponsorshipAsync(sutProvider); + } + + [Theory] + [BitAutoData] + public async Task RemoveSponsorship_SponsoredOrgNull_ThrowsBadRequest(OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + sponsorship.SponsoredOrganizationId = null; + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RemoveSponsorshipAsync(null, sponsorship)); + + Assert.Contains("The requested organization is not currently being sponsored.", exception.Message); + + await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); + await AssertDidNotRemoveSponsorshipAsync(sutProvider); + } + + [Theory] + [BitAutoData] + public async Task RemoveSponsorship_SponsorshipNotFound_ThrowsBadRequest(Organization sponsoredOrg, + SutProvider sutProvider) + { + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RemoveSponsorshipAsync(sponsoredOrg, null)); + + Assert.Contains("The requested organization is not currently being sponsored.", exception.Message); + + await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); + await AssertDidNotRemoveSponsorshipAsync(sutProvider); + } + + [Theory] + [BitAutoData] + public async Task RemoveSponsorship_SponsoredOrgNotFound_ThrowsBadRequest(OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RemoveSponsorshipAsync(null, sponsorship)); + + Assert.Contains("Unable to find the sponsored Organization.", exception.Message); + + await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); + await AssertDidNotRemoveSponsorshipAsync(sutProvider); + } + + [Theory] + [BitAutoData] + public async Task DoRemoveSponsorshipAsync_NullDoNothing(SutProvider sutProvider) + { + await sutProvider.Sut.DoRemoveSponsorshipAsync(null, null); + + await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); + await AssertDidNotRemoveSponsorshipAsync(sutProvider); + } + + [Theory] + [BitAutoData] + public async Task DoRemoveSponsorshipAsync_NullSponsoredOrg(OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + await sutProvider.Sut.DoRemoveSponsorshipAsync(null, sponsorship); + + await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); + await AssertRemovedSponsorshipAsync(sponsorship, sutProvider); + } + + [Theory] + [BitAutoData] + public async Task DoRemoveSponsorshipAsync_NullSponsorship(Organization sponsoredOrg, + SutProvider sutProvider) + { + await sutProvider.Sut.DoRemoveSponsorshipAsync(sponsoredOrg, null); + + await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, null, sutProvider); + await AssertDidNotRemoveSponsorshipAsync(sutProvider); + } + + [Theory] + [BitAutoData] + public async Task DoRemoveSponsorshipAsync_RemoveBoth(Organization sponsoredOrg, + OrganizationSponsorship sponsorship, SutProvider sutProvider) + { + await sutProvider.Sut.DoRemoveSponsorshipAsync(sponsoredOrg, sponsorship); + + await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, sponsorship, sutProvider); + await AssertRemovedSponsorshipAsync(sponsorship, sutProvider); + } + } +} diff --git a/test/Core.Test/Services/PolicyServiceTests.cs b/test/Core.Test/Services/PolicyServiceTests.cs index 00ad9dcae..ac83d0ff7 100644 --- a/test/Core.Test/Services/PolicyServiceTests.cs +++ b/test/Core.Test/Services/PolicyServiceTests.cs @@ -6,19 +6,19 @@ using Bit.Core.Models.Data; using Bit.Core.Models.Table; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Test.AutoFixture; -using Bit.Core.Test.AutoFixture.Attributes; -using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; using PolicyFixtures = Bit.Core.Test.AutoFixture.PolicyFixtures; using NSubstitute; using Xunit; +using Bit.Core.Enums; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.AutoFixture; namespace Bit.Core.Test.Services { public class PolicyServiceTests { [Theory, CustomAutoData(typeof(SutProviderCustomization))] - public async Task SaveAsync_OrganizationDoesNotExist_ThrowsBadRequest([PolicyFixtures.Policy(Enums.PolicyType.DisableSend)] Core.Models.Table.Policy policy, SutProvider sutProvider) + public async Task SaveAsync_OrganizationDoesNotExist_ThrowsBadRequest([PolicyFixtures.Policy(PolicyType.DisableSend)] Core.Models.Table.Policy policy, SutProvider sutProvider) { SetupOrg(sutProvider, policy.OrganizationId, null); @@ -40,7 +40,7 @@ namespace Bit.Core.Test.Services } [Theory, CustomAutoData(typeof(SutProviderCustomization))] - public async Task SaveAsync_OrganizationCannotUsePolicies_ThrowsBadRequest([PolicyFixtures.Policy(Enums.PolicyType.DisableSend)] Core.Models.Table.Policy policy, SutProvider sutProvider) + public async Task SaveAsync_OrganizationCannotUsePolicies_ThrowsBadRequest([PolicyFixtures.Policy(PolicyType.DisableSend)] Core.Models.Table.Policy policy, SutProvider sutProvider) { var orgId = Guid.NewGuid(); @@ -67,7 +67,7 @@ namespace Bit.Core.Test.Services } [Theory, CustomAutoData(typeof(SutProviderCustomization))] - public async Task SaveAsync_SingleOrg_RequireSsoEnabled_ThrowsBadRequest([PolicyFixtures.Policy(Enums.PolicyType.SingleOrg)] Core.Models.Table.Policy policy, SutProvider sutProvider) + public async Task SaveAsync_SingleOrg_RequireSsoEnabled_ThrowsBadRequest([PolicyFixtures.Policy(PolicyType.SingleOrg)] Core.Models.Table.Policy policy, SutProvider sutProvider) { policy.Enabled = false; @@ -78,7 +78,7 @@ namespace Bit.Core.Test.Services }); sutProvider.GetDependency() - .GetByOrganizationIdTypeAsync(policy.OrganizationId, Enums.PolicyType.RequireSso) + .GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.RequireSso) .Returns(Task.FromResult(new Core.Models.Table.Policy { Enabled = true })); var badRequestException = await Assert.ThrowsAsync( @@ -176,7 +176,7 @@ namespace Bit.Core.Test.Services }); sutProvider.GetDependency() - .GetByOrganizationIdTypeAsync(policy.OrganizationId, Enums.PolicyType.SingleOrg) + .GetByOrganizationIdTypeAsync(policy.OrganizationId, PolicyType.SingleOrg) .Returns(Task.FromResult(new Core.Models.Table.Policy { Enabled = false })); var badRequestException = await Assert.ThrowsAsync( @@ -197,7 +197,7 @@ namespace Bit.Core.Test.Services } [Theory, CustomAutoData(typeof(SutProviderCustomization))] - public async Task SaveAsync_NewPolicy_Created([PolicyFixtures.Policy(Enums.PolicyType.MasterPassword)] Core.Models.Table.Policy policy, SutProvider sutProvider) + public async Task SaveAsync_NewPolicy_Created([PolicyFixtures.Policy(PolicyType.MasterPassword)] Core.Models.Table.Policy policy, SutProvider sutProvider) { policy.Id = default; @@ -212,7 +212,7 @@ namespace Bit.Core.Test.Services await sutProvider.Sut.SaveAsync(policy, Substitute.For(), Substitute.For(), Guid.NewGuid()); await sutProvider.GetDependency().Received() - .LogPolicyEventAsync(policy, Enums.EventType.Policy_Updated); + .LogPolicyEventAsync(policy, EventType.Policy_Updated); await sutProvider.GetDependency().Received() .UpsertAsync(policy); @@ -254,7 +254,7 @@ namespace Bit.Core.Test.Services } [Theory, CustomAutoData(typeof(SutProviderCustomization))] - public async Task SaveAsync_ExistingPolicy_UpdateTwoFactor([PolicyFixtures.Policy(Enums.PolicyType.TwoFactorAuthentication)] Core.Models.Table.Policy policy, SutProvider sutProvider) + public async Task SaveAsync_ExistingPolicy_UpdateTwoFactor([PolicyFixtures.Policy(PolicyType.TwoFactorAuthentication)] Core.Models.Table.Policy policy, SutProvider sutProvider) { // If the policy that this is updating isn't enabled then do some work now that the current one is enabled @@ -272,15 +272,15 @@ namespace Bit.Core.Test.Services .Returns(new Core.Models.Table.Policy { Id = policy.Id, - Type = Enums.PolicyType.TwoFactorAuthentication, + Type = PolicyType.TwoFactorAuthentication, Enabled = false, }); var orgUserDetail = new Core.Models.Data.OrganizationUserUserDetails { Id = Guid.NewGuid(), - Status = Enums.OrganizationUserStatusType.Accepted, - Type = Enums.OrganizationUserType.User, + Status = OrganizationUserStatusType.Accepted, + Type = OrganizationUserType.User, // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync Email = "test@bitwarden.com", Name = "TEST", @@ -313,7 +313,7 @@ namespace Bit.Core.Test.Services .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(org.Name, orgUserDetail.Email); await sutProvider.GetDependency().Received() - .LogPolicyEventAsync(policy, Enums.EventType.Policy_Updated); + .LogPolicyEventAsync(policy, EventType.Policy_Updated); await sutProvider.GetDependency().Received() .UpsertAsync(policy); @@ -323,7 +323,7 @@ namespace Bit.Core.Test.Services } [Theory, CustomAutoData(typeof(SutProviderCustomization))] - public async Task SaveAsync_ExistingPolicy_UpdateSingleOrg([PolicyFixtures.Policy(Enums.PolicyType.TwoFactorAuthentication)] Core.Models.Table.Policy policy, SutProvider sutProvider) + public async Task SaveAsync_ExistingPolicy_UpdateSingleOrg([PolicyFixtures.Policy(PolicyType.TwoFactorAuthentication)] Core.Models.Table.Policy policy, SutProvider sutProvider) { // If the policy that this is updating isn't enabled then do some work now that the current one is enabled @@ -341,15 +341,15 @@ namespace Bit.Core.Test.Services .Returns(new Core.Models.Table.Policy { Id = policy.Id, - Type = Enums.PolicyType.SingleOrg, + Type = PolicyType.SingleOrg, Enabled = false, }); var orgUserDetail = new Core.Models.Data.OrganizationUserUserDetails { Id = Guid.NewGuid(), - Status = Enums.OrganizationUserStatusType.Accepted, - Type = Enums.OrganizationUserType.User, + Status = OrganizationUserStatusType.Accepted, + Type = OrganizationUserType.User, // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync Email = "test@bitwarden.com", Name = "TEST", @@ -376,7 +376,7 @@ namespace Bit.Core.Test.Services await sutProvider.Sut.SaveAsync(policy, userService, organizationService, savingUserId); await sutProvider.GetDependency().Received() - .LogPolicyEventAsync(policy, Enums.EventType.Policy_Updated); + .LogPolicyEventAsync(policy, EventType.Policy_Updated); await sutProvider.GetDependency().Received() .UpsertAsync(policy); diff --git a/test/Core.Test/Services/SendServiceTests.cs b/test/Core.Test/Services/SendServiceTests.cs index b18733edc..7c08f6d00 100644 --- a/test/Core.Test/Services/SendServiceTests.cs +++ b/test/Core.Test/Services/SendServiceTests.cs @@ -1,21 +1,19 @@ using System; using System.Collections.Generic; -using System.IO; -using System.Text; -using System.Text.Json; -using System.Linq; -using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data; using Bit.Core.Models.Table; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Test.AutoFixture; using Bit.Core.Test.AutoFixture.SendFixtures; using Microsoft.AspNetCore.Identity; using NSubstitute; using Xunit; +using System.Text.Json; +using Bit.Test.Common.AutoFixture; +using System.IO; +using System.Text; namespace Bit.Core.Test.Services { diff --git a/test/Core.Test/Services/SsoConfigServiceTests.cs b/test/Core.Test/Services/SsoConfigServiceTests.cs index ecb732e94..77eeb5675 100644 --- a/test/Core.Test/Services/SsoConfigServiceTests.cs +++ b/test/Core.Test/Services/SsoConfigServiceTests.cs @@ -5,8 +5,8 @@ using Bit.Core.Models.Data; using Bit.Core.Models.Table; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Test.AutoFixture; -using Bit.Core.Test.AutoFixture.Attributes; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; diff --git a/test/Core.Test/Utilities/CoreHelpersTests.cs b/test/Core.Test/Utilities/CoreHelpersTests.cs index cf43aa13b..2d0a13d46 100644 --- a/test/Core.Test/Utilities/CoreHelpersTests.cs +++ b/test/Core.Test/Utilities/CoreHelpersTests.cs @@ -3,15 +3,15 @@ using System.Collections.Generic; using System.Linq; using Bit.Core.Utilities; using Xunit; -using Bit.Core.Test.AutoFixture.Attributes; using Bit.Core.Test.AutoFixture.UserFixtures; using IdentityModel; using Bit.Core.Enums.Provider; using Bit.Core.Models.Table; using Bit.Core.Context; using AutoFixture; -using Bit.Core.Test.AutoFixture; using Bit.Core.Enums; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.AutoFixture; namespace Bit.Core.Test.Utilities { @@ -351,5 +351,16 @@ namespace Bit.Core.Test.Utilities } } + [Theory] + [InlineData("hi@email.com", "hi@email.com")] // Short email with no room to obfuscate + [InlineData("name@email.com", "na**@email.com")] // Can obfuscate + [InlineData("reallylongnamethatnooneshouldhave@email", "re*******************************@email")] // Really long email and no .com, .net, etc + [InlineData("name@", "name@")] // @ symbol but no domain + [InlineData("", "")] // Empty string + [InlineData(null, null)] // null + public void ObfuscateEmail_Success(string input, string expected) + { + Assert.Equal(expected, CoreHelpers.ObfuscateEmail(input)); + } } } diff --git a/test/bitwarden.tests.sln b/test/bitwarden.tests.sln index 8968eea3d..cd5f7820f 100644 --- a/test/bitwarden.tests.sln +++ b/test/bitwarden.tests.sln @@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Icons.Test", "Icons.Test\Ic EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Api.Test", "Api.Test\Api.Test.csproj", "{2B29139A-E3B5-4A44-8A85-1593ACB797CC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "Common\Common.csproj", "{E94B2922-EE05-435C-9472-FDEFEAD0AA37}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -58,5 +60,17 @@ Global {2B29139A-E3B5-4A44-8A85-1593ACB797CC}.Release|x64.Build.0 = Release|Any CPU {2B29139A-E3B5-4A44-8A85-1593ACB797CC}.Release|x86.ActiveCfg = Release|Any CPU {2B29139A-E3B5-4A44-8A85-1593ACB797CC}.Release|x86.Build.0 = Release|Any CPU + {E94B2922-EE05-435C-9472-FDEFEAD0AA37}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E94B2922-EE05-435C-9472-FDEFEAD0AA37}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E94B2922-EE05-435C-9472-FDEFEAD0AA37}.Debug|x64.ActiveCfg = Debug|Any CPU + {E94B2922-EE05-435C-9472-FDEFEAD0AA37}.Debug|x64.Build.0 = Debug|Any CPU + {E94B2922-EE05-435C-9472-FDEFEAD0AA37}.Debug|x86.ActiveCfg = Debug|Any CPU + {E94B2922-EE05-435C-9472-FDEFEAD0AA37}.Debug|x86.Build.0 = Debug|Any CPU + {E94B2922-EE05-435C-9472-FDEFEAD0AA37}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E94B2922-EE05-435C-9472-FDEFEAD0AA37}.Release|Any CPU.Build.0 = Release|Any CPU + {E94B2922-EE05-435C-9472-FDEFEAD0AA37}.Release|x64.ActiveCfg = Release|Any CPU + {E94B2922-EE05-435C-9472-FDEFEAD0AA37}.Release|x64.Build.0 = Release|Any CPU + {E94B2922-EE05-435C-9472-FDEFEAD0AA37}.Release|x86.ActiveCfg = Release|Any CPU + {E94B2922-EE05-435C-9472-FDEFEAD0AA37}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql b/util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql new file mode 100644 index 000000000..9cb6ff1ae --- /dev/null +++ b/util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql @@ -0,0 +1,663 @@ +-- Create Organization Sponsorships table +IF OBJECT_ID('[dbo].[OrganizationSponsorship]') IS NULL +BEGIN +CREATE TABLE [dbo].[OrganizationSponsorship] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [InstallationId] UNIQUEIDENTIFIER NULL, + [SponsoringOrganizationId] UNIQUEIDENTIFIER NULL, + [SponsoringOrganizationUserID] UNIQUEIDENTIFIER NULL, + [SponsoredOrganizationId] UNIQUEIDENTIFIER NULL, + [FriendlyName] NVARCHAR(256) NULL, + [OfferedToEmail] NVARCHAR (256) NULL, + [PlanSponsorshipType] TINYINT NULL, + [CloudSponsor] BIT NULL, + [LastSyncDate] DATETIME2 (7) NULL, + [TimesRenewedWithoutValidation] TINYINT DEFAULT 0, + [SponsorshipLapsedDate] DATETIME2 (7) NULL, + CONSTRAINT [PK_OrganizationSponsorship] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_OrganizationSponsorship_InstallationId] FOREIGN KEY ([InstallationId]) REFERENCES [dbo].[Installation] ([Id]), + CONSTRAINT [FK_OrganizationSponsorship_SponsoringOrg] FOREIGN KEY ([SponsoringOrganizationId]) REFERENCES [dbo].[Organization] ([Id]), + CONSTRAINT [FK_OrganizationSponsorship_SponsoredOrg] FOREIGN KEY ([SponsoredOrganizationId]) REFERENCES [dbo].[Organization] ([Id]), +); +END +GO + + +-- Create indexes +IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationSponsorship_InstallationId') +BEGIN +CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_InstallationId] + ON [dbo].[OrganizationSponsorship]([InstallationId] ASC) + WHERE [InstallationId] IS NOT NULL; +END +GO + +IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationSponsorship_SponsoringOrganizationId') +BEGIN +CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationId] + ON [dbo].[OrganizationSponsorship]([SponsoringOrganizationId] ASC) + WHERE [SponsoringOrganizationId] IS NOT NULL; +END +GO + +IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationSponsorship_SponsoringOrganizationUserId') +BEGIN +CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationUserId] + ON [dbo].[OrganizationSponsorship]([SponsoringOrganizationUserID] ASC) + WHERE [SponsoringOrganizationUserID] IS NOT NULL; +END +GO + +IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationSponsorship_OfferedToEmail') +BEGIN +CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_OfferedToEmail] + ON [dbo].[OrganizationSponsorship]([OfferedToEmail] ASC) + WHERE [OfferedToEmail] IS NOT NULL; +END +GO + +IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationSponsorship_SponsoredOrganizationID') +BEGIN +CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoredOrganizationID] + ON [dbo].[OrganizationSponsorship]([SponsoredOrganizationId] ASC) + WHERE [SponsoredOrganizationId] IS NOT NULL; +END +GO + + +-- Create View +IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'OrganizationSponsorshipView') +BEGIN + DROP VIEW [dbo].[OrganizationSponsorshipView]; +END +GO + +CREATE VIEW [dbo].[OrganizationSponsorshipView] +AS +SELECT + * +FROM + [dbo].[OrganizationSponsorship] +GO + + +-- OrganizationSponsorship_ReadById +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadById]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationSponsorship_ReadById] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationSponsorship_ReadById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationSponsorshipView] + WHERE + [Id] = @Id +END +GO + + +-- OrganizationSponsorship_Create +IF OBJECT_ID('[dbo].[OrganizationSponsorship_Create]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationSponsorship_Create] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationSponsorship_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @InstallationId UNIQUEIDENTIFIER, + @SponsoringOrganizationId UNIQUEIDENTIFIER, + @SponsoringOrganizationUserID UNIQUEIDENTIFIER, + @SponsoredOrganizationId UNIQUEIDENTIFIER, + @FriendlyName NVARCHAR(256), + @OfferedToEmail NVARCHAR(256), + @PlanSponsorshipType TINYINT, + @CloudSponsor BIT, + @LastSyncDate DATETIME2 (7), + @TimesRenewedWithoutValidation TINYINT, + @SponsorshipLapsedDate DATETIME2 (7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[OrganizationSponsorship] + ( + [Id], + [InstallationId], + [SponsoringOrganizationId], + [SponsoringOrganizationUserID], + [SponsoredOrganizationId], + [FriendlyName], + [OfferedToEmail], + [PlanSponsorshipType], + [CloudSponsor], + [LastSyncDate], + [TimesRenewedWithoutValidation], + [SponsorshipLapsedDate] + ) + VALUES + ( + @Id, + @InstallationId, + @SponsoringOrganizationId, + @SponsoringOrganizationUserID, + @SponsoredOrganizationId, + @FriendlyName, + @OfferedToEmail, + @PlanSponsorshipType, + @CloudSponsor, + @LastSyncDate, + @TimesRenewedWithoutValidation, + @SponsorshipLapsedDate + ) +END +GO + +-- OrganizationSponsorship_Update +IF OBJECT_ID('[dbo].[OrganizationSponsorship_Update]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationSponsorship_Update] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationSponsorship_Update] + @Id UNIQUEIDENTIFIER, + @InstallationId UNIQUEIDENTIFIER, + @SponsoringOrganizationId UNIQUEIDENTIFIER, + @SponsoringOrganizationUserID UNIQUEIDENTIFIER, + @SponsoredOrganizationId UNIQUEIDENTIFIER, + @FriendlyName NVARCHAR(256), + @OfferedToEmail NVARCHAR(256), + @PlanSponsorshipType TINYINT, + @CloudSponsor BIT, + @LastSyncDate DATETIME2 (7), + @TimesRenewedWithoutValidation TINYINT, + @SponsorshipLapsedDate DATETIME2 (7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[OrganizationSponsorship] + SET + [InstallationId] = @InstallationId, + [SponsoringOrganizationId] = @SponsoringOrganizationId, + [SponsoringOrganizationUserID] = @SponsoringOrganizationUserID, + [SponsoredOrganizationId] = @SponsoredOrganizationId, + [FriendlyName] = @FriendlyName, + [OfferedToEmail] = @OfferedToEmail, + [PlanSponsorshipType] = @PlanSponsorshipType, + [CloudSponsor] = @CloudSponsor, + [LastSyncDate] = @LastSyncDate, + [TimesRenewedWithoutValidation] = @TimesRenewedWithoutValidation, + [SponsorshipLapsedDate] = @SponsorshipLapsedDate + WHERE + [Id] = @Id +END +GO + + +-- OrganizationSponsorship_DeleteById +IF OBJECT_ID('[dbo].[OrganizationSponsorship_DeleteById]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationSponsorship_DeleteById] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationSponsorship_DeleteById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + BEGIN TRANSACTION OrgSponsorship_DeleteById + + DELETE + FROM + [dbo].[OrganizationSponsorship] + WHERE + [Id] = @Id + + COMMIT TRANSACTION OrgSponsorship_DeleteById +END +GO + + +-- OrganizationSponsorship_ReadBySponsoringOrganizationUserId +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationUserId]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationUserId] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationUserId] + @SponsoringOrganizationUserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationSponsorshipView] + WHERE + [SponsoringOrganizationUserId] = @SponsoringOrganizationUserId +END +GO + + + +-- OrganizationSponsorship_ReadBySponsoredOrganizationId +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadBySponsoredOrganizationId]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationSponsorship_ReadBySponsoredOrganizationId] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationSponsorship_ReadBySponsoredOrganizationId] + @SponsoredOrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationSponsorshipView] + WHERE + [SponsoredOrganizationId] = @SponsoredOrganizationId +END +GO + +-- OrganizationSponsorship_ReadByOfferedToEmail +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadByOfferedToEmail]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationSponsorship_ReadByOfferedToEmail] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationSponsorship_ReadByOfferedToEmail] + @OfferedToEmail NVARCHAR (256) -- Should not be null +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationSponsorshipView] + WHERE + [OfferedToEmail] = @OfferedToEmail +END +GO + +-- OrganizationSponsorship_OrganizationDeleted +IF OBJECT_ID('[dbo].[OrganizationSponsorship_OrganizationDeleted]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationSponsorship_OrganizationDeleted] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationSponsorship_OrganizationDeleted] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[OrganizationSponsorship] + SET + [SponsoringOrganizationId] = NULL + WHERE + [SponsoringOrganizationId] = @OrganizationId AND + [CloudSponsor] = 0 + + UPDATE + [dbo].[OrganizationSponsorship] + SET + [SponsoredOrganizationId] = NULL + WHERE + [SponsoredOrganizationId] = @OrganizationId AND + [CloudSponsor] = 0 + + DELETE + FROM + [dbo].[OrganizationSponsorship] + WHERE + [CloudSponsor] = 1 AND + ([SponsoredOrganizationId] = @OrganizationId OR + [SponsoringOrganizationId] = @OrganizationId) +END +GO + +-- OrganizationSponsorship_OrganizationUserDeleted +IF OBJECT_ID('[dbo].[OrganizationSponsorship_OrganizationUserDeleted]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationSponsorship_OrganizationUserDeleted] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationSponsorship_OrganizationUserDeleted] + @OrganizationUserId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DELETE + FROM + [dbo].[OrganizationSponsorship] + WHERE + [SponsoringOrganizationUserId] = @OrganizationUserId +END +GO + +-- OrganizationSponsorship_OrganizationUsersDeleted +IF OBJECT_ID('[dbo].[OrganizationSponsorship_OrganizationUsersDeleted]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationSponsorship_OrganizationUsersDeleted] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationSponsorship_OrganizationUsersDeleted] + @SponsoringOrganizationUserIds [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + DECLARE @BatchSize INT = 100 + + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION OS_DeleteMany_OUs + + DELETE TOP(@BatchSize) OS + FROM + [dbo].[OrganizationSponsorship] OS + INNER JOIN + @SponsoringOrganizationUserIds I ON I.Id = OS.SponsoringOrganizationUserId + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION OS_DeleteMany_OUs + END +END +GO + +-- Update Organization delete sprocs to handle organization sponsorships +IF OBJECT_ID('[dbo].[Organization_DeleteById]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Organization_DeleteById] +END +GO + +CREATE PROCEDURE [dbo].[Organization_DeleteById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @Id + + DECLARE @BatchSize INT = 100 + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION Organization_DeleteById_Ciphers + + DELETE TOP(@BatchSize) + FROM + [dbo].[Cipher] + WHERE + [UserId] IS NULL + AND [OrganizationId] = @Id + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION Organization_DeleteById_Ciphers + END + + BEGIN TRANSACTION Organization_DeleteById + + DELETE + FROM + [dbo].[SsoUser] + WHERE + [OrganizationId] = @Id + + DELETE + FROM + [dbo].[SsoConfig] + WHERE + [OrganizationId] = @Id + + DELETE CU + FROM + [dbo].[CollectionUser] CU + INNER JOIN + [dbo].[OrganizationUser] OU ON [CU].[OrganizationUserId] = [OU].[Id] + WHERE + [OU].[OrganizationId] = @Id + + DELETE + FROM + [dbo].[OrganizationUser] + WHERE + [OrganizationId] = @Id + + DELETE + FROM + [dbo].[ProviderOrganization] + WHERE + [OrganizationId] = @Id + + EXEC[dbo].[OrganizationSponsorship_OrganizationDeleted] @Id + + DELETE + FROM + [dbo].[Organization] + WHERE + [Id] = @Id + + COMMIT TRANSACTION Organization_DeleteById +END +GO + +-- Update Organization User delete sprocs to handle organization sponsorships +IF OBJECT_ID('[dbo].[OrganizationUser_DeleteById]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationUser_DeleteById] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationUser_DeleteById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserId] @Id + + DECLARE @OrganizationId UNIQUEIDENTIFIER + DECLARE @UserId UNIQUEIDENTIFIER + + SELECT + @OrganizationId = [OrganizationId], + @UserId = [UserId] + FROM + [dbo].[OrganizationUser] + WHERE + [Id] = @Id + + IF @OrganizationId IS NOT NULL AND @UserId IS NOT NULL + BEGIN + EXEC [dbo].[SsoUser_Delete] @UserId, @OrganizationId + END + + DELETE + FROM + [dbo].[CollectionUser] + WHERE + [OrganizationUserId] = @Id + + DELETE + FROM + [dbo].[GroupUser] + WHERE + [OrganizationUserId] = @Id + + EXEC [dbo].[OrganizationSponsorship_OrganizationUserDeleted] @Id + + DELETE + FROM + [dbo].[OrganizationUser] + WHERE + [Id] = @Id +END +GO + + +IF OBJECT_ID('[dbo].[OrganizationUser_DeleteByIds]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationUser_DeleteByIds] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationUser_DeleteByIds] + @Ids [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationUserIds] @Ids + + DECLARE @UserAndOrganizationIds [dbo].[TwoGuidIdArray] + + INSERT INTO @UserAndOrganizationIds + (Id1, Id2) + SELECT + UserId, + OrganizationId + FROM + [dbo].[OrganizationUser] OU + INNER JOIN + @Ids OUIds ON OUIds.Id = OU.Id + WHERE + UserId IS NOT NULL AND + OrganizationId IS NOT NULL + + BEGIN + EXEC [dbo].[SsoUser_DeleteMany] @UserAndOrganizationIds + END + + DECLARE @BatchSize INT = 100 + + -- Delete CollectionUsers + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION CollectionUser_DeleteMany_CUs + + DELETE TOP(@BatchSize) CU + FROM + [dbo].[CollectionUser] CU + INNER JOIN + @Ids I ON I.Id = CU.OrganizationUserId + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION CollectionUser_DeleteMany_CUs + END + + SET @BatchSize = 100; + + -- Delete GroupUsers + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION GroupUser_DeleteMany_GroupUsers + + DELETE TOP(@BatchSize) GU + FROM + [dbo].[GroupUser] GU + INNER JOIN + @Ids I ON I.Id = GU.OrganizationUserId + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION GoupUser_DeleteMany_GroupUsers + END + + EXEC [dbo].[OrganizationSponsorship_OrganizationUsersDeleted] @Ids + + SET @BatchSize = 100; + + -- Delete OrganizationUsers + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION OrganizationUser_DeleteMany_OUs + + DELETE TOP(@BatchSize) OU + FROM + [dbo].[OrganizationUser] OU + INNER JOIN + @Ids I ON I.Id = OU.Id + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION OrganizationUser_DeleteMany_OUs + END +END +GO + +-- OrganizationUserOrganizationDetailsView update +ALTER VIEW [dbo].[OrganizationUserOrganizationDetailsView] +AS +SELECT + OU.[UserId], + OU.[OrganizationId], + O.[Name], + O.[Enabled], + O.[PlanType], + O.[UsePolicies], + O.[UseSso], + O.[UseGroups], + O.[UseDirectory], + O.[UseEvents], + O.[UseTotp], + O.[Use2fa], + O.[UseApi], + O.[UseResetPassword], + O.[SelfHost], + O.[UsersGetPremium], + O.[Seats], + O.[MaxCollections], + O.[MaxStorageGb], + O.[Identifier], + OU.[Key], + OU.[ResetPasswordKey], + O.[PublicKey], + O.[PrivateKey], + OU.[Status], + OU.[Type], + SU.[ExternalId] SsoExternalId, + OU.[Permissions], + PO.[ProviderId], + P.[Name] ProviderName, + OS.[FriendlyName] FamilySponsorshipFriendlyName +FROM + [dbo].[OrganizationUser] OU +INNER JOIN + [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] +LEFT JOIN + [dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId] +LEFT JOIN + [dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id] +LEFT JOIN + [dbo].[Provider] P ON P.[Id] = PO.[ProviderId] +LEFT JOIN + [dbo].[OrganizationSponsorship] OS ON OS.[SponsoringOrganizationUserId] = OU.[Id] diff --git a/util/Migrator/DbScripts/2021-11-18_00_MergeKeyConnectorAndFFE.sql b/util/Migrator/DbScripts/2021-11-18_00_MergeKeyConnectorAndFFE.sql new file mode 100644 index 000000000..759d251c4 --- /dev/null +++ b/util/Migrator/DbScripts/2021-11-18_00_MergeKeyConnectorAndFFE.sql @@ -0,0 +1,57 @@ +IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'OrganizationUserOrganizationDetailsView') + BEGIN + DROP VIEW [dbo].[OrganizationUserOrganizationDetailsView] + END +GO + +CREATE VIEW [dbo].[OrganizationUserOrganizationDetailsView] +AS +SELECT + OU.[UserId], + OU.[OrganizationId], + O.[Name], + O.[Enabled], + O.[PlanType], + O.[UsePolicies], + O.[UseSso], + O.[UseKeyConnector], + O.[UseGroups], + O.[UseDirectory], + O.[UseEvents], + O.[UseTotp], + O.[Use2fa], + O.[UseApi], + O.[UseResetPassword], + O.[SelfHost], + O.[UsersGetPremium], + O.[Seats], + O.[MaxCollections], + O.[MaxStorageGb], + O.[Identifier], + OU.[Key], + OU.[ResetPasswordKey], + O.[PublicKey], + O.[PrivateKey], + OU.[Status], + OU.[Type], + SU.[ExternalId] SsoExternalId, + OU.[Permissions], + PO.[ProviderId], + P.[Name] ProviderName, + SS.[Data] SsoConfig, + OS.[FriendlyName] FamilySponsorshipFriendlyName +FROM + [dbo].[OrganizationUser] OU +LEFT JOIN + [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] +LEFT JOIN + [dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId] +LEFT JOIN + [dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id] +LEFT JOIN + [dbo].[Provider] P ON P.[Id] = PO.[ProviderId] +LEFT JOIN + [dbo].[SsoConfig] SS ON SS.[OrganizationId] = OU.[OrganizationId] +LEFT JOIN + [dbo].[OrganizationSponsorship] OS ON OS.[SponsoringOrganizationUserID] = OU.[Id] +GO diff --git a/util/MySqlMigrations/Migrations/20211108225243_OrganizationSponsorship.Designer.cs b/util/MySqlMigrations/Migrations/20211108225243_OrganizationSponsorship.Designer.cs new file mode 100644 index 000000000..4631f9e77 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20211108225243_OrganizationSponsorship.Designer.cs @@ -0,0 +1,1567 @@ +// +using System; +using Bit.Core.Repositories.EntityFramework; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Bit.MySqlMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20211108225243_OrganizationSponsorship")] + partial class OrganizationSponsorship + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 64) + .HasAnnotation("ProductVersion", "5.0.9"); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Cipher", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Attachments") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Favorites") + .HasColumnType("longtext"); + + b.Property("Folders") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Reprompt") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Collection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.HasIndex("UserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("GranteeId") + .HasColumnType("char(36)"); + + b.Property("GrantorId") + .HasColumnType("char(36)"); + + b.Property("KeyEncrypted") + .HasColumnType("longtext"); + + b.Property("LastNotificationDate") + .HasColumnType("datetime(6)"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("WaitTimeDays") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Event", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ActingUserId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("DeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("PolicyId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("ProviderOrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderUserId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("Event"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Folder", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Grant", b => + { + b.Property("Key") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ClientId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ConsumedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Key"); + + b.ToTable("Grant"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Group", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessAll") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.HasIndex("UserId"); + + b.ToTable("GroupUser"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.HasKey("Id"); + + b.ToTable("Installation"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("BillingEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property("Plan") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("SelfHost") + .HasColumnType("tinyint(1)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property("UseSso") + .HasColumnType("tinyint(1)"); + + b.Property("UseTotp") + .HasColumnType("tinyint(1)"); + + b.Property("UsersGetPremium") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Organization"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CloudSponsor") + .HasColumnType("tinyint(1)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("SponsorshipLapsedDate") + .HasColumnType("datetime(6)"); + + b.Property("TimesRenewedWithoutValidation") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("InstallationId"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.ToTable("OrganizationSponsorship"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessAll") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Policy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Policy"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasColumnType("longtext"); + + b.Property("BusinessAddress1") + .HasColumnType("longtext"); + + b.Property("BusinessAddress2") + .HasColumnType("longtext"); + + b.Property("BusinessAddress3") + .HasColumnType("longtext"); + + b.Property("BusinessCountry") + .HasColumnType("longtext"); + + b.Property("BusinessName") + .HasColumnType("longtext"); + + b.Property("BusinessTaxNumber") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Provider"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Settings") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletionDate") + .HasColumnType("datetime(6)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("HideEmail") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MaxAccessCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Send"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("SsoUser"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Rate") + .HasColumnType("decimal(65,30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Transaction", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PaymentMethodType") + .HasColumnType("tinyint unsigned"); + + b.Property("Refunded") + .HasColumnType("tinyint(1)"); + + b.Property("RefundedAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Transaction"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.U2f", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + b.Property("AppId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Challenge") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("KeyHandle") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("Version") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("U2f"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.User", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccountRevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailVerified") + .HasColumnType("tinyint(1)"); + + b.Property("EquivalentDomains") + .HasColumnType("longtext"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("longtext"); + + b.Property("ForcePasswordReset") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Kdf") + .HasColumnType("tinyint unsigned"); + + b.Property("KdfIterations") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Premium") + .HasColumnType("tinyint(1)"); + + b.Property("PremiumExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RenewalReminderDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesCryptoAgent") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Cipher", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Core.Models.EntityFramework.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Collection", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.CollectionCipher", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Core.Models.EntityFramework.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.CollectionGroup", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Core.Models.EntityFramework.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.CollectionUser", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Core.Models.EntityFramework.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Core.Models.EntityFramework.User", null) + .WithMany("CollectionUsers") + .HasForeignKey("UserId"); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Device", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.EmergencyAccess", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Core.Models.EntityFramework.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Folder", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Group", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.GroupUser", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Core.Models.EntityFramework.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Core.Models.EntityFramework.User", null) + .WithMany("GroupUsers") + .HasForeignKey("UserId"); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.OrganizationSponsorship", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId"); + + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("Installation"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.OrganizationUser", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Core.Models.EntityFramework.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Policy", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Core.Models.EntityFramework.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Provider.ProviderUser", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Core.Models.EntityFramework.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Send", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Core.Models.EntityFramework.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.SsoConfig", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.SsoUser", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Core.Models.EntityFramework.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Transaction", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Core.Models.EntityFramework.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.U2f", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.User", "User") + .WithMany("U2fs") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Organization", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("CollectionUsers"); + + b.Navigation("Folders"); + + b.Navigation("GroupUsers"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + + b.Navigation("U2fs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/MySqlMigrations/Migrations/20211108225243_OrganizationSponsorship.cs b/util/MySqlMigrations/Migrations/20211108225243_OrganizationSponsorship.cs new file mode 100644 index 000000000..ae63c2958 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20211108225243_OrganizationSponsorship.cs @@ -0,0 +1,86 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Bit.MySqlMigrations.Migrations +{ + public partial class OrganizationSponsorship : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UsesCryptoAgent", + table: "User", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + + migrationBuilder.CreateTable( + name: "OrganizationSponsorship", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + InstallationId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + SponsoringOrganizationId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + SponsoringOrganizationUserId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + SponsoredOrganizationId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + FriendlyName = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + OfferedToEmail = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + PlanSponsorshipType = table.Column(type: "tinyint unsigned", nullable: true), + CloudSponsor = table.Column(type: "tinyint(1)", nullable: false), + LastSyncDate = table.Column(type: "datetime(6)", nullable: true), + TimesRenewedWithoutValidation = table.Column(type: "tinyint unsigned", nullable: false), + SponsorshipLapsedDate = table.Column(type: "datetime(6)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationSponsorship", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationSponsorship_Installation_InstallationId", + column: x => x.InstallationId, + principalTable: "Installation", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_OrganizationSponsorship_Organization_SponsoredOrganizationId", + column: x => x.SponsoredOrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_OrganizationSponsorship_Organization_SponsoringOrganizationId", + column: x => x.SponsoringOrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationSponsorship_InstallationId", + table: "OrganizationSponsorship", + column: "InstallationId"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationSponsorship_SponsoredOrganizationId", + table: "OrganizationSponsorship", + column: "SponsoredOrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationSponsorship_SponsoringOrganizationId", + table: "OrganizationSponsorship", + column: "SponsoringOrganizationId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OrganizationSponsorship"); + + migrationBuilder.DropColumn( + name: "UsesCryptoAgent", + table: "User"); + } + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index 50709984d..653f22c8b 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -588,6 +588,57 @@ namespace Bit.MySqlMigrations.Migrations b.ToTable("Organization"); }); + modelBuilder.Entity("Bit.Core.Models.EntityFramework.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CloudSponsor") + .HasColumnType("tinyint(1)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("SponsorshipLapsedDate") + .HasColumnType("datetime(6)"); + + b.Property("TimesRenewedWithoutValidation") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("InstallationId"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.ToTable("OrganizationSponsorship"); + }); + modelBuilder.Entity("Bit.Core.Models.EntityFramework.OrganizationUser", b => { b.Property("Id") @@ -1298,6 +1349,27 @@ namespace Bit.MySqlMigrations.Migrations b.Navigation("OrganizationUser"); }); + modelBuilder.Entity("Bit.Core.Models.EntityFramework.OrganizationSponsorship", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId"); + + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("Installation"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + modelBuilder.Entity("Bit.Core.Models.EntityFramework.OrganizationUser", b => { b.HasOne("Bit.Core.Models.EntityFramework.Organization", "Organization") diff --git a/util/MySqlMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.sql b/util/MySqlMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.sql new file mode 100644 index 000000000..5e442e48c --- /dev/null +++ b/util/MySqlMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.sql @@ -0,0 +1,33 @@ +START TRANSACTION; + +ALTER TABLE `User` ADD `UsesCryptoAgent` tinyint(1) NOT NULL DEFAULT FALSE; + +CREATE TABLE `OrganizationSponsorship` ( + `Id` char(36) COLLATE ascii_general_ci NOT NULL, + `InstallationId` char(36) COLLATE ascii_general_ci NULL, + `SponsoringOrganizationId` char(36) COLLATE ascii_general_ci NULL, + `SponsoringOrganizationUserId` char(36) COLLATE ascii_general_ci NULL, + `SponsoredOrganizationId` char(36) COLLATE ascii_general_ci NULL, + `FriendlyName` varchar(256) CHARACTER SET utf8mb4 NULL, + `OfferedToEmail` varchar(256) CHARACTER SET utf8mb4 NULL, + `PlanSponsorshipType` tinyint unsigned NULL, + `CloudSponsor` tinyint(1) NOT NULL, + `LastSyncDate` datetime(6) NULL, + `TimesRenewedWithoutValidation` tinyint unsigned NOT NULL, + `SponsorshipLapsedDate` datetime(6) NULL, + CONSTRAINT `PK_OrganizationSponsorship` PRIMARY KEY (`Id`), + CONSTRAINT `FK_OrganizationSponsorship_Installation_InstallationId` FOREIGN KEY (`InstallationId`) REFERENCES `Installation` (`Id`) ON DELETE RESTRICT, + CONSTRAINT `FK_OrganizationSponsorship_Organization_SponsoredOrganizationId` FOREIGN KEY (`SponsoredOrganizationId`) REFERENCES `Organization` (`Id`) ON DELETE RESTRICT, + CONSTRAINT `FK_OrganizationSponsorship_Organization_SponsoringOrganizationId` FOREIGN KEY (`SponsoringOrganizationId`) REFERENCES `Organization` (`Id`) ON DELETE RESTRICT +) CHARACTER SET utf8mb4; + +CREATE INDEX `IX_OrganizationSponsorship_InstallationId` ON `OrganizationSponsorship` (`InstallationId`); + +CREATE INDEX `IX_OrganizationSponsorship_SponsoredOrganizationId` ON `OrganizationSponsorship` (`SponsoredOrganizationId`); + +CREATE INDEX `IX_OrganizationSponsorship_SponsoringOrganizationId` ON `OrganizationSponsorship` (`SponsoringOrganizationId`); + +INSERT INTO `__EFMigrationsHistory` (`MigrationId`, `ProductVersion`) +VALUES ('20211108225243_OrganizationSponsorship', '5.0.9'); + +COMMIT; diff --git a/util/PostgresMigrations/Migrations/20211108225011_OrganizationSponsorship.Designer.cs b/util/PostgresMigrations/Migrations/20211108225011_OrganizationSponsorship.Designer.cs new file mode 100644 index 000000000..d512fb402 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20211108225011_OrganizationSponsorship.Designer.cs @@ -0,0 +1,1576 @@ +// +using System; +using Bit.Core.Repositories.EntityFramework; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Bit.PostgresMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20211108225011_OrganizationSponsorship")] + partial class OrganizationSponsorship + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:postgresIndetermanisticCollation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.9") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Collection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.HasIndex("UserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GranteeId") + .HasColumnType("uuid"); + + b.Property("GrantorId") + .HasColumnType("uuid"); + + b.Property("KeyEncrypted") + .HasColumnType("text"); + + b.Property("LastNotificationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("WaitTimeDays") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Event", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ActingUserId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp without time zone"); + + b.Property("DeviceType") + .HasColumnType("smallint"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("PolicyId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("ProviderOrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderUserId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Event"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Grant", b => + { + b.Property("Key") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ClientId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ConsumedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Key"); + + b.ToTable("Grant"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Group", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessAll") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.HasIndex("UserId"); + + b.ToTable("GroupUser"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.HasKey("Id"); + + b.ToTable("Installation"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("BillingEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp without time zone"); + + b.Property("Plan") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("SelfHost") + .HasColumnType("boolean"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .HasColumnType("boolean"); + + b.Property("UseSso") + .HasColumnType("boolean"); + + b.Property("UseTotp") + .HasColumnType("boolean"); + + b.Property("UsersGetPremium") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Organization"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CloudSponsor") + .HasColumnType("boolean"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp without time zone"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("SponsorshipLapsedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TimesRenewedWithoutValidation") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("InstallationId"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.ToTable("OrganizationSponsorship"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessAll") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Policy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Policy"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasColumnType("text"); + + b.Property("BusinessAddress1") + .HasColumnType("text"); + + b.Property("BusinessAddress2") + .HasColumnType("text"); + + b.Property("BusinessAddress3") + .HasColumnType("text"); + + b.Property("BusinessCountry") + .HasColumnType("text"); + + b.Property("BusinessName") + .HasColumnType("text"); + + b.Property("BusinessTaxNumber") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Provider"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("HideEmail") + .HasColumnType("boolean"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MaxAccessCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Send"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("SsoUser"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PaymentMethodType") + .HasColumnType("smallint"); + + b.Property("Refunded") + .HasColumnType("boolean"); + + b.Property("RefundedAmount") + .HasColumnType("numeric"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Transaction"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.U2f", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("AppId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Challenge") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("KeyHandle") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("Version") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("U2f"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Culture") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("EquivalentDomains") + .HasColumnType("text"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("text"); + + b.Property("ForcePasswordReset") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Kdf") + .HasColumnType("smallint"); + + b.Property("KdfIterations") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Premium") + .HasColumnType("boolean"); + + b.Property("PremiumExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesCryptoAgent") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Cipher", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Core.Models.EntityFramework.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Collection", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.CollectionCipher", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Core.Models.EntityFramework.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.CollectionGroup", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Core.Models.EntityFramework.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.CollectionUser", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Core.Models.EntityFramework.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Core.Models.EntityFramework.User", null) + .WithMany("CollectionUsers") + .HasForeignKey("UserId"); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Device", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.EmergencyAccess", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Core.Models.EntityFramework.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Folder", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Group", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.GroupUser", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Core.Models.EntityFramework.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Core.Models.EntityFramework.User", null) + .WithMany("GroupUsers") + .HasForeignKey("UserId"); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.OrganizationSponsorship", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId"); + + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("Installation"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.OrganizationUser", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Core.Models.EntityFramework.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Policy", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Core.Models.EntityFramework.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Provider.ProviderUser", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Core.Models.EntityFramework.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Send", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Core.Models.EntityFramework.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.SsoConfig", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.SsoUser", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Core.Models.EntityFramework.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Transaction", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Core.Models.EntityFramework.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.U2f", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.User", "User") + .WithMany("U2fs") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.Organization", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Core.Models.EntityFramework.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("CollectionUsers"); + + b.Navigation("Folders"); + + b.Navigation("GroupUsers"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + + b.Navigation("U2fs"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/PostgresMigrations/Migrations/20211108225011_OrganizationSponsorship.cs b/util/PostgresMigrations/Migrations/20211108225011_OrganizationSponsorship.cs new file mode 100644 index 000000000..162792a6e --- /dev/null +++ b/util/PostgresMigrations/Migrations/20211108225011_OrganizationSponsorship.cs @@ -0,0 +1,83 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Bit.PostgresMigrations.Migrations +{ + public partial class OrganizationSponsorship : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UsesCryptoAgent", + table: "User", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.CreateTable( + name: "OrganizationSponsorship", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + InstallationId = table.Column(type: "uuid", nullable: true), + SponsoringOrganizationId = table.Column(type: "uuid", nullable: true), + SponsoringOrganizationUserId = table.Column(type: "uuid", nullable: true), + SponsoredOrganizationId = table.Column(type: "uuid", nullable: true), + FriendlyName = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + OfferedToEmail = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + PlanSponsorshipType = table.Column(type: "smallint", nullable: true), + CloudSponsor = table.Column(type: "boolean", nullable: false), + LastSyncDate = table.Column(type: "timestamp without time zone", nullable: true), + TimesRenewedWithoutValidation = table.Column(type: "smallint", nullable: false), + SponsorshipLapsedDate = table.Column(type: "timestamp without time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationSponsorship", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationSponsorship_Installation_InstallationId", + column: x => x.InstallationId, + principalTable: "Installation", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_OrganizationSponsorship_Organization_SponsoredOrganizationId", + column: x => x.SponsoredOrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_OrganizationSponsorship_Organization_SponsoringOrganization~", + column: x => x.SponsoringOrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationSponsorship_InstallationId", + table: "OrganizationSponsorship", + column: "InstallationId"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationSponsorship_SponsoredOrganizationId", + table: "OrganizationSponsorship", + column: "SponsoredOrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationSponsorship_SponsoringOrganizationId", + table: "OrganizationSponsorship", + column: "SponsoringOrganizationId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OrganizationSponsorship"); + + migrationBuilder.DropColumn( + name: "UsesCryptoAgent", + table: "User"); + } + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index 1c4b70b11..ed14e0421 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -592,6 +592,57 @@ namespace Bit.PostgresMigrations.Migrations b.ToTable("Organization"); }); + modelBuilder.Entity("Bit.Core.Models.EntityFramework.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CloudSponsor") + .HasColumnType("boolean"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp without time zone"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("SponsorshipLapsedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("TimesRenewedWithoutValidation") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("InstallationId"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.ToTable("OrganizationSponsorship"); + }); + modelBuilder.Entity("Bit.Core.Models.EntityFramework.OrganizationUser", b => { b.Property("Id") @@ -1307,6 +1358,27 @@ namespace Bit.PostgresMigrations.Migrations b.Navigation("OrganizationUser"); }); + modelBuilder.Entity("Bit.Core.Models.EntityFramework.OrganizationSponsorship", b => + { + b.HasOne("Bit.Core.Models.EntityFramework.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId"); + + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Core.Models.EntityFramework.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("Installation"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + modelBuilder.Entity("Bit.Core.Models.EntityFramework.OrganizationUser", b => { b.HasOne("Bit.Core.Models.EntityFramework.Organization", "Organization") diff --git a/util/PostgresMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.psql b/util/PostgresMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.psql new file mode 100644 index 000000000..24d5eaa08 --- /dev/null +++ b/util/PostgresMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.psql @@ -0,0 +1,33 @@ +START TRANSACTION; + +ALTER TABLE "User" ADD "UsesCryptoAgent" boolean NOT NULL DEFAULT FALSE; + +CREATE TABLE "OrganizationSponsorship" ( + "Id" uuid NOT NULL, + "InstallationId" uuid NULL, + "SponsoringOrganizationId" uuid NULL, + "SponsoringOrganizationUserId" uuid NULL, + "SponsoredOrganizationId" uuid NULL, + "FriendlyName" character varying(256) NULL, + "OfferedToEmail" character varying(256) NULL, + "PlanSponsorshipType" smallint NULL, + "CloudSponsor" boolean NOT NULL, + "LastSyncDate" timestamp without time zone NULL, + "TimesRenewedWithoutValidation" smallint NOT NULL, + "SponsorshipLapsedDate" timestamp without time zone NULL, + CONSTRAINT "PK_OrganizationSponsorship" PRIMARY KEY ("Id"), + CONSTRAINT "FK_OrganizationSponsorship_Installation_InstallationId" FOREIGN KEY ("InstallationId") REFERENCES "Installation" ("Id") ON DELETE RESTRICT, + CONSTRAINT "FK_OrganizationSponsorship_Organization_SponsoredOrganizationId" FOREIGN KEY ("SponsoredOrganizationId") REFERENCES "Organization" ("Id") ON DELETE RESTRICT, + CONSTRAINT "FK_OrganizationSponsorship_Organization_SponsoringOrganization~" FOREIGN KEY ("SponsoringOrganizationId") REFERENCES "Organization" ("Id") ON DELETE RESTRICT +); + +CREATE INDEX "IX_OrganizationSponsorship_InstallationId" ON "OrganizationSponsorship" ("InstallationId"); + +CREATE INDEX "IX_OrganizationSponsorship_SponsoredOrganizationId" ON "OrganizationSponsorship" ("SponsoredOrganizationId"); + +CREATE INDEX "IX_OrganizationSponsorship_SponsoringOrganizationId" ON "OrganizationSponsorship" ("SponsoringOrganizationId"); + +INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") +VALUES ('20211108225011_OrganizationSponsorship', '5.0.9'); + +COMMIT;