From 33edc8eba052836aef59a0459996d11d5c58b647 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Fri, 19 Nov 2021 16:25:06 -0600 Subject: [PATCH] Families for Enterprise (#1714) * Create common test infrastructure project * Add helpers to further type PlanTypes * Enable testing of ASP.net MVC controllers Controller properties have all kinds of validations in the background. In general, we don't user properties on our Controllers, so the easiest way to allow for Autofixture-based testing of our Controllers is to just omit setting all properties on them. * Workaround for broken MemberAutoDataAttribute https://github.com/AutoFixture/AutoFixture/pull/1164 shows that only the first test case is pulled for this attribute. This is a workaround that populates the provided parameters, left to right, using AutoFixture to populate any remaining. * WIP: Organization sponsorship flow * Add Attribute to use the Bit Autodata dependency chain BitAutoDataAttribute is used to mark a Theory as autopopulating parameters. Extract common attribute methods to to a helper class. Cannot inherit a common base, since both require inheriting from different Xunit base classes to work. * WIP: scaffolding for families for enterprise sponsorship flow * Fix broken tests * Create sponsorship offer (#1688) * Initial db work (#1687) * Add organization sponsorship databases to all providers * Generalize create and update for database, specialize in code * Add PlanSponsorshipType to db model * Write valid json for test entries * Initial scaffolding of emails (#1686) * Initial scaffolding of emails * Work on adding models for FamilyForEnterprise emails * Switch verbage * Put preliminary copy in emails * Skip test * Families for enterprise/stripe integrations (#1699) * Add PlanSponsorshipType to static store * Add sponsorship type to token and creates sponsorship * PascalCase properties * Require sponsorship for remove * Create subscription sponsorship helper class * Handle Sponsored subscription changes * Add sponsorship id to subscription metadata * Make sponsoring references nullable This state indicates that a sponsorship has lapsed, but was not able to be reverted for billing reasons * WIP: Validate and remove subscriptions * Update sponsorships on organization and org user delete * Add friendly name to organization sponsorship * Add sponsorship available boolean to orgDetails * Add sponsorship service to DI * Use userId to find org users * Send f4e offer email * Simplify names of f4e mail messages * Fix Stripe org default tax rates * Universal sponsorship redeem api * Populate user in current context * Add product type to organization details * Use upgrade path to change sponsorship Sponsorships need to be annual to match the GB add-on charge rate * Use organization and auth to find organization sponsorship * Add resend sponsorship offer api endpoint * Fix double email send * Fix sponsorship upgrade options * Add is sponsored item to subscription response * Add sponsorship validation to upcoming invoice webhook * Add sponsorship validation to upcoming invoice webhook * Fix organization delete sponsorship hooks * Test org sponsorship service * Fix sproc * Create common test infrastructure project * Add helpers to further type PlanTypes * Enable testing of ASP.net MVC controllers Controller properties have all kinds of validations in the background. In general, we don't user properties on our Controllers, so the easiest way to allow for Autofixture-based testing of our Controllers is to just omit setting all properties on them. * Workaround for broken MemberAutoDataAttribute https://github.com/AutoFixture/AutoFixture/pull/1164 shows that only the first test case is pulled for this attribute. This is a workaround that populates the provided parameters, left to right, using AutoFixture to populate any remaining. * WIP: Organization sponsorship flow * Add Attribute to use the Bit Autodata dependency chain BitAutoDataAttribute is used to mark a Theory as autopopulating parameters. Extract common attribute methods to to a helper class. Cannot inherit a common base, since both require inheriting from different Xunit base classes to work. * WIP: scaffolding for families for enterprise sponsorship flow * Fix broken tests * Create sponsorship offer (#1688) * Initial db work (#1687) * Add organization sponsorship databases to all providers * Generalize create and update for database, specialize in code * Add PlanSponsorshipType to db model * Write valid json for test entries * Initial scaffolding of emails (#1686) * Initial scaffolding of emails * Work on adding models for FamilyForEnterprise emails * Switch verbage * Put preliminary copy in emails * Skip test * Families for enterprise/stripe integrations (#1699) * Add PlanSponsorshipType to static store * Add sponsorship type to token and creates sponsorship * PascalCase properties * Require sponsorship for remove * Create subscription sponsorship helper class * Handle Sponsored subscription changes * Add sponsorship id to subscription metadata * Make sponsoring references nullable This state indicates that a sponsorship has lapsed, but was not able to be reverted for billing reasons * WIP: Validate and remove subscriptions * Update sponsorships on organization and org user delete * Add friendly name to organization sponsorship * Add sponsorship available boolean to orgDetails * Add sponsorship service to DI * Use userId to find org users * Send f4e offer email * Simplify names of f4e mail messages * Fix Stripe org default tax rates * Universal sponsorship redeem api * Populate user in current context * Add product type to organization details * Use upgrade path to change sponsorship Sponsorships need to be annual to match the GB add-on charge rate * Use organization and auth to find organization sponsorship * Add resend sponsorship offer api endpoint * Fix double email send * Fix sponsorship upgrade options * Add is sponsored item to subscription response * Add sponsorship validation to upcoming invoice webhook * Add sponsorship validation to upcoming invoice webhook * Fix organization delete sponsorship hooks * Test org sponsorship service * Fix sproc * Fix build error * Update emails * Fix tests * Skip local test * Add newline * Fix stripe subscription update * Finish emails * Skip test * Fix unit tests * Remove unused variable * Fix unit tests * Switch to handlebars ifs * Remove ending email * Remove reconfirmation template * Switch naming convention * Switch naming convention * Fix migration * Update copy and links * Switch to using Guid in the method * Remove unneeded css styles * Add sql files to Sql.sqlproj * Removed old comments * Made name more verbose * Fix SQL error * Move unit tests to service * Fix sp * Revert "Move unit tests to service" This reverts commit 1185bf3ec8ca36ccd75717ed2463adf8885159a6. * Do repository validation in service layer * Fix tests * Fix merge conflicts and remove TODO * Remove unneeded models * Fix spacing and formatting * Switch Org -> Organization * Remove single use variables * Switch method name * Fix Controller * Switch to obfuscating email * Fix unit tests Co-authored-by: Justin Baur --- bitwarden-server.sln | 8 +- .../Services/ProviderServiceTests.cs | 4 +- .../OrganizationSponsorshipsController.cs | 134 ++ src/Billing/Controllers/StripeController.cs | 13 + src/Core/Enums/PaymentMethodType.cs | 2 + src/Core/Enums/PlanSponsorshipType.cs | 10 + src/Core/Enums/PlanType.cs | 2 +- ...ForEnterpriseOfferExistingAccount.html.hbs | 21 + ...ForEnterpriseOfferExistingAccount.text.hbs | 5 + ...iliesForEnterpriseOfferNewAccount.html.hbs | 21 + ...iliesForEnterpriseOfferNewAccount.text.hbs | 7 + ...nterpriseRedeemedToEnterpriseUser.html.hbs | 9 + ...nterpriseRedeemedToEnterpriseUser.text.hbs | 3 + ...ForEnterpriseRedeemedToFamilyUser.html.hbs | 9 + ...ForEnterpriseRedeemedToFamilyUser.text.hbs | 3 + ...ForEnterpriseSponsorshipReverting.html.hbs | 9 + ...ForEnterpriseSponsorshipReverting.text.hbs | 3 + .../OrganizationUserInvited.html.hbs | 4 + .../OrganizationUserInvited.text.hbs | 3 + ...ganizationSponsorshipRedeemRequestModel.cs | 14 + .../OrganizationSponsorshipRequestModel.cs | 21 + .../ProfileOrganizationResponseModel.cs | 9 + .../Api/Response/SubscriptionResponseModel.cs | 2 + .../Business/SubscriptionCreateOptions.cs | 6 +- src/Core/Models/Business/SubscriptionInfo.cs | 2 + .../Models/Business/SubscriptionUpdate.cs | 180 +- .../OrganizationUserOrganizationDetails.cs | 2 + .../OrganizationSponsorship.cs | 20 + ...EnterpriseOfferExistingAccountViewModel.cs | 10 + ...esForEnterpriseOfferNewAccountViewModel.cs | 10 + ...EnterpriseSponsorshipRevertingViewModel.cs | 7 + .../Mail/OrganizationUserInvitedViewModel.cs | 1 + src/Core/Models/StaticStore/SponsoredPlan.cs | 15 + .../Models/Table/OrganizationSponsorship.cs | 31 + .../EntityFramework/DatabaseContext.cs | 4 + .../EntityFramework/OrganizationRepository.cs | 24 + .../OrganizationSponsorshipRepository.cs | 52 + .../OrganizationUserRepository.cs | 20 + ...izationUserOrganizationDetailsViewQuery.cs | 7 +- .../IOrganizationSponsorshipRepository.cs | 15 + .../OrganizationSponsorshipRepository.cs | 67 + src/Core/Services/IMailService.cs | 7 +- .../IOrganizationSponsorshipService.cs | 22 + src/Core/Services/IPaymentService.cs | 9 +- .../Implementations/HandlebarsMailService.cs | 80 +- .../Implementations/OrganizationService.cs | 13 +- .../OrganizationSponsorshipService.cs | 310 ++++ .../Implementations/StripePaymentService.cs | 76 +- .../NoopImplementations/NoopMailService.cs | 19 +- src/Core/Utilities/CoreHelpers.cs | 34 + .../Utilities/ServiceCollectionExtensions.cs | 3 + src/Core/Utilities/StaticStore.cs | 18 + src/Sql/Sql.sqlproj | 11 + .../OrganizationSponsorship_Create.sql | 46 + .../OrganizationSponsorship_DeleteById.sql | 17 + ...izationSponsorship_OrganizationDeleted.sql | 31 + ...ionSponsorship_OrganizationUserDeleted.sql | 13 + ...onSponsorship_OrganizationUsersDeleted.sql | 25 + .../OrganizationSponsorship_ReadById.sql | 14 + ...zationSponsorship_ReadByOfferedToEmail.sql | 14 + ...nsorship_ReadBySponsoredOrganizationId.sql | 14 + ...ship_ReadBySponsoringOrganiationUserId.sql | 14 + .../OrganizationSponsorship_Update.sql | 33 + .../OrganizationUser_DeleteById.sql | 4 +- .../OrganizationUser_DeleteByIds.sql | 1 + .../Organization_DeleteById.sql | 2 + .../dbo/Tables/OrganizationSponsorship.sql | 43 + test/Api.Test/Api.Test.csproj | 1 + .../ControllerCustomizeAttribute.cs | 17 + .../AutoFixture/ControllerCustomization.cs | 38 + ...OrganizationSponsorshipsControllerTests.cs | 109 ++ .../Attributes/BitAutoDataAttribute.cs | 29 + .../Attributes/BitCustomizeAttribute.cs | 22 + .../Attributes/CustomAutoDataAttribute.cs | 2 +- .../InlineCustomAutoDataAttribute.cs | 4 +- .../Attributes/InlineSutAutoDataAttribute.cs | 20 + .../Attributes/MemberAutoDataAttribute.cs | 27 + .../Attributes/SutAutoDataAttribute.cs | 20 + .../BuilderWithoutAutoProperties.cs | 41 + .../AutoFixture/FixtureExtensions.cs | 2 +- .../AutoFixture/GlobalSettingsFixtures.cs | 25 +- .../AutoFixture/ISutProvider.cs | 2 +- .../AutoFixture/SutProvider.cs | 3 +- .../AutoFixture/SutProviderCustomization.cs | 2 +- test/Common/Common.csproj | 25 + test/Common/Helpers/AssertHelper.cs | 58 + .../Helpers/BitAutoDataAttributeHelpers.cs | 55 + test/Common/Helpers/Factories.cs | 19 + test/Core.Test/AutoFixture/CipherFixtures.cs | 5 +- .../AutoFixture/CollectionCipherFixtures.cs | 11 +- .../AutoFixture/CollectionFixtures.cs | 10 +- .../AutoFixture/CurrentContextFixtures.cs | 1 + test/Core.Test/AutoFixture/DeviceFixtures.cs | 11 +- .../AutoFixture/EmergencyAccessFixtures.cs | 12 +- .../EntityFrameworkRepositoryFixtures.cs | 3 +- test/Core.Test/AutoFixture/EventFixtures.cs | 10 +- test/Core.Test/AutoFixture/FolderFixtures.cs | 10 +- test/Core.Test/AutoFixture/GrantFixtures.cs | 10 +- test/Core.Test/AutoFixture/GroupFixtures.cs | 4 +- .../AutoFixture/GroupUserFixtures.cs | 10 +- .../AutoFixture/InstallationFixtures.cs | 10 +- .../AutoFixture/OrganizationFixtures.cs | 8 +- .../OrganizationSponsorshipFixtures.cs | 60 + .../AutoFixture/OrganizationUserFixtures.cs | 6 +- test/Core.Test/AutoFixture/PolicyFixtures.cs | 4 +- test/Core.Test/AutoFixture/SendFixtures.cs | 4 +- .../AutoFixture/SsoConfigFixtures.cs | 6 +- test/Core.Test/AutoFixture/SsoUserFixtures.cs | 10 +- test/Core.Test/AutoFixture/TaxRateFixtures.cs | 10 +- .../AutoFixture/TransactionFixutres.cs | 8 +- test/Core.Test/AutoFixture/U2fFixtures.cs | 10 +- test/Core.Test/AutoFixture/UserFixtures.cs | 4 +- test/Core.Test/Core.Test.csproj | 1 + test/Core.Test/Helpers/Factories.cs | 12 +- .../OrganizationSponsorshipCompare.cs | 26 + .../OrganizationSponsorshipRepositoryTests.cs | 138 ++ test/Core.Test/Services/CipherServiceTests.cs | 2 +- .../Services/CollectionServiceTests.cs | 4 +- test/Core.Test/Services/DeviceServiceTests.cs | 5 +- .../Services/EmergencyAccessServiceTests.cs | 2 + test/Core.Test/Services/GroupServiceTests.cs | 2 + .../Services/HandlebarsMailServiceTests.cs | 139 ++ .../LocalAttachmentStorageServiceTests.cs | 4 +- .../Services/OrganizationServiceTests.cs | 7 +- .../OrganizationSponsorshipServiceTests.cs | 680 +++++++ test/Core.Test/Services/PolicyServiceTests.cs | 40 +- test/Core.Test/Services/SendServiceTests.cs | 10 +- .../Services/SsoConfigServiceTests.cs | 4 +- test/Core.Test/Utilities/CoreHelpersTests.cs | 15 +- test/bitwarden.tests.sln | 14 + .../2021-11-02_00_OrganizationSponsorship.sql | 663 +++++++ .../2021-11-18_00_MergeKeyConnectorAndFFE.sql | 57 + ...225243_OrganizationSponsorship.Designer.cs | 1567 ++++++++++++++++ .../20211108225243_OrganizationSponsorship.cs | 86 + .../DatabaseContextModelSnapshot.cs | 72 + .../2021-11-02_00_OrganizationSponsorship.sql | 33 + ...225011_OrganizationSponsorship.Designer.cs | 1576 +++++++++++++++++ .../20211108225011_OrganizationSponsorship.cs | 83 + .../DatabaseContextModelSnapshot.cs | 72 + ...2021-11-02_00_OrganizationSponsorship.psql | 33 + 140 files changed, 7482 insertions(+), 285 deletions(-) create mode 100644 src/Api/Controllers/OrganizationSponsorshipsController.cs create mode 100644 src/Core/Enums/PlanSponsorshipType.cs create mode 100644 src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferExistingAccount.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferExistingAccount.text.hbs create mode 100644 src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferNewAccount.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferNewAccount.text.hbs create mode 100644 src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToEnterpriseUser.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToEnterpriseUser.text.hbs create mode 100644 src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToFamilyUser.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToFamilyUser.text.hbs create mode 100644 src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipReverting.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipReverting.text.hbs create mode 100644 src/Core/Models/Api/Request/Organizations/OrganizationSponsorshipRedeemRequestModel.cs create mode 100644 src/Core/Models/Api/Request/Organizations/OrganizationSponsorshipRequestModel.cs create mode 100644 src/Core/Models/EntityFramework/OrganizationSponsorship.cs create mode 100644 src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseOfferExistingAccountViewModel.cs create mode 100644 src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseOfferNewAccountViewModel.cs create mode 100644 src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipRevertingViewModel.cs create mode 100644 src/Core/Models/StaticStore/SponsoredPlan.cs create mode 100644 src/Core/Models/Table/OrganizationSponsorship.cs create mode 100644 src/Core/Repositories/EntityFramework/OrganizationSponsorshipRepository.cs create mode 100644 src/Core/Repositories/IOrganizationSponsorshipRepository.cs create mode 100644 src/Core/Repositories/SqlServer/OrganizationSponsorshipRepository.cs create mode 100644 src/Core/Services/IOrganizationSponsorshipService.cs create mode 100644 src/Core/Services/Implementations/OrganizationSponsorshipService.cs create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Create.sql create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationSponsorship_DeleteById.sql create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationDeleted.sql create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationUserDeleted.sql create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationUsersDeleted.sql create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadById.sql create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadByOfferedToEmail.sql create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoredOrganizationId.sql create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoringOrganiationUserId.sql create mode 100644 src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Update.sql create mode 100644 src/Sql/dbo/Tables/OrganizationSponsorship.sql create mode 100644 test/Api.Test/AutoFixture/Attributes/ControllerCustomizeAttribute.cs create mode 100644 test/Api.Test/AutoFixture/ControllerCustomization.cs create mode 100644 test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs create mode 100644 test/Common/AutoFixture/Attributes/BitAutoDataAttribute.cs create mode 100644 test/Common/AutoFixture/Attributes/BitCustomizeAttribute.cs rename test/{Core.Test => Common}/AutoFixture/Attributes/CustomAutoDataAttribute.cs (93%) rename test/{Core.Test => Common}/AutoFixture/Attributes/InlineCustomAutoDataAttribute.cs (83%) create mode 100644 test/Common/AutoFixture/Attributes/InlineSutAutoDataAttribute.cs create mode 100644 test/Common/AutoFixture/Attributes/MemberAutoDataAttribute.cs create mode 100644 test/Common/AutoFixture/Attributes/SutAutoDataAttribute.cs create mode 100644 test/Common/AutoFixture/BuilderWithoutAutoProperties.cs rename test/{Core.Test => Common}/AutoFixture/FixtureExtensions.cs (87%) rename test/{Core.Test => Common}/AutoFixture/GlobalSettingsFixtures.cs (57%) rename test/{Core.Test => Common}/AutoFixture/ISutProvider.cs (76%) rename test/{Core.Test => Common}/AutoFixture/SutProvider.cs (98%) rename test/{Core.Test => Common}/AutoFixture/SutProviderCustomization.cs (94%) create mode 100644 test/Common/Common.csproj create mode 100644 test/Common/Helpers/AssertHelper.cs create mode 100644 test/Common/Helpers/BitAutoDataAttributeHelpers.cs create mode 100644 test/Common/Helpers/Factories.cs create mode 100644 test/Core.Test/AutoFixture/OrganizationSponsorshipFixtures.cs create mode 100644 test/Core.Test/Repositories/EntityFramework/EqualityComparers/OrganizationSponsorshipCompare.cs create mode 100644 test/Core.Test/Repositories/EntityFramework/OrganizationSponsorshipRepositoryTests.cs create mode 100644 test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs create mode 100644 util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql create mode 100644 util/Migrator/DbScripts/2021-11-18_00_MergeKeyConnectorAndFFE.sql create mode 100644 util/MySqlMigrations/Migrations/20211108225243_OrganizationSponsorship.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20211108225243_OrganizationSponsorship.cs create mode 100644 util/MySqlMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.sql create mode 100644 util/PostgresMigrations/Migrations/20211108225011_OrganizationSponsorship.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20211108225011_OrganizationSponsorship.cs create mode 100644 util/PostgresMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.psql 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;