From 53b82d7d06a951c11b87c5995072b543472a5584 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Fri, 29 Oct 2021 15:05:45 -0400 Subject: [PATCH 01/34] Create common test infrastructure project --- bitwarden-server.sln | 8 +++- .../Attributes/CustomAutoDataAttribute.cs | 2 +- .../InlineCustomAutoDataAttribute.cs | 4 +- .../Attributes/InlineSutAutoDataAttribute.cs | 20 +++++++++ .../Attributes/SutAutoDataAttribute.cs | 15 +++++++ .../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/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 | 2 +- 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 ++-- .../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 +----- test/Core.Test/Services/CipherServiceTests.cs | 2 +- .../Services/CollectionServiceTests.cs | 4 +- test/Core.Test/Services/DeviceServiceTests.cs | 5 ++- test/Core.Test/Services/GroupServiceTests.cs | 2 + .../LocalAttachmentStorageServiceTests.cs | 4 +- .../Services/OrganizationServiceTests.cs | 5 +-- test/Core.Test/Services/PolicyServiceTests.cs | 42 +++++++++---------- test/Core.Test/Services/SendServiceTests.cs | 10 ++--- .../Services/SsoConfigServiceTests.cs | 4 +- test/Core.Test/Utilities/CoreHelpersTests.cs | 4 +- test/bitwarden.tests.sln | 14 +++++++ 48 files changed, 210 insertions(+), 202 deletions(-) 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/SutAutoDataAttribute.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/Factories.cs diff --git a/bitwarden-server.sln b/bitwarden-server.sln index 410afae947..50095d7259 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/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 ac2e81c1a9..cd2feebcac 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 ac1c22a65c..d36f963a4f 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 0000000000..89eebad8c0 --- /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/SutAutoDataAttribute.cs b/test/Common/AutoFixture/Attributes/SutAutoDataAttribute.cs new file mode 100644 index 0000000000..0aa666dade --- /dev/null +++ b/test/Common/AutoFixture/Attributes/SutAutoDataAttribute.cs @@ -0,0 +1,15 @@ +using System; +using System.Linq; +using System.Reflection; +using AutoFixture; +using AutoFixture.Xunit2; + +namespace Bit.Test.Common.AutoFixture.Attributes +{ + public class SutAutoDataAttribute : CustomAutoDataAttribute + { + public SutAutoDataAttribute(params Type[] iCustomizationTypes) : base( + iCustomizationTypes.Append(typeof(SutProviderCustomization)).ToArray()) + { } + } +} 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 10021fbee1..7249e8e41f 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 9eea0b063c..8df15b107b 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 3a22bf4895..c72dc4a279 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 e8b475d248..0846655a7e 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 549af17e78..f6041e91e6 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 0000000000..16dc167d41 --- /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/Factories.cs b/test/Common/Helpers/Factories.cs new file mode 100644 index 0000000000..5067662d23 --- /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 30d21f132a..23730fe9d4 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 54bc7524cf..79a3cd38a3 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 adad029d86..fd1d91b5c0 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 21b82102bd..8dcaeb6460 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 17472c5fb7..4fac109b06 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 cdf240803c..760a92a1e9 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 88bf72168d..4096f6919e 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 { diff --git a/test/Core.Test/AutoFixture/EventFixtures.cs b/test/Core.Test/AutoFixture/EventFixtures.cs index b903b2639f..f5674ddd49 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 9449ae71c3..f46de2f75d 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 a0142a29ad..2c837f2169 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 77190ce7b4..b13a1c063d 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 1ee09445e5..6a44d7c816 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 8e1aa74eee..87980fa824 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 2eb84ea6ae..819ebeb857 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/OrganizationUserFixtures.cs b/test/Core.Test/AutoFixture/OrganizationUserFixtures.cs index f5e5256dd0..1a083c2f32 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 8689916939..71b2e30a73 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 1588837c6e..17dee60ed9 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 14e681af75..5ecbc247c0 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 bb756a73bc..9dc452466b 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 b0a94eacce..d1e89fa5d9 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 e2660c8c7a..0610ced028 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 d813fd20ea..78fac0c4d5 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 fb9a3e27c9..1608db1510 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 45c44fa1bd..725a51d4c4 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 61469a543a..56092122dc 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/Services/CipherServiceTests.cs b/test/Core.Test/Services/CipherServiceTests.cs index 94aa498630..45e1f189fd 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 b1748e8587..7276216902 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 dd125759b2..4136486343 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/GroupServiceTests.cs b/test/Core.Test/Services/GroupServiceTests.cs index 3b8467ce37..04ddf20ced 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/LocalAttachmentStorageServiceTests.cs b/test/Core.Test/Services/LocalAttachmentStorageServiceTests.cs index e70495d474..386bc2beef 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 c9e74b93f0..3bd77b8e8d 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 { diff --git a/test/Core.Test/Services/PolicyServiceTests.cs b/test/Core.Test/Services/PolicyServiceTests.cs index b344e1877c..db2561461d 100644 --- a/test/Core.Test/Services/PolicyServiceTests.cs +++ b/test/Core.Test/Services/PolicyServiceTests.cs @@ -5,19 +5,19 @@ using Bit.Core.Exceptions; 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); @@ -39,7 +39,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(); @@ -66,7 +66,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; @@ -77,7 +77,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( @@ -98,7 +98,7 @@ namespace Bit.Core.Test.Services } [Theory, CustomAutoData(typeof(SutProviderCustomization))] - public async Task SaveAsync_RequireSsoPolicy_NotEnabled_ThrowsBadRequestAsync([PolicyFixtures.Policy(Enums.PolicyType.RequireSso)] Core.Models.Table.Policy policy, SutProvider sutProvider) + public async Task SaveAsync_RequireSsoPolicy_NotEnabled_ThrowsBadRequestAsync([PolicyFixtures.Policy(PolicyType.RequireSso)] Core.Models.Table.Policy policy, SutProvider sutProvider) { policy.Enabled = true; @@ -109,7 +109,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( @@ -130,7 +130,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; @@ -145,7 +145,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); @@ -155,7 +155,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 @@ -173,15 +173,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", @@ -214,7 +214,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); @@ -224,7 +224,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 @@ -242,15 +242,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", @@ -277,7 +277,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 b18733edc5..7c08f6d007 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 403df8dac7..c9b1baaabb 100644 --- a/test/Core.Test/Services/SsoConfigServiceTests.cs +++ b/test/Core.Test/Services/SsoConfigServiceTests.cs @@ -3,8 +3,8 @@ using System.Threading.Tasks; 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 cf43aa13ba..f793a41f63 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 { diff --git a/test/bitwarden.tests.sln b/test/bitwarden.tests.sln index 8968eea3d5..cd5f7820f3 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 From 3a11101c30e478ec90980ee5bf8afee269fbbf54 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Fri, 29 Oct 2021 18:24:14 -0400 Subject: [PATCH 02/34] Add helpers to further type PlanTypes --- src/Core/Enums/PlanType.cs | 22 ++++++++++++ test/Core.Test/Enums/PlanTypeHelperTests.cs | 39 +++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 test/Core.Test/Enums/PlanTypeHelperTests.cs diff --git a/src/Core/Enums/PlanType.cs b/src/Core/Enums/PlanType.cs index 572c40fea8..501b8ec3c0 100644 --- a/src/Core/Enums/PlanType.cs +++ b/src/Core/Enums/PlanType.cs @@ -1,4 +1,6 @@ using System.ComponentModel.DataAnnotations; +using System.Linq; +using Bit.Core.Models.Table; namespace Bit.Core.Enums { @@ -29,4 +31,24 @@ namespace Bit.Core.Enums [Display(Name = "Enterprise (Annually)")] EnterpriseAnnually= 11, } + + public static class PlanTypeHelper + { + private static readonly PlanType[] _freePlans = new[] { PlanType.Free }; + private static readonly PlanType[] _familiesPlans = new[] { PlanType.FamiliesAnnually, PlanType.FamiliesAnnually2019 }; + private static readonly PlanType[] _teamsPlans = new[] { PlanType.TeamsAnnually, PlanType.TeamsAnnually2019, + PlanType.TeamsMonthly, PlanType.TeamsMonthly2019}; + private static readonly PlanType[] _enterprisePlans = new[] { PlanType.EnterpriseAnnually, + PlanType.EnterpriseAnnually2019, PlanType.EnterpriseMonthly, PlanType.EnterpriseMonthly2019 }; + + private static bool HasPlan(PlanType[] planTypes, PlanType planType) => planTypes.Any(p => p == planType); + public static bool HasFreePlan(Organization org) => IsFree(org.PlanType); + public static bool IsFree(PlanType planType) => HasPlan(_freePlans, planType); + public static bool HasFamiliesPlan(Organization org) => IsFamilies(org.PlanType); + public static bool IsFamilies(PlanType planType) => HasPlan(_familiesPlans, planType); + public static bool HasTeamsPlan(Organization org) => IsTeams(org.PlanType); + public static bool IsTeams(PlanType planType) => HasPlan(_teamsPlans, planType); + public static bool HasEnterprisePlan(Organization org) => IsEnterprise(org.PlanType); + public static bool IsEnterprise(PlanType planType) => HasPlan(_enterprisePlans, planType); + } } diff --git a/test/Core.Test/Enums/PlanTypeHelperTests.cs b/test/Core.Test/Enums/PlanTypeHelperTests.cs new file mode 100644 index 0000000000..d8af51f1cb --- /dev/null +++ b/test/Core.Test/Enums/PlanTypeHelperTests.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using Bit.Core.Enums; +using Bit.Core.Models.Table; +using Xunit; + +namespace Bit.Core.Test.Enums +{ + public class PlanTypeHelperTests + { + private static IEnumerable PlanArchetypeArray(PlanType planType) => new PlanType?[] { + PlanTypeHelper.HasFreePlan(new Organization {PlanType = planType}) ? planType : null, + PlanTypeHelper.HasFamiliesPlan(new Organization {PlanType = planType}) ? planType : null, + PlanTypeHelper.HasTeamsPlan(new Organization {PlanType = planType}) ? planType : null, + PlanTypeHelper.HasEnterprisePlan(new Organization {PlanType = planType}) ? planType : null, + }.Where(v => v.HasValue).Select(v => (PlanType)v); + + public static IEnumerable PlanTypes => Enum.GetValues().Select(p => new object[] { p }); + public static IEnumerable PlanTypesExceptCustom => + Enum.GetValues().Except(new[] { PlanType.Custom }).Select(p => new object[] { p }); + + [Theory] + [MemberData(nameof(PlanTypesExceptCustom))] + public void NonCustomPlanTypesBelongToPlanArchetype(PlanType planType) + { + Assert.Contains(planType, PlanArchetypeArray(planType)); + } + + [Theory] + [MemberData(nameof(PlanTypesExceptCustom))] + public void PlanTypesBelongToOnlyOneArchetype(PlanType planType) + { + Console.WriteLine(planType); + Assert.Single(PlanArchetypeArray(planType)); + } + } +} From d08e9359af3c80bb43dd976387b1289605cb5467 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Fri, 29 Oct 2021 18:37:37 -0400 Subject: [PATCH 03/34] 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. --- test/Api.Test/Api.Test.csproj | 1 + .../AutoFixture/ControllerCustomization.cs | 38 +++++++++++++++++ .../Attributes/BitCustomizeAttribute.cs | 22 ++++++++++ .../BuilderWithoutAutoProperties.cs | 41 +++++++++++++++++++ 4 files changed, 102 insertions(+) create mode 100644 test/Api.Test/AutoFixture/ControllerCustomization.cs create mode 100644 test/Common/AutoFixture/Attributes/BitCustomizeAttribute.cs create mode 100644 test/Common/AutoFixture/BuilderWithoutAutoProperties.cs diff --git a/test/Api.Test/Api.Test.csproj b/test/Api.Test/Api.Test.csproj index e8c71e9f23..dd902a33d0 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/ControllerCustomization.cs b/test/Api.Test/AutoFixture/ControllerCustomization.cs new file mode 100644 index 0000000000..137cadb895 --- /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/Common/AutoFixture/Attributes/BitCustomizeAttribute.cs b/test/Common/AutoFixture/Attributes/BitCustomizeAttribute.cs new file mode 100644 index 0000000000..32910ef537 --- /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/Common/AutoFixture/BuilderWithoutAutoProperties.cs b/test/Common/AutoFixture/BuilderWithoutAutoProperties.cs new file mode 100644 index 0000000000..81df3206a0 --- /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); + } +} From 0cbeb75b29f848fcd03aef71df4f6357ea3485cf Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Fri, 29 Oct 2021 18:40:50 -0400 Subject: [PATCH 04/34] 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. --- .../Attributes/MemberAutoDataAttribute.cs | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 test/Common/AutoFixture/Attributes/MemberAutoDataAttribute.cs diff --git a/test/Common/AutoFixture/Attributes/MemberAutoDataAttribute.cs b/test/Common/AutoFixture/Attributes/MemberAutoDataAttribute.cs new file mode 100644 index 0000000000..cded9a26cc --- /dev/null +++ b/test/Common/AutoFixture/Attributes/MemberAutoDataAttribute.cs @@ -0,0 +1,69 @@ +using System; +using Xunit.Sdk; +using Xunit; +using AutoFixture; +using System.Reflection; +using System.Collections.Generic; +using AutoFixture.Xunit2; +using System.Linq; +using AutoFixture.Kernel; +using System.Collections; + +namespace Bit.Test.Common.AutoFixture.Attributes +{ + public class MemberAutoDataAttribute : MemberDataAttributeBase + { + private readonly Func _createFixture; + + public MemberAutoDataAttribute(string memberName, params object[] parameters) : + this(() => new Fixture(), memberName, parameters) + { } + + public MemberAutoDataAttribute(Func createFixture, string memberName, params object[] parameters) : + base(memberName, parameters) + { + _createFixture = createFixture; + } + + protected override object[] ConvertDataItem(MethodInfo testMethod, object item) + { + var methodParameters = testMethod.GetParameters(); + var classCustomizations = testMethod.DeclaringType.GetCustomAttributes().Select(attr => attr.GetCustomization()); + var methodCustomizations = testMethod.GetCustomAttributes().Select(attr => attr.GetCustomization()); + + var array = item as object[] ?? Array.Empty(); + + var fixture = ApplyCustomizations(ApplyCustomizations(_createFixture(), classCustomizations), methodCustomizations); + var missingParameters = methodParameters.Skip(array.Length).Select(p => CustomizeAndCreate(p, fixture)); + + return array.Concat(missingParameters).ToArray(); + } + + private 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); + } + + private 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; + } + } +} From b8fdbbcb9f2e044b8c5c6194df574d477ae6abf4 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Fri, 29 Oct 2021 18:43:45 -0400 Subject: [PATCH 05/34] WIP: Organization sponsorship flow --- .../OrganizationSponsorshipsController.cs | 136 ++++++++++++++++++ ...ganizationSponsorshipRedeemRequestModel.cs | 11 ++ .../OrganizationSponsorshipRequestModel.cs | 16 +++ .../Models/Table/OrganizationSponsorship.cs | 24 ++++ .../IOrganizationSponsorshipRepository.cs | 14 ++ .../OrganizationSponsorshipRepository.cs | 49 +++++++ .../IOrganizationSponsorshipService.cs | 7 + .../OrganizationSponsorshipService.cs | 14 ++ .../ControllerCustomizeAttribute.cs | 17 +++ ...OrganizationSponsorshipsControllerTests.cs | 40 ++++++ .../Attributes/SutAutoDataAttribute.cs | 5 + 11 files changed, 333 insertions(+) create mode 100644 src/Api/Controllers/OrganizationSponsorshipsController.cs 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/Table/OrganizationSponsorship.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 test/Api.Test/AutoFixture/Attributes/ControllerCustomizeAttribute.cs create mode 100644 test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs diff --git a/src/Api/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Controllers/OrganizationSponsorshipsController.cs new file mode 100644 index 0000000000..49d0557ea8 --- /dev/null +++ b/src/Api/Controllers/OrganizationSponsorshipsController.cs @@ -0,0 +1,136 @@ +using System; +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.Repositories; +using Bit.Core.Services; +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; + public OrganizationSponsorshipsController(IOrganizationSponsorshipService organizationSponsorshipService, + IOrganizationSponsorshipRepository organizationSponsorshipRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICurrentContext currentContext) + { + _organizationsSponsorshipService = organizationSponsorshipService; + _organizationSponsorshipRepository = organizationSponsorshipRepository; + _organizationRepository = organizationRepository; + _organizationUserRepository = organizationUserRepository; + _currentContext = currentContext; + } + + [HttpPost("{sponsoringOrgId}/families-for-enterprise")] + public async Task CreateSponsorship(string sponsoringOrgId, [FromBody] OrganizationSponsorshipRequestModel model) + { + // TODO: validate has right to sponsor, send sponsorship email + var sponsoringOrgIdGuid = new Guid(sponsoringOrgId); + var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgIdGuid); + if (sponsoringOrg == null || !PlanTypeHelper.HasEnterprisePlan(sponsoringOrg)) + { + throw new BadRequestException("Specified Organization cannot sponsor other organizations."); + } + + var sponsoringOrgUser = await _organizationUserRepository.GetByIdAsync(model.OrganizationUserId); + if (sponsoringOrgUser == null || sponsoringOrgUser.Status != OrganizationUserStatusType.Confirmed) + { + throw new BadRequestException("Only confirm 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"); + } + + // TODO: send sponsorship email + + throw new NotImplementedException(); + } + + [HttpPost("sponsored/redeem/families-for-enterprise")] + public async Task RedeemSponsorship([FromQuery] string sponsorshipInfo, [FromBody] OrganizationSponsorshipRedeemRequestModel model) + { + // TODO: parse out sponsorshipInfo + + if (!await _currentContext.OrganizationOwner(model.SponsoredOrganizationId)) + { + throw new BadRequestException("Can only redeem sponsorship for and organization you own"); + } + + var existingOrgSponsorship = await _organizationSponsorshipRepository.GetBySponsoredOrganizationIdAsync(model.SponsoredOrganizationId); + if (existingOrgSponsorship != null) + { + throw new BadRequestException("Cannot redeem a sponsorship offer for and organization that is already sponsored. Revoke existing sponsorship first."); + } + + var organizationToSponsor = await _organizationRepository.GetByIdAsync(model.SponsoredOrganizationId); + // TODO: only current families plan? + if (organizationToSponsor == null || !PlanTypeHelper.HasFamiliesPlan(organizationToSponsor)) + { + throw new BadRequestException("Can only redeem sponsorship offer on families organizations"); + } + + // TODO: check user is owner of proposed org, it isn't currently sponsored, and set up sponsorship + throw new NotImplementedException(); + } + + [HttpDelete("{sponsoringOrgId}/{sponsoringOrgUserId}")] + [HttpPost("{sponsoringOrgId}/{sponsoringOrgUserId}/delete")] + public async Task RevokeSponsorship(string sponsoringOrgId, string sponsoringOrgUserId) + { + var sponsoringOrgIdGuid = new Guid(sponsoringOrgId); + var sponsoringOrgUserIdGuid = new Guid(sponsoringOrgUserId); + + var orgUser = await _organizationUserRepository.GetByIdAsync(sponsoringOrgUserIdGuid); + if (_currentContext.UserId != orgUser?.UserId) + { + throw new BadRequestException("Can only revoke a sponsorship you own."); + } + + var existingOrgSponsorship = await _organizationSponsorshipRepository.GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUserIdGuid); + if (existingOrgSponsorship == null) + { + throw new BadRequestException("You are not currently sponsoring and organization."); + } + + // TODO: remove sponsorship + throw new NotImplementedException(); + } + + [HttpDelete("sponsored/{sponsoredOrgId}")] + [HttpPost("sponsored/{sponsoredOrgId}/remove")] + public async Task RemoveSponsorship(string sponsoredOrgId) + { + var sponsoredOrgIdGuid = new Guid(sponsoredOrgId); + + if (!await _currentContext.OrganizationOwner(sponsoredOrgIdGuid)) + { + throw new BadRequestException("Only the owner of an organization can remove sponsorship."); + } + + var existingOrgSponsorship = await _organizationSponsorshipRepository.GetBySponsoringOrganizationUserIdAsync(sponsoredOrgIdGuid); + if (existingOrgSponsorship == null) + { + throw new BadRequestException("The requested organization is not currently being sponsored"); + } + + // TODO: remove sponsorship + throw new NotImplementedException(); + } + } +} 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 0000000000..a23ac5f9a4 --- /dev/null +++ b/src/Core/Models/Api/Request/Organizations/OrganizationSponsorshipRedeemRequestModel.cs @@ -0,0 +1,11 @@ +using System; +using System.ComponentModel.DataAnnotations; + +namespace Bit.Core.Models.Api +{ + public class OrganizationSponsorshipRedeemRequestModel + { + [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 0000000000..1e8c8a9811 --- /dev/null +++ b/src/Core/Models/Api/Request/Organizations/OrganizationSponsorshipRequestModel.cs @@ -0,0 +1,16 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Bit.Core.Utilities; + +namespace Bit.Core.Models.Api.Request +{ + public class OrganizationSponsorshipRequestModel + { + [Required] + public Guid OrganizationUserId { get; set; } + [Required] + [StringLength(256)] + [StrictEmailAddress] + public string sponsoredEmail { get; set; } + } +} diff --git a/src/Core/Models/Table/OrganizationSponsorship.cs b/src/Core/Models/Table/OrganizationSponsorship.cs new file mode 100644 index 0000000000..3644f69dc1 --- /dev/null +++ b/src/Core/Models/Table/OrganizationSponsorship.cs @@ -0,0 +1,24 @@ +using System; +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 SponsoringUserId { get; set; } + public Guid? SponsoredOrganizationId { get; set; } + 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/IOrganizationSponsorshipRepository.cs b/src/Core/Repositories/IOrganizationSponsorshipRepository.cs new file mode 100644 index 0000000000..1d0fea79af --- /dev/null +++ b/src/Core/Repositories/IOrganizationSponsorshipRepository.cs @@ -0,0 +1,14 @@ +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); + } +} diff --git a/src/Core/Repositories/SqlServer/OrganizationSponsorshipRepository.cs b/src/Core/Repositories/SqlServer/OrganizationSponsorshipRepository.cs new file mode 100644 index 0000000000..604867a9fa --- /dev/null +++ b/src/Core/Repositories/SqlServer/OrganizationSponsorshipRepository.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +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(); + } + } + } +} diff --git a/src/Core/Services/IOrganizationSponsorshipService.cs b/src/Core/Services/IOrganizationSponsorshipService.cs new file mode 100644 index 0000000000..01ddf14f68 --- /dev/null +++ b/src/Core/Services/IOrganizationSponsorshipService.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Services +{ + public interface IOrganizationSponsorshipService + { + + } +} diff --git a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs new file mode 100644 index 0000000000..5cdc9a71aa --- /dev/null +++ b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs @@ -0,0 +1,14 @@ +using Bit.Core.Repositories; + +namespace Bit.Core.Services +{ + public class OrganizationSponsorshipService : IOrganizationSponsorshipService + { + private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository; + + public OrganizationSponsorshipService(IOrganizationSponsorshipRepository organizationSponsorshipRepository) + { + _organizationSponsorshipRepository = organizationSponsorshipRepository; + } + } +} diff --git a/test/Api.Test/AutoFixture/Attributes/ControllerCustomizeAttribute.cs b/test/Api.Test/AutoFixture/Attributes/ControllerCustomizeAttribute.cs new file mode 100644 index 0000000000..fa5f764b95 --- /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/Controllers/OrganizationSponsorshipsControllerTests.cs b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs new file mode 100644 index 0000000000..845a0f5f0e --- /dev/null +++ b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs @@ -0,0 +1,40 @@ +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; + +namespace Bit.Api.Test.Controllers +{ + [ControllerCustomize(typeof(OrganizationSponsorshipsController))] + [SutProviderCustomize] + public class OrganizationSponsorshipsControllerTests + { + public static IEnumerable EnterprisePlanTypes => + Enum.GetValues().Where(p => PlanTypeHelper.IsEnterprise(p)).Select(p => new object[] { p }); + public static IEnumerable NonEnterprisePlanTypes => + Enum.GetValues().Where(p => !PlanTypeHelper.IsEnterprise(p)).Select(p => new object[] { p }); + + [Theory] + [MemberAutoData(nameof(NonEnterprisePlanTypes))] + public async Task CreateSponsorship_BadSponsoringOrgPlan_ThrowsBadRequest(PlanType sponsoringOrgPlan, Organization org, + SutProvider sutProvider) + { + org.PlanType = sponsoringOrgPlan; + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.CreateSponsorship(org.Id.ToString(), null)); + + Assert.Contains("Specified Organization cannot sponsor other organizations.", exception.Message); + } + } +} diff --git a/test/Common/AutoFixture/Attributes/SutAutoDataAttribute.cs b/test/Common/AutoFixture/Attributes/SutAutoDataAttribute.cs index 0aa666dade..1c8bd089b2 100644 --- a/test/Common/AutoFixture/Attributes/SutAutoDataAttribute.cs +++ b/test/Common/AutoFixture/Attributes/SutAutoDataAttribute.cs @@ -6,6 +6,11 @@ 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( From dea366828b084b9d9831a5b4e93a354f312f5c13 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Sat, 30 Oct 2021 13:32:15 -0400 Subject: [PATCH 06/34] 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. --- .../Attributes/BitAutoDataAttribute.cs | 29 ++++++++++ .../Attributes/MemberAutoDataAttribute.cs | 54 ++---------------- .../Helpers/BitAutoDataAttributeHelpers.cs | 55 +++++++++++++++++++ 3 files changed, 90 insertions(+), 48 deletions(-) create mode 100644 test/Common/AutoFixture/Attributes/BitAutoDataAttribute.cs create mode 100644 test/Common/Helpers/BitAutoDataAttributeHelpers.cs diff --git a/test/Common/AutoFixture/Attributes/BitAutoDataAttribute.cs b/test/Common/AutoFixture/Attributes/BitAutoDataAttribute.cs new file mode 100644 index 0000000000..dfb06a254c --- /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/MemberAutoDataAttribute.cs b/test/Common/AutoFixture/Attributes/MemberAutoDataAttribute.cs index cded9a26cc..a154128ae0 100644 --- a/test/Common/AutoFixture/Attributes/MemberAutoDataAttribute.cs +++ b/test/Common/AutoFixture/Attributes/MemberAutoDataAttribute.cs @@ -1,69 +1,27 @@ using System; -using Xunit.Sdk; using Xunit; using AutoFixture; using System.Reflection; -using System.Collections.Generic; -using AutoFixture.Xunit2; using System.Linq; -using AutoFixture.Kernel; -using System.Collections; +using Bit.Test.Common.Helpers; namespace Bit.Test.Common.AutoFixture.Attributes { - public class MemberAutoDataAttribute : MemberDataAttributeBase + public class BitMemberAutoDataAttribute : MemberDataAttributeBase { private readonly Func _createFixture; - public MemberAutoDataAttribute(string memberName, params object[] parameters) : + public BitMemberAutoDataAttribute(string memberName, params object[] parameters) : this(() => new Fixture(), memberName, parameters) { } - public MemberAutoDataAttribute(Func createFixture, string memberName, params object[] parameters) : + public BitMemberAutoDataAttribute(Func createFixture, string memberName, params object[] parameters) : base(memberName, parameters) { _createFixture = createFixture; } - protected override object[] ConvertDataItem(MethodInfo testMethod, object item) - { - var methodParameters = testMethod.GetParameters(); - var classCustomizations = testMethod.DeclaringType.GetCustomAttributes().Select(attr => attr.GetCustomization()); - var methodCustomizations = testMethod.GetCustomAttributes().Select(attr => attr.GetCustomization()); - - var array = item as object[] ?? Array.Empty(); - - var fixture = ApplyCustomizations(ApplyCustomizations(_createFixture(), classCustomizations), methodCustomizations); - var missingParameters = methodParameters.Skip(array.Length).Select(p => CustomizeAndCreate(p, fixture)); - - return array.Concat(missingParameters).ToArray(); - } - - private 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); - } - - private 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; - } + protected override object[] ConvertDataItem(MethodInfo testMethod, object item) => + BitAutoDataAttributeHelpers.GetData(testMethod, _createFixture(), item as object[]).First(); } } diff --git a/test/Common/Helpers/BitAutoDataAttributeHelpers.cs b/test/Common/Helpers/BitAutoDataAttributeHelpers.cs new file mode 100644 index 0000000000..394a03ed86 --- /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[] fixedTestParamters) + { + var methodParameters = testMethod.GetParameters(); + var classCustomizations = testMethod.DeclaringType.GetCustomAttributes().Select(attr => attr.GetCustomization()); + var methodCustomizations = testMethod.GetCustomAttributes().Select(attr => attr.GetCustomization()); + + fixedTestParamters = fixedTestParamters ?? Array.Empty(); + + fixture = ApplyCustomizations(ApplyCustomizations(fixture, classCustomizations), methodCustomizations); + var missingParameters = methodParameters.Skip(fixedTestParamters.Length).Select(p => CustomizeAndCreate(p, fixture)); + + return new object[1][] { fixedTestParamters.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; + } + } +} From 0255a2ea15fd37684582fca04860dc3fbc684398 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Sat, 30 Oct 2021 13:34:03 -0400 Subject: [PATCH 07/34] WIP: scaffolding for families for enterprise sponsorship flow --- .../OrganizationSponsorshipsController.cs | 60 ++++--- .../Models/Table/OrganizationSponsorship.cs | 6 + .../IOrganizationSponsorshipRepository.cs | 1 + .../OrganizationSponsorshipRepository.cs | 22 ++- .../IOrganizationSponsorshipService.cs | 7 +- .../OrganizationSponsorshipService.cs | 22 +++ ...OrganizationSponsorshipsControllerTests.cs | 168 +++++++++++++++++- 7 files changed, 258 insertions(+), 28 deletions(-) diff --git a/src/Api/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Controllers/OrganizationSponsorshipsController.cs index 49d0557ea8..a24b74cef7 100644 --- a/src/Api/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Controllers/OrganizationSponsorshipsController.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Threading.Tasks; using Bit.Core.Context; using Bit.Core.Enums; @@ -35,7 +36,7 @@ namespace Bit.Api.Controllers } [HttpPost("{sponsoringOrgId}/families-for-enterprise")] - public async Task CreateSponsorship(string sponsoringOrgId, [FromBody] OrganizationSponsorshipRequestModel model) + public async Task CreateSponsorship(string sponsoringOrgId, [FromBody] OrganizationSponsorshipRequestModel model) { // TODO: validate has right to sponsor, send sponsorship email var sponsoringOrgIdGuid = new Guid(sponsoringOrgId); @@ -50,32 +51,45 @@ namespace Bit.Api.Controllers { throw new BadRequestException("Only confirm users can sponsor other organizations."); } + if (sponsoringOrgUser.UserId != _currentContext.UserId) + { + throw new BadRequestException("Can only create organization sponsorships for yourself."); + } var existingOrgSponsorship = await _organizationSponsorshipRepository.GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id); if (existingOrgSponsorship != null) { - throw new BadRequestException("Can only sponsor one organization per Organization User"); + throw new BadRequestException("Can only sponsor one organization per Organization User."); } - // TODO: send sponsorship email - - throw new NotImplementedException(); + await _organizationsSponsorshipService.OfferSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, model.sponsoredEmail); } [HttpPost("sponsored/redeem/families-for-enterprise")] - public async Task RedeemSponsorship([FromQuery] string sponsorshipInfo, [FromBody] OrganizationSponsorshipRedeemRequestModel model) + public async Task RedeemSponsorship([FromQuery] string sponsorshipInfo, [FromBody] OrganizationSponsorshipRedeemRequestModel model) { // TODO: parse out sponsorshipInfo if (!await _currentContext.OrganizationOwner(model.SponsoredOrganizationId)) { - throw new BadRequestException("Can only redeem sponsorship for and organization you own"); + throw new BadRequestException("Can only redeem sponsorship for an organization you own"); + } + var existingSponsorshipOffer = await _organizationSponsorshipRepository + .GetByOfferedToEmailAsync(_currentContext.User.Email); + if (existingSponsorshipOffer == null) + { + throw new BadRequestException("No unredeemed sponsorship offer exists for you."); } - var existingOrgSponsorship = await _organizationSponsorshipRepository.GetBySponsoredOrganizationIdAsync(model.SponsoredOrganizationId); + var existingOrgSponsorship = await _organizationSponsorshipRepository + .GetBySponsoredOrganizationIdAsync(model.SponsoredOrganizationId); if (existingOrgSponsorship != null) { - throw new BadRequestException("Cannot redeem a sponsorship offer for and organization that is already sponsored. Revoke existing sponsorship first."); + throw new BadRequestException("Cannot redeem a sponsorship offer for an organization that is already sponsored. Revoke existing sponsorship first."); + } + if (_currentContext.User.Email != existingOrgSponsorship.OfferedToEmail) + { + throw new BadRequestException("This sponsorship offer was issued to a different user email address."); } var organizationToSponsor = await _organizationRepository.GetByIdAsync(model.SponsoredOrganizationId); @@ -85,36 +99,34 @@ namespace Bit.Api.Controllers throw new BadRequestException("Can only redeem sponsorship offer on families organizations"); } - // TODO: check user is owner of proposed org, it isn't currently sponsored, and set up sponsorship - throw new NotImplementedException(); + await _organizationsSponsorshipService.SetUpSponsorshipAsync(existingSponsorshipOffer, organizationToSponsor); } - [HttpDelete("{sponsoringOrgId}/{sponsoringOrgUserId}")] - [HttpPost("{sponsoringOrgId}/{sponsoringOrgUserId}/delete")] - public async Task RevokeSponsorship(string sponsoringOrgId, string sponsoringOrgUserId) + [HttpDelete("{sponsoringOrgUserId}")] + [HttpPost("{sponsoringOrgUserId}/delete")] + public async Task RevokeSponsorship(string sponsoringOrgUserId) { - var sponsoringOrgIdGuid = new Guid(sponsoringOrgId); var sponsoringOrgUserIdGuid = new Guid(sponsoringOrgUserId); var orgUser = await _organizationUserRepository.GetByIdAsync(sponsoringOrgUserIdGuid); if (_currentContext.UserId != orgUser?.UserId) { - throw new BadRequestException("Can only revoke a sponsorship you own."); + throw new BadRequestException("Can only revoke a sponsorship you granted."); } - var existingOrgSponsorship = await _organizationSponsorshipRepository.GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUserIdGuid); + var existingOrgSponsorship = await _organizationSponsorshipRepository + .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUserIdGuid); if (existingOrgSponsorship == null) { throw new BadRequestException("You are not currently sponsoring and organization."); } - // TODO: remove sponsorship - throw new NotImplementedException(); + await _organizationsSponsorshipService.RemoveSponsorshipAsync(existingOrgSponsorship); } [HttpDelete("sponsored/{sponsoredOrgId}")] [HttpPost("sponsored/{sponsoredOrgId}/remove")] - public async Task RemoveSponsorship(string sponsoredOrgId) + public async Task RemoveSponsorship(string sponsoredOrgId) { var sponsoredOrgIdGuid = new Guid(sponsoredOrgId); @@ -123,14 +135,14 @@ namespace Bit.Api.Controllers throw new BadRequestException("Only the owner of an organization can remove sponsorship."); } - var existingOrgSponsorship = await _organizationSponsorshipRepository.GetBySponsoringOrganizationUserIdAsync(sponsoredOrgIdGuid); + var existingOrgSponsorship = await _organizationSponsorshipRepository + .GetBySponsoredOrganizationIdAsync(sponsoredOrgIdGuid); if (existingOrgSponsorship == null) { - throw new BadRequestException("The requested organization is not currently being sponsored"); + throw new BadRequestException("The requested organization is not currently being sponsored."); } - // TODO: remove sponsorship - throw new NotImplementedException(); + await _organizationsSponsorshipService.RemoveSponsorshipAsync(existingOrgSponsorship); } } } diff --git a/src/Core/Models/Table/OrganizationSponsorship.cs b/src/Core/Models/Table/OrganizationSponsorship.cs index 3644f69dc1..33b5b8adfa 100644 --- a/src/Core/Models/Table/OrganizationSponsorship.cs +++ b/src/Core/Models/Table/OrganizationSponsorship.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel.DataAnnotations; using Bit.Core.Utilities; namespace Bit.Core.Models.Table @@ -7,10 +8,15 @@ namespace Bit.Core.Models.Table { public Guid Id { get; set; } public Guid InstallationId { get; set; } + [Required] public Guid SponsoringOrganizationId { get; set; } + [Required] public Guid SponsoringOrganizationUserId { get; set; } public Guid SponsoringUserId { get; set; } + [MaxLength(256)] + public string OfferedToEmail { get; set; } public Guid? SponsoredOrganizationId { get; set; } + [Required] public bool CloudSponsor { get; set; } public DateTime? LastSyncDate { get; set; } public byte TimesRenewedWithoutValidation { get; set; } diff --git a/src/Core/Repositories/IOrganizationSponsorshipRepository.cs b/src/Core/Repositories/IOrganizationSponsorshipRepository.cs index 1d0fea79af..9d81cae919 100644 --- a/src/Core/Repositories/IOrganizationSponsorshipRepository.cs +++ b/src/Core/Repositories/IOrganizationSponsorshipRepository.cs @@ -10,5 +10,6 @@ namespace Bit.Core.Repositories { 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 index 604867a9fa..c759e906b8 100644 --- a/src/Core/Repositories/SqlServer/OrganizationSponsorshipRepository.cs +++ b/src/Core/Repositories/SqlServer/OrganizationSponsorshipRepository.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Data; using System.Data.SqlClient; using System.Linq; @@ -26,7 +25,10 @@ namespace Bit.Core.Repositories.SqlServer { var results = await connection.QueryAsync( "[dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationUserId]", - new { SponsoringOrganizationUserId = sponsoringOrganizationUserId }, + new + { + SponsoringOrganizationUserId = sponsoringOrganizationUserId + }, commandType: CommandType.StoredProcedure); return results.SingleOrDefault(); @@ -45,5 +47,21 @@ namespace Bit.Core.Repositories.SqlServer 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/IOrganizationSponsorshipService.cs b/src/Core/Services/IOrganizationSponsorshipService.cs index 01ddf14f68..070b982260 100644 --- a/src/Core/Services/IOrganizationSponsorshipService.cs +++ b/src/Core/Services/IOrganizationSponsorshipService.cs @@ -1,7 +1,12 @@ +using System.Threading.Tasks; +using Bit.Core.Models.Table; + namespace Bit.Core.Services { public interface IOrganizationSponsorshipService { - + Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, string sponsoredEmail); + Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship, Organization sponsoredOrganization); + Task RemoveSponsorshipAsync(OrganizationSponsorship sponsorship); } } diff --git a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs index 5cdc9a71aa..9a9f1213f7 100644 --- a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs +++ b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs @@ -1,3 +1,6 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Models.Table; using Bit.Core.Repositories; namespace Bit.Core.Services @@ -10,5 +13,24 @@ namespace Bit.Core.Services { _organizationSponsorshipRepository = organizationSponsorshipRepository; } + + public async Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, string sponsoredEmail) + { + // TODO: send sponsorship email, update sponsorship with offered email + throw new NotImplementedException(); + } + + public async Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship, Organization sponsoredOrganization) + { + // TODO: set up sponsorship + throw new NotImplementedException(); + } + + public async Task RemoveSponsorshipAsync(OrganizationSponsorship sponsorship) + { + // TODO: remove sponsorship + throw new NotImplementedException(); + } + } } diff --git a/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs index 845a0f5f0e..a2231d1efa 100644 --- a/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs +++ b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs @@ -12,6 +12,9 @@ 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; namespace Bit.Api.Test.Controllers { @@ -25,16 +28,179 @@ namespace Bit.Api.Test.Controllers Enum.GetValues().Where(p => !PlanTypeHelper.IsEnterprise(p)).Select(p => new object[] { p }); [Theory] - [MemberAutoData(nameof(NonEnterprisePlanTypes))] + [BitMemberAutoData(nameof(NonEnterprisePlanTypes))] public async Task CreateSponsorship_BadSponsoringOrgPlan_ThrowsBadRequest(PlanType sponsoringOrgPlan, Organization org, SutProvider sutProvider) { org.PlanType = sponsoringOrgPlan; + sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.CreateSponsorship(org.Id.ToString(), null)); Assert.Contains("Specified Organization cannot sponsor other organizations.", exception.Message); + await sutProvider.GetDependency() + .OfferSponsorshipAsync(default, default, default) + .DidNotReceiveWithAnyArgs(); + } + + public static IEnumerable NonConfirmedOrganizationUsersStatuses => + Enum.GetValues() + .Where(s => s != OrganizationUserStatusType.Confirmed) + .Select(s => new object[] { s }); + + [Theory] + [BitMemberAutoData(nameof(NonConfirmedOrganizationUsersStatuses))] + public async Task CreateSponsorship_BadSponsoringUserStatus_ThrowsBadRequest( + OrganizationUserStatusType statusType, Guid userId, Organization org, OrganizationUser orgUser, + OrganizationSponsorshipRequestModel model, SutProvider sutProvider) + { + org.PlanType = PlanType.EnterpriseAnnually; + orgUser.Status = statusType; + orgUser.UserId = userId; + model.OrganizationUserId = orgUser.Id; + + sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetByIdAsync(orgUser.Id).Returns(orgUser); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.CreateSponsorship(org.Id.ToString(), model)); + + Assert.Contains("Only confirm users can sponsor other organizations.", exception.Message); + await sutProvider.GetDependency() + .OfferSponsorshipAsync(default, default, default) + .DidNotReceiveWithAnyArgs(); + } + + [Theory] + [BitAutoData("c56c7ab4-a174-412a-a822-abe53ea71d50")] + public async Task CreateSponsorship_CreateSponsorshipAsDifferentUser_ThrowsBadRequest(Guid userId, + Organization org, OrganizationUser orgUser, OrganizationSponsorshipRequestModel model, + SutProvider sutProvider) + { + org.PlanType = PlanType.EnterpriseAnnually; + orgUser.Status = OrganizationUserStatusType.Confirmed; + model.OrganizationUserId = orgUser.Id; + + sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); + sutProvider.GetDependency().UserId.Returns(userId); + sutProvider.GetDependency().GetByIdAsync(orgUser.Id).Returns(orgUser); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.CreateSponsorship(org.Id.ToString(), model)); + + Assert.Contains("Can only create organization sponsorships for yourself.", exception.Message); + await sutProvider.GetDependency() + .OfferSponsorshipAsync(default, default, default) + .DidNotReceiveWithAnyArgs(); + } + + [Theory] + [BitAutoData] + public async Task CreateSponsorship_AlreadySponsoring_ThrowsBadRequest(Organization org, + OrganizationUser orgUser, OrganizationSponsorship sponsorship, + OrganizationSponsorshipRequestModel model, SutProvider sutProvider) + { + org.PlanType = PlanType.EnterpriseAnnually; + orgUser.Status = OrganizationUserStatusType.Confirmed; + model.OrganizationUserId = orgUser.Id; + + sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); + sutProvider.GetDependency().UserId.Returns(orgUser.UserId); + sutProvider.GetDependency().GetByIdAsync(orgUser.Id).Returns(orgUser); + sutProvider.GetDependency() + .GetBySponsoringOrganizationUserIdAsync(orgUser.Id).Returns(sponsorship); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.CreateSponsorship(org.Id.ToString(), model)); + + Assert.Contains("Can only sponsor one organization per Organization User.", exception.Message); + await sutProvider.GetDependency() + .OfferSponsorshipAsync(default, default, default) + .DidNotReceiveWithAnyArgs(); + } + + // TODO: Test redeem sponsorship + + [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.ToString())); + + Assert.Contains("Can only revoke a sponsorship you granted.", exception.Message); + await sutProvider.GetDependency() + .RemoveSponsorshipAsync(default) + .DidNotReceiveWithAnyArgs(); + } + + [Theory] + [BitAutoData] + public async Task RevokeSponsorship_NoExistingSponsorship_ThrowsBadRequest(OrganizationUser sponsoringOrgUser, + OrganizationSponsorship sponsorship, SutProvider sutProvider) + { + sutProvider.GetDependency().UserId.Returns(sponsoringOrgUser.UserId); + sutProvider.GetDependency().GetByIdAsync(sponsoringOrgUser.Id) + .Returns(sponsoringOrgUser); + sutProvider.GetDependency() + .GetBySponsoringOrganizationUserIdAsync(Arg.Is(v => v != sponsoringOrgUser.Id)) + .Returns(sponsorship); + sutProvider.GetDependency() + .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id) + .Returns((OrganizationSponsorship)null); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RevokeSponsorship(sponsoringOrgUser.Id.ToString())); + + Assert.Contains("You are not currently sponsoring and organization.", exception.Message); + await sutProvider.GetDependency() + .RemoveSponsorshipAsync(default) + .DidNotReceiveWithAnyArgs(); + } + + [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.ToString())); + + Assert.Contains("Only the owner of an organization can remove sponsorship.", exception.Message); + await sutProvider.GetDependency() + .RemoveSponsorshipAsync(default) + .DidNotReceiveWithAnyArgs(); + } + + [Theory] + [BitAutoData] + public async Task RemoveSponsorship_NotSponsored_ThrowsBadRequest(Organization sponsoredOrg, + OrganizationSponsorship sponsorship, SutProvider sutProvider) + { + sutProvider.GetDependency().OrganizationOwner(Arg.Any()).Returns(true); + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id) + .Returns((OrganizationSponsorship)null); + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(Arg.Is(v => v != sponsoredOrg.Id)) + .Returns(sponsorship); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RemoveSponsorship(sponsoredOrg.Id.ToString())); + + Assert.Contains("The requested organization is not currently being sponsored.", exception.Message); + await sutProvider.GetDependency() + .RemoveSponsorshipAsync(default) + .DidNotReceiveWithAnyArgs(); } } } From aefcbcbe7d095fbcca8f759e68f0069c45f34fa3 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Sat, 30 Oct 2021 13:47:21 -0400 Subject: [PATCH 08/34] Fix broken tests --- .../Services/ProviderServiceTests.cs | 4 +-- ...OrganizationSponsorshipsControllerTests.cs | 32 +++++++++---------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/bitwarden_license/test/CmmCore.Test/Services/ProviderServiceTests.cs b/bitwarden_license/test/CmmCore.Test/Services/ProviderServiceTests.cs index a46d854432..b10c249269 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/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs index a2231d1efa..32e3e3f666 100644 --- a/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs +++ b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs @@ -40,8 +40,8 @@ namespace Bit.Api.Test.Controllers Assert.Contains("Specified Organization cannot sponsor other organizations.", exception.Message); await sutProvider.GetDependency() - .OfferSponsorshipAsync(default, default, default) - .DidNotReceiveWithAnyArgs(); + .DidNotReceiveWithAnyArgs() + .OfferSponsorshipAsync(default, default, default); } public static IEnumerable NonConfirmedOrganizationUsersStatuses => @@ -69,8 +69,8 @@ namespace Bit.Api.Test.Controllers Assert.Contains("Only confirm users can sponsor other organizations.", exception.Message); await sutProvider.GetDependency() - .OfferSponsorshipAsync(default, default, default) - .DidNotReceiveWithAnyArgs(); + .DidNotReceiveWithAnyArgs() + .OfferSponsorshipAsync(default, default, default); } [Theory] @@ -92,8 +92,8 @@ namespace Bit.Api.Test.Controllers Assert.Contains("Can only create organization sponsorships for yourself.", exception.Message); await sutProvider.GetDependency() - .OfferSponsorshipAsync(default, default, default) - .DidNotReceiveWithAnyArgs(); + .DidNotReceiveWithAnyArgs() + .OfferSponsorshipAsync(default, default, default); } [Theory] @@ -117,8 +117,8 @@ namespace Bit.Api.Test.Controllers Assert.Contains("Can only sponsor one organization per Organization User.", exception.Message); await sutProvider.GetDependency() - .OfferSponsorshipAsync(default, default, default) - .DidNotReceiveWithAnyArgs(); + .DidNotReceiveWithAnyArgs() + .OfferSponsorshipAsync(default, default, default); } // TODO: Test redeem sponsorship @@ -137,8 +137,8 @@ namespace Bit.Api.Test.Controllers Assert.Contains("Can only revoke a sponsorship you granted.", exception.Message); await sutProvider.GetDependency() - .RemoveSponsorshipAsync(default) - .DidNotReceiveWithAnyArgs(); + .DidNotReceiveWithAnyArgs() + .RemoveSponsorshipAsync(default); } [Theory] @@ -161,8 +161,8 @@ namespace Bit.Api.Test.Controllers Assert.Contains("You are not currently sponsoring and organization.", exception.Message); await sutProvider.GetDependency() - .RemoveSponsorshipAsync(default) - .DidNotReceiveWithAnyArgs(); + .DidNotReceiveWithAnyArgs() + .RemoveSponsorshipAsync(default); } [Theory] @@ -177,8 +177,8 @@ namespace Bit.Api.Test.Controllers Assert.Contains("Only the owner of an organization can remove sponsorship.", exception.Message); await sutProvider.GetDependency() - .RemoveSponsorshipAsync(default) - .DidNotReceiveWithAnyArgs(); + .DidNotReceiveWithAnyArgs() + .RemoveSponsorshipAsync(default); } [Theory] @@ -199,8 +199,8 @@ namespace Bit.Api.Test.Controllers Assert.Contains("The requested organization is not currently being sponsored.", exception.Message); await sutProvider.GetDependency() - .RemoveSponsorshipAsync(default) - .DidNotReceiveWithAnyArgs(); + .DidNotReceiveWithAnyArgs() + .RemoveSponsorshipAsync(default); } } } From ee46a6d63bc20e0472a396167f0bdc2fb1402258 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Thu, 4 Nov 2021 08:25:40 -0500 Subject: [PATCH 09/34] Create sponsorship offer (#1688) --- .../OrganizationSponsorshipsController.cs | 23 ++- .../IOrganizationSponsorshipService.cs | 1 + .../OrganizationSponsorshipService.cs | 72 +++++++++- ...OrganizationSponsorshipsControllerTests.cs | 136 +++++++++++++++++- test/Common/Helpers/AssertHelper.cs | 58 ++++++++ .../OrganizationSponsorshipServiceTests.cs | 74 ++++++++++ 6 files changed, 351 insertions(+), 13 deletions(-) create mode 100644 test/Common/Helpers/AssertHelper.cs create mode 100644 test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs diff --git a/src/Api/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Controllers/OrganizationSponsorshipsController.cs index a24b74cef7..fde4f938e6 100644 --- a/src/Api/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Controllers/OrganizationSponsorshipsController.cs @@ -8,6 +8,7 @@ using Bit.Core.Models.Api; using Bit.Core.Models.Api.Request; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -36,6 +37,7 @@ namespace Bit.Api.Controllers } [HttpPost("{sponsoringOrgId}/families-for-enterprise")] + [SelfHosted(NotSelfHostedOnly = true)] public async Task CreateSponsorship(string sponsoringOrgId, [FromBody] OrganizationSponsorshipRequestModel model) { // TODO: validate has right to sponsor, send sponsorship email @@ -66,13 +68,18 @@ namespace Bit.Api.Controllers } [HttpPost("sponsored/redeem/families-for-enterprise")] - public async Task RedeemSponsorship([FromQuery] string sponsorshipInfo, [FromBody] OrganizationSponsorshipRedeemRequestModel model) + [SelfHosted(NotSelfHostedOnly = true)] + public async Task RedeemSponsorship([FromQuery] string sponsorshipToken, [FromBody] OrganizationSponsorshipRedeemRequestModel model) { // TODO: parse out sponsorshipInfo + 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"); + throw new BadRequestException("Can only redeem sponsorship for an organization you own."); } var existingSponsorshipOffer = await _organizationSponsorshipRepository .GetByOfferedToEmailAsync(_currentContext.User.Email); @@ -80,6 +87,10 @@ namespace Bit.Api.Controllers { throw new BadRequestException("No unredeemed sponsorship offer exists for you."); } + if (_currentContext.User.Email != existingSponsorshipOffer.OfferedToEmail) + { + throw new BadRequestException("This sponsorship offer was issued to a different user email address."); + } var existingOrgSponsorship = await _organizationSponsorshipRepository .GetBySponsoredOrganizationIdAsync(model.SponsoredOrganizationId); @@ -87,16 +98,12 @@ namespace Bit.Api.Controllers { throw new BadRequestException("Cannot redeem a sponsorship offer for an organization that is already sponsored. Revoke existing sponsorship first."); } - if (_currentContext.User.Email != existingOrgSponsorship.OfferedToEmail) - { - throw new BadRequestException("This sponsorship offer was issued to a different user email address."); - } var organizationToSponsor = await _organizationRepository.GetByIdAsync(model.SponsoredOrganizationId); // TODO: only current families plan? if (organizationToSponsor == null || !PlanTypeHelper.HasFamiliesPlan(organizationToSponsor)) { - throw new BadRequestException("Can only redeem sponsorship offer on families organizations"); + throw new BadRequestException("Can only redeem sponsorship offer on families organizations."); } await _organizationsSponsorshipService.SetUpSponsorshipAsync(existingSponsorshipOffer, organizationToSponsor); @@ -104,6 +111,7 @@ namespace Bit.Api.Controllers [HttpDelete("{sponsoringOrgUserId}")] [HttpPost("{sponsoringOrgUserId}/delete")] + [SelfHosted(NotSelfHostedOnly = true)] public async Task RevokeSponsorship(string sponsoringOrgUserId) { var sponsoringOrgUserIdGuid = new Guid(sponsoringOrgUserId); @@ -126,6 +134,7 @@ namespace Bit.Api.Controllers [HttpDelete("sponsored/{sponsoredOrgId}")] [HttpPost("sponsored/{sponsoredOrgId}/remove")] + [SelfHosted(NotSelfHostedOnly = true)] public async Task RemoveSponsorship(string sponsoredOrgId) { var sponsoredOrgIdGuid = new Guid(sponsoredOrgId); diff --git a/src/Core/Services/IOrganizationSponsorshipService.cs b/src/Core/Services/IOrganizationSponsorshipService.cs index 070b982260..ade55ac982 100644 --- a/src/Core/Services/IOrganizationSponsorshipService.cs +++ b/src/Core/Services/IOrganizationSponsorshipService.cs @@ -5,6 +5,7 @@ namespace Bit.Core.Services { public interface IOrganizationSponsorshipService { + Task ValidateRedemptionTokenAsync(string encryptedToken); Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, string sponsoredEmail); Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship, Organization sponsoredOrganization); Task RemoveSponsorshipAsync(OrganizationSponsorship sponsorship); diff --git a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs index 9a9f1213f7..b49bbd2f3b 100644 --- a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs +++ b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs @@ -2,27 +2,89 @@ using System; using System.Threading.Tasks; using Bit.Core.Models.Table; using Bit.Core.Repositories; +using Microsoft.AspNetCore.DataProtection; namespace Bit.Core.Services { public class OrganizationSponsorshipService : IOrganizationSponsorshipService { - private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository; + private const string FamiliesForEnterpriseTokenName = "FamiliesForEnterpriseToken"; + private const string TokenClearTextPrefix = "BWOrganizationSponsorship_"; - public OrganizationSponsorshipService(IOrganizationSponsorshipRepository organizationSponsorshipRepository) + private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository; + private readonly IDataProtector _dataProtector; + + public OrganizationSponsorshipService(IOrganizationSponsorshipRepository organizationSponsorshipRepository, + IDataProtector dataProtector) { _organizationSponsorshipRepository = organizationSponsorshipRepository; + _dataProtector = dataProtector; } + public async Task ValidateRedemptionTokenAsync(string encryptedToken) + { + if (!encryptedToken.StartsWith(TokenClearTextPrefix)) + { + return false; + } + + var decryptedToken = _dataProtector.Unprotect(encryptedToken); + var dataParts = decryptedToken.Split(' '); + + if (dataParts.Length != 2) + { + return false; + } + + if (dataParts[0].Equals(FamiliesForEnterpriseTokenName)) + { + if (!Guid.TryParse(dataParts[1], out Guid sponsorshipId)) + { + return false; + } + + var sponsorship = await _organizationSponsorshipRepository.GetByIdAsync(sponsorshipId); + return sponsorship != null; + } + + return false; + } + + private string RedemptionToken(Guid sponsorshipId) => + string.Concat( + TokenClearTextPrefix, + _dataProtector.Protect($"{FamiliesForEnterpriseTokenName} {sponsorshipId}") + ); + public async Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, string sponsoredEmail) { - // TODO: send sponsorship email, update sponsorship with offered email - throw new NotImplementedException(); + var sponsorship = new OrganizationSponsorship + { + SponsoringOrganizationId = sponsoringOrg.Id, + SponsoringOrganizationUserId = sponsoringOrgUser.Id, + OfferedToEmail = sponsoredEmail, + CloudSponsor = true, + }; + + try + { + sponsorship = await _organizationSponsorshipRepository.CreateAsync(sponsorship); + + // TODO: send email to sponsoredEmail w/ redemption token link + } + catch + { + if (sponsorship.Id != default) + { + await _organizationSponsorshipRepository.DeleteAsync(sponsorship); + } + throw; + } } public async Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship, Organization sponsoredOrganization) { - // TODO: set up sponsorship + // TODO: set up sponsorship, remember remove offeredToEmail from sponsorship throw new NotImplementedException(); } diff --git a/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs index 32e3e3f666..d9dd59e64f 100644 --- a/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs +++ b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs @@ -15,6 +15,7 @@ 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; namespace Bit.Api.Test.Controllers { @@ -26,6 +27,8 @@ namespace Bit.Api.Test.Controllers Enum.GetValues().Where(p => PlanTypeHelper.IsEnterprise(p)).Select(p => new object[] { p }); public static IEnumerable NonEnterprisePlanTypes => Enum.GetValues().Where(p => !PlanTypeHelper.IsEnterprise(p)).Select(p => new object[] { p }); + public static IEnumerable NonFamiliesPlanTypes => + Enum.GetValues().Where(p => !PlanTypeHelper.IsFamilies(p)).Select(p => new object[] { p }); [Theory] [BitMemberAutoData(nameof(NonEnterprisePlanTypes))] @@ -121,7 +124,138 @@ namespace Bit.Api.Test.Controllers .OfferSponsorshipAsync(default, default, default); } - // TODO: Test redeem sponsorship + [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 RedeemSponsorship_SponsorshipNotFound_ThrowsBadRequest(string sponsorshipToken, + OrganizationSponsorshipRedeemRequestModel model, User user, + SutProvider sutProvider) + { + sutProvider.GetDependency().ValidateRedemptionTokenAsync(sponsorshipToken) + .Returns(true); + sutProvider.GetDependency().OrganizationOwner(model.SponsoredOrganizationId).Returns(true); + sutProvider.GetDependency().User.Returns(user); + sutProvider.GetDependency().GetByOfferedToEmailAsync(user.Email) + .Returns((OrganizationSponsorship)null); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RedeemSponsorship(sponsorshipToken, model)); + + Assert.Contains("No unredeemed sponsorship offer exists for you.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SetUpSponsorshipAsync(default, default); + } + + [Theory] + [BitAutoData] + public async Task RedeemSponsorship_OfferedToDifferentEmail_ThrowsBadRequest(string sponsorshipToken, + OrganizationSponsorshipRedeemRequestModel model, User user, OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + sutProvider.GetDependency().ValidateRedemptionTokenAsync(sponsorshipToken) + .Returns(true); + sutProvider.GetDependency().OrganizationOwner(model.SponsoredOrganizationId).Returns(true); + sutProvider.GetDependency().User.Returns(user); + sutProvider.GetDependency().GetByOfferedToEmailAsync(user.Email) + .Returns(sponsorship); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RedeemSponsorship(sponsorshipToken, model)); + + Assert.Contains("This sponsorship offer was issued to a different user email address.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SetUpSponsorshipAsync(default, default); + } + + [Theory] + [BitAutoData] + public async Task RedeemSponsorship_OrgAlreadySponsored_ThrowsBadRequest(string sponsorshipToken, + OrganizationSponsorshipRedeemRequestModel model, User user, OrganizationSponsorship sponsorship, + OrganizationSponsorship existingSponsorship, SutProvider sutProvider) + { + user.Email = sponsorship.OfferedToEmail; + + sutProvider.GetDependency().ValidateRedemptionTokenAsync(sponsorshipToken) + .Returns(true); + sutProvider.GetDependency().OrganizationOwner(model.SponsoredOrganizationId).Returns(true); + sutProvider.GetDependency().User.Returns(user); + sutProvider.GetDependency() + .GetByOfferedToEmailAsync(sponsorship.OfferedToEmail).Returns(sponsorship); + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(model.SponsoredOrganizationId).Returns(existingSponsorship); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RedeemSponsorship(sponsorshipToken, model)); + + Assert.Contains("Cannot redeem a sponsorship offer for an organization that is already sponsored. Revoke existing sponsorship first.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SetUpSponsorshipAsync(default, default); + } + + [Theory] + [BitAutoData] + public async Task RedeemSponsorship_OrgNotFamiles_ThrowsBadRequest(PlanType planType, string sponsorshipToken, + OrganizationSponsorshipRedeemRequestModel model, User user, OrganizationSponsorship sponsorship, + Organization org, SutProvider sutProvider) + { + user.Email = sponsorship.OfferedToEmail; + org.PlanType = planType; + + sutProvider.GetDependency().ValidateRedemptionTokenAsync(sponsorshipToken) + .Returns(true); + sutProvider.GetDependency().OrganizationOwner(model.SponsoredOrganizationId).Returns(true); + sutProvider.GetDependency().User.Returns(user); + sutProvider.GetDependency() + .GetByOfferedToEmailAsync(sponsorship.OfferedToEmail).Returns(sponsorship); + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(model.SponsoredOrganizationId).Returns((OrganizationSponsorship)null); + sutProvider.GetDependency().GetByIdAsync(model.SponsoredOrganizationId).Returns(org); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RedeemSponsorship(sponsorshipToken, model)); + + Assert.Contains("Can only redeem sponsorship offer on families organizations.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SetUpSponsorshipAsync(default, default); + } [Theory] [BitAutoData] diff --git a/test/Common/Helpers/AssertHelper.cs b/test/Common/Helpers/AssertHelper.cs new file mode 100644 index 0000000000..fc94fb2305 --- /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 expectedPi in expected.GetType().GetProperties().Where(pi => !relevantExcludedProperties.Contains(pi.Name))) + { + var actualPi = actual.GetType().GetProperty(expectedPi.Name); + + if (actualPi == null) + { + var settings = new JsonSerializerSettings { Formatting = Formatting.Indented }; + throw new Exception(string.Concat($"Expected actual object to contain a property named {expectedPi.Name}, but it does not\n", + $"Expected:\n{JsonConvert.SerializeObject(expected, settings)}\n", + $"Actual:\n{JsonConvert.SerializeObject(actual, new JsonSerializerSettings { Formatting = Formatting.Indented })}")); + } + + if (expectedPi.PropertyType == typeof(string) || expectedPi.PropertyType.IsValueType) + { + Assert.Equal(expectedPi.GetValue(expected), actualPi.GetValue(actual)); + } + else + { + var prefix = $"{expectedPi.PropertyType.Name}."; + var nextExcludedProperties = excludedPropertyStrings.Where(name => name.StartsWith(prefix)) + .Select(name => name[prefix.Length..]).ToArray(); + AssertPropertyEqual(expectedPi.GetValue(expected), actualPi.GetValue(actual), nextExcludedProperties); + } + } + } + + public static Predicate AssertEqualExpectedPredicate(T expected) => (actual) => + { + Assert.Equal(expected, actual); + return true; + }; + } +} diff --git a/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs b/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs new file mode 100644 index 0000000000..3588345fa7 --- /dev/null +++ b/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs @@ -0,0 +1,74 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Models.Table; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using Microsoft.IdentityModel.Tokens; +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; + } + } + + [Theory] + [BitAutoData] + public async Task OfferSponsorship_CreatesSponsorship(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, + string sponsoredEmail, SutProvider sutProvider) + { + await sutProvider.Sut.OfferSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, sponsoredEmail); + + var expectedSponsorship = new OrganizationSponsorship + { + SponsoringOrganizationId = sponsoringOrg.Id, + SponsoringOrganizationUserId = sponsoringOrgUser.Id, + OfferedToEmail = sponsoredEmail, + CloudSponsor = true, + }; + + await sutProvider.GetDependency().Received(1) + .CreateAsync(Arg.Is(s => sponsorshipValidator(s, expectedSponsorship))); + // TODO: Validate email called with appropriate token.s + } + + [Theory] + [BitAutoData] + public async Task OfferSponsorship_CreateSponsorshipThrows_RevertsDatabase(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, + string sponsoredEmail, SutProvider sutProvider) + { + 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, sponsoredEmail)); + Assert.Same(expectedException, actualException); + + await sutProvider.GetDependency().Received(1) + .DeleteAsync(createdSponsorship); + } + } +} From 079adc60b6e8ddb425dcddf1480c71fc1b8c80be Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Thu, 4 Nov 2021 10:46:49 -0500 Subject: [PATCH 10/34] Initial db work (#1687) * Add organization sponsorship databases to all providers * Generalize create and update for database, specialize in code --- .../OrganizationSponsorship.cs | 20 + .../Models/Table/OrganizationSponsorship.cs | 5 +- .../EntityFramework/DatabaseContext.cs | 4 + .../OrganizationSponsorshipRepository.cs | 51 + .../Utilities/ServiceCollectionExtensions.cs | 2 + .../OrganizationSponsorship_Create.sql | 43 + .../OrganizationSponsorship_DeleteById.sql | 17 + .../OrganizationSponsorship_ReadById.sql | 14 + ...zationSponsorship_ReadByOfferedToEmail.sql | 14 + ...nsorship_ReadBySponsoredOrganizationId.sql | 14 + ...ship_ReadBySponsoringOrganiationUserId.sql | 14 + .../OrganizationSponsorship_Update.sql | 31 + .../dbo/Tables/OrganizationSponsorship.sql | 41 + .../EntityFrameworkRepositoryFixtures.cs | 1 + .../OrganizationSponsorshipFixtures.cs | 59 + .../OrganizationSponsorshipCompare.cs | 26 + .../OrganizationSponsorshipRepositoryTests.cs | 138 ++ .../2021-11-02_00_OrganizationSponsorship.sql | 286 +++ ...213543_OrganizationSponsorship.Designer.cs | 1562 ++++++++++++++++ .../20211102213543_OrganizationSponsorship.cs | 83 + .../DatabaseContextModelSnapshot.cs | 70 + .../2021-11-02_00_OrganizationSponsorship.sql | 31 + ...205745_OrganizationSponsorship.Designer.cs | 1571 +++++++++++++++++ .../20211102205745_OrganizationSponsorship.cs | 81 + .../DatabaseContextModelSnapshot.cs | 70 + ...2021-11-02_00_OrganizationSponsorship.psql | 31 + 26 files changed, 4276 insertions(+), 3 deletions(-) create mode 100644 src/Core/Models/EntityFramework/OrganizationSponsorship.cs create mode 100644 src/Core/Repositories/EntityFramework/OrganizationSponsorshipRepository.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_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/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 util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql create mode 100644 util/MySqlMigrations/Migrations/20211102213543_OrganizationSponsorship.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20211102213543_OrganizationSponsorship.cs create mode 100644 util/MySqlMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.sql create mode 100644 util/PostgresMigrations/Migrations/20211102205745_OrganizationSponsorship.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20211102205745_OrganizationSponsorship.cs create mode 100644 util/PostgresMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.psql diff --git a/src/Core/Models/EntityFramework/OrganizationSponsorship.cs b/src/Core/Models/EntityFramework/OrganizationSponsorship.cs new file mode 100644 index 0000000000..53dcd3e9ed --- /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/Table/OrganizationSponsorship.cs b/src/Core/Models/Table/OrganizationSponsorship.cs index 33b5b8adfa..7966d8a090 100644 --- a/src/Core/Models/Table/OrganizationSponsorship.cs +++ b/src/Core/Models/Table/OrganizationSponsorship.cs @@ -7,15 +7,14 @@ namespace Bit.Core.Models.Table public class OrganizationSponsorship : ITableObject { public Guid Id { get; set; } - public Guid InstallationId { get; set; } + public Guid? InstallationId { get; set; } [Required] public Guid SponsoringOrganizationId { get; set; } [Required] public Guid SponsoringOrganizationUserId { get; set; } - public Guid SponsoringUserId { get; set; } + public Guid? SponsoredOrganizationId { get; set; } [MaxLength(256)] public string OfferedToEmail { get; set; } - public Guid? SponsoredOrganizationId { get; set; } [Required] public bool CloudSponsor { get; set; } public DateTime? LastSyncDate { get; set; } diff --git a/src/Core/Repositories/EntityFramework/DatabaseContext.cs b/src/Core/Repositories/EntityFramework/DatabaseContext.cs index fcc8275f99..d352e69803 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/OrganizationSponsorshipRepository.cs b/src/Core/Repositories/EntityFramework/OrganizationSponsorshipRepository.cs new file mode 100644 index 0000000000..611ede3cde --- /dev/null +++ b/src/Core/Repositories/EntityFramework/OrganizationSponsorshipRepository.cs @@ -0,0 +1,51 @@ +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/Utilities/ServiceCollectionExtensions.cs b/src/Core/Utilities/ServiceCollectionExtensions.cs index 04c6a2eeb9..24a5093f8d 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(); 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 0000000000..93257c3bc8 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Create.sql @@ -0,0 +1,43 @@ +CREATE PROCEDURE [dbo].[OrganizationSponsorship_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @InstallationId UNIQUEIDENTIFIER, + @SponsoringOrganizationId UNIQUEIDENTIFIER, + @SponsoringOrganizationUserID UNIQUEIDENTIFIER, + @SponsoredOrganizationId UNIQUEIDENTIFIER, + @OfferedToEmail NVARCHAR(256), + @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], + [CloudSponsor], + [LastSyncDate], + [TimesRenewedWithoutValidation], + [SponsorshipLapsedDate] + ) + VALUES + ( + @Id, + @InstallationId, + @SponsoringOrganizationId, + @SponsoringOrganizationUserID, + @SponsoredOrganizationId, + @OfferedToEmail, + @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 0000000000..914707154f --- /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_ReadById.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadById.sql new file mode 100644 index 0000000000..630200a32d --- /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 0000000000..22fac3f981 --- /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 0000000000..203249cf8f --- /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 0000000000..817a95cbce --- /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 0000000000..364385f542 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Update.sql @@ -0,0 +1,31 @@ +CREATE PROCEDURE [dbo].[OrganizationSponsorship_Update] + @Id UNIQUEIDENTIFIER, + @InstallationId UNIQUEIDENTIFIER, + @SponsoringOrganizationId UNIQUEIDENTIFIER, + @SponsoringOrganizationUserID UNIQUEIDENTIFIER, + @SponsoredOrganizationId UNIQUEIDENTIFIER, + @OfferedToEmail NVARCHAR(256), + @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, + [CloudSponsor] = @CloudSponsor, + [LastSyncDate] = @LastSyncDate, + [TimesRenewedWithoutValidation] = @TimesRenewedWithoutValidation, + [SponsorshipLapsedDate] = @SponsorshipLapsedDate + WHERE + [Id] = @Id +END +GO diff --git a/src/Sql/dbo/Tables/OrganizationSponsorship.sql b/src/Sql/dbo/Tables/OrganizationSponsorship.sql new file mode 100644 index 0000000000..2dc3ead159 --- /dev/null +++ b/src/Sql/dbo/Tables/OrganizationSponsorship.sql @@ -0,0 +1,41 @@ +CREATE TABLE [dbo].[OrganizationSponsorship] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [InstallationId] UNIQUEIDENTIFIER NULL, + [SponsoringOrganizationId] UNIQUEIDENTIFIER NOT NULL, + [SponsorginOrganizationUserID] UNIQUEIDENTIFIER NOT NULL, + [SponsoredOrganizationId] UNIQUEIDENTIFIER NULL, + [OfferedToEmail] NVARCHAR (256) 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].[Organization]([Id] ASC, [InstallationId] ASC) + WHERE [InstallationId] IS NOT NULL; + +GO +CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationId] + ON [dbo].[Organization]([Id] ASC, [SponsoringOrganizationId] ASC) + +GO +CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationUserId] + ON [dbo].[Organization]([Id] ASC, [SponsorginOrganizationUserID] ASC) + +GO +CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_OfferedToEmail] + ON [dbo].[Organization]([Id] ASC, [OfferedToEmail] ASC) + WHERE [OfferedToEmail] IS NOT NULL; + +GO +CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoredOrganizationID] + ON [dbo].[Organization]([Id] ASC, [SponsoredOrganizationId] ASC) + WHERE [SponsoredOrganizationId] IS NOT NULL; + diff --git a/test/Core.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs b/test/Core.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs index 4096f6919e..0d82ef9b47 100644 --- a/test/Core.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs +++ b/test/Core.Test/AutoFixture/EntityFrameworkRepositoryFixtures.cs @@ -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/OrganizationSponsorshipFixtures.cs b/test/Core.Test/AutoFixture/OrganizationSponsorshipFixtures.cs new file mode 100644 index 0000000000..4a1704ed2a --- /dev/null +++ b/test/Core.Test/AutoFixture/OrganizationSponsorshipFixtures.cs @@ -0,0 +1,59 @@ +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; + +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()) + { } + } + + internal class InlineEfOrganizationSponsorshipAutoDataAttribute : InlineCustomAutoDataAttribute + { + public InlineEfOrganizationSponsorshipAutoDataAttribute(params object[] values) : base(new[] { typeof(SutProviderCustomization), + typeof(EfOrganizationSponsorship) }, values) + { } + } +} 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 0000000000..cd1c292621 --- /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 0000000000..67b7ab409a --- /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/util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql b/util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql new file mode 100644 index 0000000000..12113573e9 --- /dev/null +++ b/util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql @@ -0,0 +1,286 @@ +-- 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 NOT NULL, + [SponsoringOrganizationUserID] UNIQUEIDENTIFIER NOT NULL, + [SponsoredOrganizationId] UNIQUEIDENTIFIER NULL, + [OfferedToEmail] NVARCHAR (256) 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) +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) +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, + @OfferedToEmail NVARCHAR(256), + @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], + [CloudSponsor], + [LastSyncDate], + [TimesRenewedWithoutValidation], + [SponsorshipLapsedDate] + ) + VALUES + ( + @Id, + @InstallationId, + @SponsoringOrganizationId, + @SponsoringOrganizationUserID, + @SponsoredOrganizationId, + @OfferedToEmail, + @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, + @OfferedToEmail NVARCHAR(256), + @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, + [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 diff --git a/util/MySqlMigrations/Migrations/20211102213543_OrganizationSponsorship.Designer.cs b/util/MySqlMigrations/Migrations/20211102213543_OrganizationSponsorship.Designer.cs new file mode 100644 index 0000000000..9a915376e5 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20211102213543_OrganizationSponsorship.Designer.cs @@ -0,0 +1,1562 @@ +// +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("20211102213543_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("InstallationId") + .HasColumnType("char(36)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + 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") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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/20211102213543_OrganizationSponsorship.cs b/util/MySqlMigrations/Migrations/20211102213543_OrganizationSponsorship.cs new file mode 100644 index 0000000000..38da37d966 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20211102213543_OrganizationSponsorship.cs @@ -0,0 +1,83 @@ +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: false, collation: "ascii_general_ci"), + SponsoringOrganizationUserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + SponsoredOrganizationId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), + OfferedToEmail = table.Column(type: "varchar(256)", maxLength: 256, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + 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.Cascade); + }) + .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 9bc3ba7f39..8bca8a7114 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -585,6 +585,50 @@ 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("InstallationId") + .HasColumnType("char(36)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + 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") @@ -1127,6 +1171,9 @@ namespace Bit.MySqlMigrations.Migrations .HasMaxLength(32) .HasColumnType("varchar(32)"); + b.Property("UsesCryptoAgent") + .HasColumnType("tinyint(1)"); + b.HasKey("Id"); b.ToTable("User"); @@ -1292,6 +1339,29 @@ 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") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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 0000000000..11bcabc821 --- /dev/null +++ b/util/MySqlMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.sql @@ -0,0 +1,31 @@ +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 NOT NULL, + `SponsoringOrganizationUserId` char(36) COLLATE ascii_general_ci NOT NULL, + `SponsoredOrganizationId` char(36) COLLATE ascii_general_ci NULL, + `OfferedToEmail` varchar(256) CHARACTER SET utf8mb4 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 CASCADE +) 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 ('20211102213543_OrganizationSponsorship', '5.0.9'); + +COMMIT; diff --git a/util/PostgresMigrations/Migrations/20211102205745_OrganizationSponsorship.Designer.cs b/util/PostgresMigrations/Migrations/20211102205745_OrganizationSponsorship.Designer.cs new file mode 100644 index 0000000000..4120c68f65 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20211102205745_OrganizationSponsorship.Designer.cs @@ -0,0 +1,1571 @@ +// +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("20211102205745_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("InstallationId") + .HasColumnType("uuid"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp without time zone"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + 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") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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/20211102205745_OrganizationSponsorship.cs b/util/PostgresMigrations/Migrations/20211102205745_OrganizationSponsorship.cs new file mode 100644 index 0000000000..cf452e2bd2 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20211102205745_OrganizationSponsorship.cs @@ -0,0 +1,81 @@ +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: false), + SponsoringOrganizationUserId = table.Column(type: "uuid", nullable: false), + SponsoredOrganizationId = table.Column(type: "uuid", nullable: true), + OfferedToEmail = table.Column(type: "character varying(256)", maxLength: 256, 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.Cascade); + }); + + 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 859380ad58..3b758f3135 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -589,6 +589,50 @@ 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("InstallationId") + .HasColumnType("uuid"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp without time zone"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + 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") @@ -1136,6 +1180,9 @@ namespace Bit.PostgresMigrations.Migrations .HasMaxLength(32) .HasColumnType("character varying(32)"); + b.Property("UsesCryptoAgent") + .HasColumnType("boolean"); + b.HasKey("Id"); b.ToTable("User"); @@ -1301,6 +1348,29 @@ 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") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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 0000000000..95e8557ade --- /dev/null +++ b/util/PostgresMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.psql @@ -0,0 +1,31 @@ +START TRANSACTION; + +ALTER TABLE "User" ADD "UsesCryptoAgent" boolean NOT NULL DEFAULT FALSE; + +CREATE TABLE "OrganizationSponsorship" ( + "Id" uuid NOT NULL, + "InstallationId" uuid NULL, + "SponsoringOrganizationId" uuid NOT NULL, + "SponsoringOrganizationUserId" uuid NOT NULL, + "SponsoredOrganizationId" uuid NULL, + "OfferedToEmail" character varying(256) 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 CASCADE +); + +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 ('20211102205745_OrganizationSponsorship', '5.0.9'); + +COMMIT; From d93fcf432fdb257f48811f2b1f01f073981ceafd Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Thu, 4 Nov 2021 12:51:22 -0400 Subject: [PATCH 11/34] Add PlanSponsorshipType to db model --- .../OrganizationSponsorshipsController.cs | 2 +- src/Core/Enums/PlanSponsorshipType.cs | 10 ++++++++++ .../OrganizationSponsorshipRequestModel.cs | 5 +++++ .../Models/Table/OrganizationSponsorship.cs | 2 ++ .../Services/IOrganizationSponsorshipService.cs | 3 ++- .../OrganizationSponsorshipService.cs | 17 ++++++++++++----- .../OrganizationSponsorship_Create.sql | 3 +++ .../OrganizationSponsorship_Update.sql | 2 ++ src/Sql/dbo/Tables/OrganizationSponsorship.sql | 3 ++- .../OrganizationSponsorshipsControllerTests.cs | 8 ++++---- .../OrganizationSponsorshipServiceTests.cs | 8 ++++++-- .../2021-11-02_00_OrganizationSponsorship.sql | 6 ++++++ ...4164838_OrganizationSponsorship.Designer.cs} | 5 ++++- ...> 20211104164838_OrganizationSponsorship.cs} | 1 + .../Migrations/DatabaseContextModelSnapshot.cs | 3 +++ .../2021-11-02_00_OrganizationSponsorship.sql | 3 ++- ...4164532_OrganizationSponsorship.Designer.cs} | 5 ++++- ...> 20211104164532_OrganizationSponsorship.cs} | 1 + .../Migrations/DatabaseContextModelSnapshot.cs | 3 +++ .../2021-11-02_00_OrganizationSponsorship.psql | 3 ++- 20 files changed, 75 insertions(+), 18 deletions(-) create mode 100644 src/Core/Enums/PlanSponsorshipType.cs rename util/MySqlMigrations/Migrations/{20211102213543_OrganizationSponsorship.Designer.cs => 20211104164838_OrganizationSponsorship.Designer.cs} (99%) rename util/MySqlMigrations/Migrations/{20211102213543_OrganizationSponsorship.cs => 20211104164838_OrganizationSponsorship.cs} (97%) rename util/PostgresMigrations/Migrations/{20211102205745_OrganizationSponsorship.Designer.cs => 20211104164532_OrganizationSponsorship.Designer.cs} (99%) rename util/PostgresMigrations/Migrations/{20211102205745_OrganizationSponsorship.cs => 20211104164532_OrganizationSponsorship.cs} (97%) diff --git a/src/Api/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Controllers/OrganizationSponsorshipsController.cs index fde4f938e6..74e5feeb4a 100644 --- a/src/Api/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Controllers/OrganizationSponsorshipsController.cs @@ -64,7 +64,7 @@ namespace Bit.Api.Controllers throw new BadRequestException("Can only sponsor one organization per Organization User."); } - await _organizationsSponsorshipService.OfferSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, model.sponsoredEmail); + await _organizationsSponsorshipService.OfferSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, model.PlanSponsorshipType, model.sponsoredEmail); } [HttpPost("sponsored/redeem/families-for-enterprise")] diff --git a/src/Core/Enums/PlanSponsorshipType.cs b/src/Core/Enums/PlanSponsorshipType.cs new file mode 100644 index 0000000000..59f778e101 --- /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/Models/Api/Request/Organizations/OrganizationSponsorshipRequestModel.cs b/src/Core/Models/Api/Request/Organizations/OrganizationSponsorshipRequestModel.cs index 1e8c8a9811..646b5d2128 100644 --- a/src/Core/Models/Api/Request/Organizations/OrganizationSponsorshipRequestModel.cs +++ b/src/Core/Models/Api/Request/Organizations/OrganizationSponsorshipRequestModel.cs @@ -1,13 +1,18 @@ 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] public Guid OrganizationUserId { get; set; } + [Required] [StringLength(256)] [StrictEmailAddress] diff --git a/src/Core/Models/Table/OrganizationSponsorship.cs b/src/Core/Models/Table/OrganizationSponsorship.cs index 7966d8a090..47bded5be2 100644 --- a/src/Core/Models/Table/OrganizationSponsorship.cs +++ b/src/Core/Models/Table/OrganizationSponsorship.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel.DataAnnotations; +using Bit.Core.Enums; using Bit.Core.Utilities; namespace Bit.Core.Models.Table @@ -15,6 +16,7 @@ namespace Bit.Core.Models.Table public Guid? SponsoredOrganizationId { 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; } diff --git a/src/Core/Services/IOrganizationSponsorshipService.cs b/src/Core/Services/IOrganizationSponsorshipService.cs index ade55ac982..cd2023f176 100644 --- a/src/Core/Services/IOrganizationSponsorshipService.cs +++ b/src/Core/Services/IOrganizationSponsorshipService.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Bit.Core.Enums; using Bit.Core.Models.Table; namespace Bit.Core.Services @@ -6,7 +7,7 @@ namespace Bit.Core.Services public interface IOrganizationSponsorshipService { Task ValidateRedemptionTokenAsync(string encryptedToken); - Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, string sponsoredEmail); + Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, PlanSponsorshipType sponsorshipType, string sponsoredEmail); Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship, Organization sponsoredOrganization); Task RemoveSponsorshipAsync(OrganizationSponsorship sponsorship); } diff --git a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs index b49bbd2f3b..2e4984374f 100644 --- a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs +++ b/src/Core/Services/Implementations/OrganizationSponsorshipService.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 Microsoft.AspNetCore.DataProtection; @@ -38,25 +39,31 @@ namespace Bit.Core.Services if (dataParts[0].Equals(FamiliesForEnterpriseTokenName)) { - if (!Guid.TryParse(dataParts[1], out Guid sponsorshipId)) + if (!Guid.TryParse(dataParts[1], out Guid sponsorshipId) || + !Enum.TryParse(dataParts[2], true, out var sponsorshipType)) { return false; } var sponsorship = await _organizationSponsorshipRepository.GetByIdAsync(sponsorshipId); - return sponsorship != null; + if (sponsorship == null || sponsorship.PlanSponsorshipType != sponsorshipType) + { + return false; + } + + return true; } return false; } - private string RedemptionToken(Guid sponsorshipId) => + private string RedemptionToken(Guid sponsorshipId, PlanSponsorshipType sponsorshipType) => string.Concat( TokenClearTextPrefix, - _dataProtector.Protect($"{FamiliesForEnterpriseTokenName} {sponsorshipId}") + _dataProtector.Protect($"{FamiliesForEnterpriseTokenName} {sponsorshipId} {sponsorshipType}") ); - public async Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, string sponsoredEmail) + public async Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, PlanSponsorshipType sponsorshipType, string sponsoredEmail) { var sponsorship = new OrganizationSponsorship { diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Create.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Create.sql index 93257c3bc8..f26e9a35ba 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Create.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Create.sql @@ -5,6 +5,7 @@ CREATE PROCEDURE [dbo].[OrganizationSponsorship_Create] @SponsoringOrganizationUserID UNIQUEIDENTIFIER, @SponsoredOrganizationId UNIQUEIDENTIFIER, @OfferedToEmail NVARCHAR(256), + @PlanSponsorshipType TINYINT, @CloudSponsor BIT, @LastSyncDate DATETIME2 (7), @TimesRenewedWithoutValidation TINYINT, @@ -21,6 +22,7 @@ BEGIN [SponsoringOrganizationUserID], [SponsoredOrganizationId], [OfferedToEmail], + [PlanSponsorshipType], [CloudSponsor], [LastSyncDate], [TimesRenewedWithoutValidation], @@ -34,6 +36,7 @@ BEGIN @SponsoringOrganizationUserID, @SponsoredOrganizationId, @OfferedToEmail, + @PlanSponsorshipType, @CloudSponsor, @LastSyncDate, @TimesRenewedWithoutValidation, diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Update.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Update.sql index 364385f542..88ac048565 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Update.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Update.sql @@ -5,6 +5,7 @@ CREATE PROCEDURE [dbo].[OrganizationSponsorship_Update] @SponsoringOrganizationUserID UNIQUEIDENTIFIER, @SponsoredOrganizationId UNIQUEIDENTIFIER, @OfferedToEmail NVARCHAR(256), + @PlanSponsorshipType TINYINT, @CloudSponsor BIT, @LastSyncDate DATETIME2 (7), @TimesRenewedWithoutValidation TINYINT, @@ -21,6 +22,7 @@ BEGIN [SponsoringOrganizationUserID] = @SponsoringOrganizationUserID, [SponsoredOrganizationId] = @SponsoredOrganizationId, [OfferedToEmail] = @OfferedToEmail, + [PlanSponsorshipType] = @PlanSponsorshipType, [CloudSponsor] = @CloudSponsor, [LastSyncDate] = @LastSyncDate, [TimesRenewedWithoutValidation] = @TimesRenewedWithoutValidation, diff --git a/src/Sql/dbo/Tables/OrganizationSponsorship.sql b/src/Sql/dbo/Tables/OrganizationSponsorship.sql index 2dc3ead159..9467297b88 100644 --- a/src/Sql/dbo/Tables/OrganizationSponsorship.sql +++ b/src/Sql/dbo/Tables/OrganizationSponsorship.sql @@ -2,9 +2,10 @@ CREATE TABLE [dbo].[OrganizationSponsorship] ( [Id] UNIQUEIDENTIFIER NOT NULL, [InstallationId] UNIQUEIDENTIFIER NULL, [SponsoringOrganizationId] UNIQUEIDENTIFIER NOT NULL, - [SponsorginOrganizationUserID] UNIQUEIDENTIFIER NOT NULL, + [SponsoringOrganizationUserID] UNIQUEIDENTIFIER NOT NULL, [SponsoredOrganizationId] UNIQUEIDENTIFIER NULL, [OfferedToEmail] NVARCHAR (256) NULL, + [PlanSponsorshipType] TINYINT NULL, [CloudSponsor] BIT NULL, [LastSyncDate] DATETIME2 (7) NULL, [TimesRenewedWithoutValidation] TINYINT DEFAULT 0, diff --git a/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs index d9dd59e64f..2f1434ba97 100644 --- a/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs +++ b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs @@ -44,7 +44,7 @@ namespace Bit.Api.Test.Controllers Assert.Contains("Specified Organization cannot sponsor other organizations.", exception.Message); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() - .OfferSponsorshipAsync(default, default, default); + .OfferSponsorshipAsync(default, default, default, default); } public static IEnumerable NonConfirmedOrganizationUsersStatuses => @@ -73,7 +73,7 @@ namespace Bit.Api.Test.Controllers Assert.Contains("Only confirm users can sponsor other organizations.", exception.Message); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() - .OfferSponsorshipAsync(default, default, default); + .OfferSponsorshipAsync(default, default, default, default); } [Theory] @@ -96,7 +96,7 @@ namespace Bit.Api.Test.Controllers Assert.Contains("Can only create organization sponsorships for yourself.", exception.Message); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() - .OfferSponsorshipAsync(default, default, default); + .OfferSponsorshipAsync(default, default, default, default); } [Theory] @@ -121,7 +121,7 @@ namespace Bit.Api.Test.Controllers Assert.Contains("Can only sponsor one organization per Organization User.", exception.Message); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() - .OfferSponsorshipAsync(default, default, default); + .OfferSponsorshipAsync(default, default, default, default); } [Theory] diff --git a/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs b/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs index 3588345fa7..f916756919 100644 --- a/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs +++ b/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using Bit.Core.Enums; using Bit.Core.Models.Table; using Bit.Core.Repositories; using Bit.Core.Services; @@ -35,7 +36,8 @@ namespace Bit.Core.Test.Services public async Task OfferSponsorship_CreatesSponsorship(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, string sponsoredEmail, SutProvider sutProvider) { - await sutProvider.Sut.OfferSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, sponsoredEmail); + await sutProvider.Sut.OfferSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, + PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail); var expectedSponsorship = new OrganizationSponsorship { @@ -64,7 +66,9 @@ namespace Bit.Core.Test.Services return expectedException; }); - var actualException = await Assert.ThrowsAsync(() => sutProvider.Sut.OfferSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, sponsoredEmail)); + var actualException = await Assert.ThrowsAsync(() => + sutProvider.Sut.OfferSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, + PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail)); Assert.Same(expectedException, actualException); await sutProvider.GetDependency().Received(1) diff --git a/util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql b/util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql index 12113573e9..8a0bc78093 100644 --- a/util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql +++ b/util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql @@ -8,6 +8,7 @@ CREATE TABLE [dbo].[OrganizationSponsorship] ( [SponsoringOrganizationUserID] UNIQUEIDENTIFIER NOT NULL, [SponsoredOrganizationId] UNIQUEIDENTIFIER NULL, [OfferedToEmail] NVARCHAR (256) NULL, + [PlanSponsorshipType] TINYINT NULL, [CloudSponsor] BIT NULL, [LastSyncDate] DATETIME2 (7) NULL, [TimesRenewedWithoutValidation] TINYINT DEFAULT 0, @@ -114,6 +115,7 @@ CREATE PROCEDURE [dbo].[OrganizationSponsorship_Create] @SponsoringOrganizationUserID UNIQUEIDENTIFIER, @SponsoredOrganizationId UNIQUEIDENTIFIER, @OfferedToEmail NVARCHAR(256), + @PlanSponsorshipType TINYINT, @CloudSponsor BIT, @LastSyncDate DATETIME2 (7), @TimesRenewedWithoutValidation TINYINT, @@ -130,6 +132,7 @@ BEGIN [SponsoringOrganizationUserID], [SponsoredOrganizationId], [OfferedToEmail], + [PlanSponsorshipType], [CloudSponsor], [LastSyncDate], [TimesRenewedWithoutValidation], @@ -143,6 +146,7 @@ BEGIN @SponsoringOrganizationUserID, @SponsoredOrganizationId, @OfferedToEmail, + @PlanSponsorshipType, @CloudSponsor, @LastSyncDate, @TimesRenewedWithoutValidation, @@ -165,6 +169,7 @@ CREATE PROCEDURE [dbo].[OrganizationSponsorship_Update] @SponsoringOrganizationUserID UNIQUEIDENTIFIER, @SponsoredOrganizationId UNIQUEIDENTIFIER, @OfferedToEmail NVARCHAR(256), + @PlanSponsorshipType TINYINT, @CloudSponsor BIT, @LastSyncDate DATETIME2 (7), @TimesRenewedWithoutValidation TINYINT, @@ -181,6 +186,7 @@ BEGIN [SponsoringOrganizationUserID] = @SponsoringOrganizationUserID, [SponsoredOrganizationId] = @SponsoredOrganizationId, [OfferedToEmail] = @OfferedToEmail, + [PlanSponsorshipType] = @PlanSponsorshipType, [CloudSponsor] = @CloudSponsor, [LastSyncDate] = @LastSyncDate, [TimesRenewedWithoutValidation] = @TimesRenewedWithoutValidation, diff --git a/util/MySqlMigrations/Migrations/20211102213543_OrganizationSponsorship.Designer.cs b/util/MySqlMigrations/Migrations/20211104164838_OrganizationSponsorship.Designer.cs similarity index 99% rename from util/MySqlMigrations/Migrations/20211102213543_OrganizationSponsorship.Designer.cs rename to util/MySqlMigrations/Migrations/20211104164838_OrganizationSponsorship.Designer.cs index 9a915376e5..39646fa67b 100644 --- a/util/MySqlMigrations/Migrations/20211102213543_OrganizationSponsorship.Designer.cs +++ b/util/MySqlMigrations/Migrations/20211104164838_OrganizationSponsorship.Designer.cs @@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Bit.MySqlMigrations.Migrations { [DbContext(typeof(DatabaseContext))] - [Migration("20211102213543_OrganizationSponsorship")] + [Migration("20211104164838_OrganizationSponsorship")] partial class OrganizationSponsorship { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -605,6 +605,9 @@ namespace Bit.MySqlMigrations.Migrations .HasMaxLength(256) .HasColumnType("varchar(256)"); + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + b.Property("SponsoredOrganizationId") .HasColumnType("char(36)"); diff --git a/util/MySqlMigrations/Migrations/20211102213543_OrganizationSponsorship.cs b/util/MySqlMigrations/Migrations/20211104164838_OrganizationSponsorship.cs similarity index 97% rename from util/MySqlMigrations/Migrations/20211102213543_OrganizationSponsorship.cs rename to util/MySqlMigrations/Migrations/20211104164838_OrganizationSponsorship.cs index 38da37d966..0af0aca109 100644 --- a/util/MySqlMigrations/Migrations/20211102213543_OrganizationSponsorship.cs +++ b/util/MySqlMigrations/Migrations/20211104164838_OrganizationSponsorship.cs @@ -25,6 +25,7 @@ namespace Bit.MySqlMigrations.Migrations SponsoredOrganizationId = table.Column(type: "char(36)", nullable: true, collation: "ascii_general_ci"), 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), diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index 8bca8a7114..06545d15f1 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -603,6 +603,9 @@ namespace Bit.MySqlMigrations.Migrations .HasMaxLength(256) .HasColumnType("varchar(256)"); + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + b.Property("SponsoredOrganizationId") .HasColumnType("char(36)"); diff --git a/util/MySqlMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.sql b/util/MySqlMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.sql index 11bcabc821..cb1f7c0ffd 100644 --- a/util/MySqlMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.sql +++ b/util/MySqlMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.sql @@ -9,6 +9,7 @@ CREATE TABLE `OrganizationSponsorship` ( `SponsoringOrganizationUserId` char(36) COLLATE ascii_general_ci NOT NULL, `SponsoredOrganizationId` char(36) COLLATE ascii_general_ci 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, @@ -26,6 +27,6 @@ CREATE INDEX `IX_OrganizationSponsorship_SponsoredOrganizationId` ON `Organizati CREATE INDEX `IX_OrganizationSponsorship_SponsoringOrganizationId` ON `OrganizationSponsorship` (`SponsoringOrganizationId`); INSERT INTO `__EFMigrationsHistory` (`MigrationId`, `ProductVersion`) -VALUES ('20211102213543_OrganizationSponsorship', '5.0.9'); +VALUES ('20211104164838_OrganizationSponsorship', '5.0.9'); COMMIT; diff --git a/util/PostgresMigrations/Migrations/20211102205745_OrganizationSponsorship.Designer.cs b/util/PostgresMigrations/Migrations/20211104164532_OrganizationSponsorship.Designer.cs similarity index 99% rename from util/PostgresMigrations/Migrations/20211102205745_OrganizationSponsorship.Designer.cs rename to util/PostgresMigrations/Migrations/20211104164532_OrganizationSponsorship.Designer.cs index 4120c68f65..d383ce3669 100644 --- a/util/PostgresMigrations/Migrations/20211102205745_OrganizationSponsorship.Designer.cs +++ b/util/PostgresMigrations/Migrations/20211104164532_OrganizationSponsorship.Designer.cs @@ -10,7 +10,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Bit.PostgresMigrations.Migrations { [DbContext(typeof(DatabaseContext))] - [Migration("20211102205745_OrganizationSponsorship")] + [Migration("20211104164532_OrganizationSponsorship")] partial class OrganizationSponsorship { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -609,6 +609,9 @@ namespace Bit.PostgresMigrations.Migrations .HasMaxLength(256) .HasColumnType("character varying(256)"); + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + b.Property("SponsoredOrganizationId") .HasColumnType("uuid"); diff --git a/util/PostgresMigrations/Migrations/20211102205745_OrganizationSponsorship.cs b/util/PostgresMigrations/Migrations/20211104164532_OrganizationSponsorship.cs similarity index 97% rename from util/PostgresMigrations/Migrations/20211102205745_OrganizationSponsorship.cs rename to util/PostgresMigrations/Migrations/20211104164532_OrganizationSponsorship.cs index cf452e2bd2..0de10d335d 100644 --- a/util/PostgresMigrations/Migrations/20211102205745_OrganizationSponsorship.cs +++ b/util/PostgresMigrations/Migrations/20211104164532_OrganizationSponsorship.cs @@ -24,6 +24,7 @@ namespace Bit.PostgresMigrations.Migrations SponsoringOrganizationUserId = table.Column(type: "uuid", nullable: false), SponsoredOrganizationId = table.Column(type: "uuid", 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), diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index 3b758f3135..920d128d6a 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -607,6 +607,9 @@ namespace Bit.PostgresMigrations.Migrations .HasMaxLength(256) .HasColumnType("character varying(256)"); + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + b.Property("SponsoredOrganizationId") .HasColumnType("uuid"); diff --git a/util/PostgresMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.psql b/util/PostgresMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.psql index 95e8557ade..870aff88a1 100644 --- a/util/PostgresMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.psql +++ b/util/PostgresMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.psql @@ -9,6 +9,7 @@ CREATE TABLE "OrganizationSponsorship" ( "SponsoringOrganizationUserId" uuid NOT NULL, "SponsoredOrganizationId" uuid NULL, "OfferedToEmail" character varying(256) NULL, + "PlanSponsorshipType" smallint NULL, "CloudSponsor" boolean NOT NULL, "LastSyncDate" timestamp without time zone NULL, "TimesRenewedWithoutValidation" smallint NOT NULL, @@ -26,6 +27,6 @@ CREATE INDEX "IX_OrganizationSponsorship_SponsoredOrganizationId" ON "Organizati CREATE INDEX "IX_OrganizationSponsorship_SponsoringOrganizationId" ON "OrganizationSponsorship" ("SponsoringOrganizationId"); INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") -VALUES ('20211102205745_OrganizationSponsorship', '5.0.9'); +VALUES ('20211104164532_OrganizationSponsorship', '5.0.9'); COMMIT; From 5d5febc94bba15b1cfbaeba2b5afbb78c42982f0 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Thu, 4 Nov 2021 14:46:59 -0400 Subject: [PATCH 12/34] Write valid json for test entries --- .../Core.Test/AutoFixture/OrganizationSponsorshipFixtures.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/Core.Test/AutoFixture/OrganizationSponsorshipFixtures.cs b/test/Core.Test/AutoFixture/OrganizationSponsorshipFixtures.cs index 4a1704ed2a..7c39ca097e 100644 --- a/test/Core.Test/AutoFixture/OrganizationSponsorshipFixtures.cs +++ b/test/Core.Test/AutoFixture/OrganizationSponsorshipFixtures.cs @@ -7,6 +7,7 @@ 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 { @@ -46,14 +47,14 @@ namespace Bit.Core.Test.AutoFixture.OrganizationSponsorshipFixtures internal class EfOrganizationSponsorshipAutoDataAttribute : CustomAutoDataAttribute { - public EfOrganizationSponsorshipAutoDataAttribute() : base(new SutProviderCustomization(), new EfOrganizationSponsorship()) + public EfOrganizationSponsorshipAutoDataAttribute() : base(new SutProviderCustomization(), new EfOrganizationSponsorship(), new EfOrganization()) { } } internal class InlineEfOrganizationSponsorshipAutoDataAttribute : InlineCustomAutoDataAttribute { public InlineEfOrganizationSponsorshipAutoDataAttribute(params object[] values) : base(new[] { typeof(SutProviderCustomization), - typeof(EfOrganizationSponsorship) }, values) + typeof(EfOrganizationSponsorship), typeof(EfOrganization) }, values) { } } } From cba01968591312ec12d87c4094283c117cbc5731 Mon Sep 17 00:00:00 2001 From: Justin Baur Date: Mon, 8 Nov 2021 11:47:58 -0500 Subject: [PATCH 13/34] 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 --- ...liesForEnterpriseInviteRedeemable.html.hbs | 16 +++ ...liesForEnterpriseInviteRedeemable.text.hbs | 5 + ...erpriseInviteRedeemedToFamilyUser.html.hbs | 9 ++ ...erpriseInviteRedeemedToFamilyUser.text.hbs | 3 + ...EnterpriseInviteRedeemedToOrgUser.html.hbs | 9 ++ ...EnterpriseInviteRedeemedToOrgUser.text.hbs | 3 + ...rEnterpriseReconfirmationRequired.html.hbs | 16 +++ ...rEnterpriseReconfirmationRequired.text.hbs | 5 + ...iesForEnterpriseSponsorshipEnding.html.hbs | 9 ++ ...iesForEnterpriseSponsorshipEnding.text.hbs | 3 + ...ForEnterpriseSponsorshipReverting.html.hbs | 9 ++ ...ForEnterpriseSponsorshipReverting.text.hbs | 3 + ...sForEnterpriseInviteRedeemableViewModel.cs | 8 ++ ...riseInviteRedeemedToFamilyUserViewModel.cs | 7 + ...erpriseInviteRedeemedToOrgUserViewModel.cs | 7 + ...terpriseReconfirmationRequiredViewModel.cs | 7 + ...ForEnterpriseSponsorshipEndingViewModel.cs | 7 + ...EnterpriseSponsorshipRevertingViewModel.cs | 7 + src/Core/Services/IMailService.cs | 8 ++ .../Implementations/HandlebarsMailService.cs | 89 ++++++++++++ .../NoopImplementations/NoopMailService.cs | 30 ++++ .../Services/HandlebarsMailServiceTests.cs | 133 ++++++++++++++++++ 22 files changed, 393 insertions(+) create mode 100644 src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemable.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemable.text.hbs create mode 100644 src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToFamilyUser.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToFamilyUser.text.hbs create mode 100644 src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToOrgUser.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToOrgUser.text.hbs create mode 100644 src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseReconfirmationRequired.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseReconfirmationRequired.text.hbs create mode 100644 src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipEnding.html.hbs create mode 100644 src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipEnding.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/Mail/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemableViewModel.cs create mode 100644 src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToFamilyUserViewModel.cs create mode 100644 src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToOrgUserViewModel.cs create mode 100644 src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseReconfirmationRequiredViewModel.cs create mode 100644 src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipEndingViewModel.cs create mode 100644 src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipRevertingViewModel.cs diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemable.html.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemable.html.hbs new file mode 100644 index 0000000000..d233381b98 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemable.html.hbs @@ -0,0 +1,16 @@ +{{#>FullHtmlLayout}} + + + + + + + +
+ {{OrganizationName}} has offered to sponsor a family organization for you with Bitwarden. +
+ + Redeem + +
+{{/FullHtmlLayout}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemable.text.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemable.text.hbs new file mode 100644 index 0000000000..a6bd7bb7e2 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemable.text.hbs @@ -0,0 +1,5 @@ +{{#>BasicTextLayout}} +{{OrganizationName}} has offered to sponsor a family organization for you with Bitwarden. To redeem please click the following link: + +{{Url}} +{{/BasicTextLayout}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToFamilyUser.html.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToFamilyUser.html.hbs new file mode 100644 index 0000000000..346982ab29 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToFamilyUser.html.hbs @@ -0,0 +1,9 @@ +{{#>FullHtmlLayout}} + + + + +
+ You have redeemed a family organization sponsorship from {{OrganizationName}}. +
+{{/FullHtmlLayout}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToFamilyUser.text.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToFamilyUser.text.hbs new file mode 100644 index 0000000000..994c883586 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToFamilyUser.text.hbs @@ -0,0 +1,3 @@ +{{#>BasicTextLayout}} +You have redeemed a family organization sponsorship from {{OrganizationName}}. +{{/BasicTextLayout}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToOrgUser.html.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToOrgUser.html.hbs new file mode 100644 index 0000000000..f2615c3740 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToOrgUser.html.hbs @@ -0,0 +1,9 @@ +{{#>FullHtmlLayout}} + + + + +
+ You have redeemed a Families for Enterprise sponsorship from {{OrganizationName}}. +
+{{/FullHtmlLayout}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToOrgUser.text.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToOrgUser.text.hbs new file mode 100644 index 0000000000..8dd8e05ef2 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToOrgUser.text.hbs @@ -0,0 +1,3 @@ +{{#>BasicTextLayout}} +A user in your organization has redeemed a family invitation. +{{/BasicTextLayout}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseReconfirmationRequired.html.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseReconfirmationRequired.html.hbs new file mode 100644 index 0000000000..51750c24ff --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseReconfirmationRequired.html.hbs @@ -0,0 +1,16 @@ +{{#>FullHtmlLayout}} + + + + + + + +
+ Your Families for Enterprise sponsorship requires reconfirmation. +
+ + Reconfirm + +
+{{/FullHtmlLayout}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseReconfirmationRequired.text.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseReconfirmationRequired.text.hbs new file mode 100644 index 0000000000..82bb6387e1 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseReconfirmationRequired.text.hbs @@ -0,0 +1,5 @@ +{{#>BasicTextLayout}} +Your Families for Enterprise sponsorship requires reconfirmation. To redeem please click the following link: + +{{Url}} +{{/BasicTextLayout}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipEnding.html.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipEnding.html.hbs new file mode 100644 index 0000000000..b5d45565a9 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipEnding.html.hbs @@ -0,0 +1,9 @@ +{{#>FullHtmlLayout}} + + + + +
+ Your Families for Enterprise sponsorship has ended and you will lose premium access at the end of the current billing cycle. +
+{{/FullHtmlLayout}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipEnding.text.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipEnding.text.hbs new file mode 100644 index 0000000000..a2c98521f8 --- /dev/null +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipEnding.text.hbs @@ -0,0 +1,3 @@ +{{#>BasicTextLayout}} +Your Families for Enterprise sponsorship has ended and you will lose premium access at the end of the current billing cycle. +{{/BasicTextLayout}} \ No newline at end of file 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 0000000000..0b3f90eaef --- /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}} \ No newline at end of file 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 0000000000..16017d79af --- /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}} \ No newline at end of file diff --git a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemableViewModel.cs b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemableViewModel.cs new file mode 100644 index 0000000000..57e01e7108 --- /dev/null +++ b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemableViewModel.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Models.Mail.FamiliesForEnterprise +{ + public class FamiliesForEnterpriseInviteRedeemableViewModel : BaseMailModel + { + public string OrganizationName { get; set; } + public string Url { get; set; } + } +} diff --git a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToFamilyUserViewModel.cs b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToFamilyUserViewModel.cs new file mode 100644 index 0000000000..bda9c907cb --- /dev/null +++ b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToFamilyUserViewModel.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Models.Mail.FamiliesForEnterprise +{ + public class FamiliesForEnterpriseInviteRedeemedToFamilyUserViewModel : BaseMailModel + { + public string OrganizationName { get; set; } + } +} diff --git a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToOrgUserViewModel.cs b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToOrgUserViewModel.cs new file mode 100644 index 0000000000..a547b4f35d --- /dev/null +++ b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToOrgUserViewModel.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Models.Mail.FamiliesForEnterprise +{ + public class FamiliesForEnterpriseInviteRedeemedToOrgUserViewModel : BaseMailModel + { + public string OrganizationName { get; set; } + } +} diff --git a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseReconfirmationRequiredViewModel.cs b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseReconfirmationRequiredViewModel.cs new file mode 100644 index 0000000000..5cf7ca2e41 --- /dev/null +++ b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseReconfirmationRequiredViewModel.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Models.Mail.FamiliesForEnterprise +{ + public class FamiliesForEnterpriseReconfirmationRequiredViewModel : BaseMailModel + { + + } +} diff --git a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipEndingViewModel.cs b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipEndingViewModel.cs new file mode 100644 index 0000000000..e6568562a1 --- /dev/null +++ b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipEndingViewModel.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Models.Mail.FamiliesForEnterprise +{ + public class FamiliesForEnterpriseSponsorshipEndingViewModel : BaseMailModel + { + + } +} diff --git a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipRevertingViewModel.cs b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipRevertingViewModel.cs new file mode 100644 index 0000000000..cef96099f8 --- /dev/null +++ b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipRevertingViewModel.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Models.Mail.FamiliesForEnterprise +{ + public class FamiliesForEnterpriseSponsorshipRevertingViewModel : BaseMailModel + { + + } +} diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 8abf5a0d51..c0eb047c1e 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -49,5 +49,13 @@ namespace Bit.Core.Services Task SendProviderConfirmedEmailAsync(string providerName, string email); Task SendProviderUserRemoved(string providerName, string email); Task SendUpdatedTempPasswordEmailAsync(string email, string userName); + // TODO: Change signature to hold data needed for email + Task SendFamiliesForEnterpriseInviteRedeemableEmailAsync(string email, string organizationName, string token); + // NOTE: Not married to these next two names + Task SendFamiliesForEnterpriseInviteRedeemedToFamilyUserEmailAsync(string email); + Task SendFamiliesForEnterpriseInviteRedeemedToOrgUserEmailAsync(string email, string organizationName); + Task SendFamiliesForEnterpriseReconfirmationRequiredEmailAsync(string email); + Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email); + Task SendFamiliesForEnterpriseSponsorshipEndingEmailAsync(string email); } } diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 8e981f386e..ada022a195 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; @@ -755,5 +756,93 @@ namespace Bit.Core.Services message.Category = "UpdatedTempPassword"; await _mailDeliveryService.SendEmailAsync(message); } + + public async Task SendFamiliesForEnterpriseInviteRedeemableEmailAsync(string email, string organizationName, string token) + { + // TODO: Complete emails + var message = CreateDefaultMessage("A Family Organization Invite Is Redeemable", email); + + // NOTE: If somehow cloud vault changes this will need to change/be injected + var url = CoreHelpers.ExtendQuery(new Uri($"https://vault.bitwarden.com/#/sponsored/families-for-enterprise"), + new Dictionary + { + ["sponsorshipToken"] = token, + }); + + var model = new FamiliesForEnterpriseInviteRedeemableViewModel + { + Url = url.ToString(), + OrganizationName = organizationName, + }; + + await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseInviteRedeemable", model); + message.Category = "FamiliesForEnterpriseInviteRedeemable"; + await _mailDeliveryService.SendEmailAsync(message); + } + + public async Task SendFamiliesForEnterpriseInviteRedeemedToFamilyUserEmailAsync(string email) + { + // TODO: Complete emails + var message = CreateDefaultMessage("You Have Redeemed A Family Organization Sponsorship", email); + var model = new FamiliesForEnterpriseInviteRedeemedToFamilyUserViewModel + { + + }; + await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseInviteRedeemedToFamilyUser", model); + message.Category = "FamilyForEnterpriseInviteRedeemedToFamilyUser"; + await _mailDeliveryService.SendEmailAsync(message); + } + + public async Task SendFamiliesForEnterpriseInviteRedeemedToOrgUserEmailAsync(string email, string organizationName) + { + // TODO: Complete emails + var message = CreateDefaultMessage("A User Has Redeemeed Your Sponsorship", email); + var model = new FamiliesForEnterpriseInviteRedeemedToOrgUserViewModel + { + OrganizationName = organizationName, + }; + await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseInviteRedeemedToOrgUser", model); + message.Category = "FamilyForEnterpriseInviteRedeemedToOrgUser"; + await _mailDeliveryService.SendEmailAsync(message); + } + + public async Task SendFamiliesForEnterpriseReconfirmationRequiredEmailAsync(string email) + { + // TODO: Complete emails + var message = CreateDefaultMessage("Your Sponsorship Requires Reconfirmation", email); + var model = new FamiliesForEnterpriseReconfirmationRequiredViewModel + { + + }; + await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseReconfirmationRequired", model); + message.Category = "FamiliesForEnterpriseReconfirmationRequired"; + await _mailDeliveryService.SendEmailAsync(message); + } + + public async Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email) + { + // TODO: Complete emails + var message = CreateDefaultMessage("A Family Organization Sponsorship Is Reverting", email); + var model = new FamiliesForEnterpriseSponsorshipRevertingViewModel + { + + }; + await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseSponsorshipReverting", model); + message.Category = "FamiliesForEnterpriseSponsorshipReverting"; + await _mailDeliveryService.SendEmailAsync(message); + } + + public async Task SendFamiliesForEnterpriseSponsorshipEndingEmailAsync(string email) + { + // TODO: Complete emails + var message = CreateDefaultMessage("A Family Organization Sponsorship Is Ending", email); + var model = new FamiliesForEnterpriseSponsorshipEndingViewModel + { + + }; + await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseSponsorshipEnding", model); + message.Category = "FamiliesForEnterpriseSponsorshipEnding"; + await _mailDeliveryService.SendEmailAsync(message); + } } } diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index f514993d17..7a648a4477 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -200,5 +200,35 @@ namespace Bit.Core.Services { return Task.FromResult(0); } + + public Task SendFamiliesForEnterpriseInviteRedeemableEmailAsync(string email, string organizationName, string token) + { + return Task.FromResult(0); + } + + public Task SendFamiliesForEnterpriseInviteRedeemedToFamilyUserEmailAsync(string email) + { + return Task.FromResult(0); + } + + public Task SendFamiliesForEnterpriseInviteRedeemedToOrgUserEmailAsync(string email, string organizationName) + { + return Task.FromResult(0); + } + + public Task SendFamiliesForEnterpriseReconfirmationRequiredEmailAsync(string email) + { + return Task.FromResult(0); + } + + public Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email) + { + return Task.FromResult(0); + } + + public Task SendFamiliesForEnterpriseSponsorshipEndingEmailAsync(string email) + { + return Task.FromResult(0); + } } } diff --git a/test/Core.Test/Services/HandlebarsMailServiceTests.cs b/test/Core.Test/Services/HandlebarsMailServiceTests.cs index 1204a06136..64b1da1930 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,131 @@ namespace Bit.Core.Test.Services ); } + [Fact(Skip = "Only 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(), + }}, + }; + + 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] From 635751406418c0b62c7afe1820ebac5a403eb194 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Mon, 8 Nov 2021 17:01:09 -0600 Subject: [PATCH 14/34] 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 --- .../OrganizationSponsorshipsController.cs | 41 +- src/Billing/Controllers/StripeController.cs | 16 + src/Core/Enums/PaymentMethodType.cs | 2 + src/Core/Enums/PlanSponsorshipType.cs | 2 +- src/Core/Enums/PlanType.cs | 24 +- ...ganizationSponsorshipRedeemRequestModel.cs | 3 + .../OrganizationSponsorshipRequestModel.cs | 5 +- .../ProfileOrganizationResponseModel.cs | 2 + .../SponsoredOrganizationSubscription.cs | 39 ++ .../Business/SubscriptionCreateOptions.cs | 97 ++++- .../OrganizationUserOrganizationDetails.cs | 1 + src/Core/Models/StaticStore/SponsoredPlan.cs | 12 + .../Models/Table/OrganizationSponsorship.cs | 8 +- .../EntityFramework/DatabaseContext.cs | 2 +- .../EntityFramework/OrganizationRepository.cs | 24 ++ .../OrganizationSponsorshipRepository.cs | 2 +- .../OrganizationUserRepository.cs | 20 + ...izationUserOrganizationDetailsViewQuery.cs | 5 +- .../IOrganizationSponsorshipService.cs | 7 +- src/Core/Services/IPaymentService.cs | 9 +- .../OrganizationSponsorshipService.cs | 126 +++++- .../Implementations/StripePaymentService.cs | 79 +++- src/Core/Utilities/StaticStore.cs | 16 + ...izationSponsorship_OrganizationDeleted.sql | 31 ++ ...ionSponsorship_OrganizationUserDeleted.sql | 13 + ...onSponsorship_OrganizationUsersDeleted.sql | 24 ++ .../OrganizationUser_DeleteById.sql | 12 +- .../OrganizationUser_DeleteByIds.sql | 1 + .../Organization_DeleteById.sql | 3 + .../dbo/Tables/OrganizationSponsorship.sql | 17 +- ...OrganizationSponsorshipsControllerTests.cs | 98 ++++- test/Core.Test/Enums/PlanTypeHelperTests.cs | 39 -- .../OrganizationSponsorshipServiceTests.cs | 9 +- .../2021-11-02_00_OrganizationSponsorship.sql | 374 +++++++++++++++++- ...25243_OrganizationSponsorship.Designer.cs} | 14 +- ...20211108225243_OrganizationSponsorship.cs} | 8 +- .../DatabaseContextModelSnapshot.cs | 12 +- .../2021-11-02_00_OrganizationSponsorship.sql | 9 +- ...25011_OrganizationSponsorship.Designer.cs} | 14 +- ...20211108225011_OrganizationSponsorship.cs} | 7 +- .../DatabaseContextModelSnapshot.cs | 12 +- ...2021-11-02_00_OrganizationSponsorship.psql | 9 +- 42 files changed, 1060 insertions(+), 188 deletions(-) create mode 100644 src/Core/Models/Business/SponsoredOrganizationSubscription.cs create mode 100644 src/Core/Models/StaticStore/SponsoredPlan.cs 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 delete mode 100644 test/Core.Test/Enums/PlanTypeHelperTests.cs rename util/MySqlMigrations/Migrations/{20211104164838_OrganizationSponsorship.Designer.cs => 20211108225243_OrganizationSponsorship.Designer.cs} (99%) rename util/MySqlMigrations/Migrations/{20211104164838_OrganizationSponsorship.cs => 20211108225243_OrganizationSponsorship.cs} (91%) rename util/PostgresMigrations/Migrations/{20211104164532_OrganizationSponsorship.Designer.cs => 20211108225011_OrganizationSponsorship.Designer.cs} (99%) rename util/PostgresMigrations/Migrations/{20211104164532_OrganizationSponsorship.cs => 20211108225011_OrganizationSponsorship.cs} (93%) diff --git a/src/Api/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Controllers/OrganizationSponsorshipsController.cs index 74e5feeb4a..d8707960d3 100644 --- a/src/Api/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Controllers/OrganizationSponsorshipsController.cs @@ -42,8 +42,11 @@ namespace Bit.Api.Controllers { // TODO: validate has right to sponsor, send sponsorship email var sponsoringOrgIdGuid = new Guid(sponsoringOrgId); + var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(model.PlanSponsorshipType)?.SponsoringProductType; var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgIdGuid); - if (sponsoringOrg == null || !PlanTypeHelper.HasEnterprisePlan(sponsoringOrg)) + if (requiredSponsoringProductType == null || + sponsoringOrg == null || + StaticStore.GetPlan(sponsoringOrg.PlanType).Product != requiredSponsoringProductType.Value) { throw new BadRequestException("Specified Organization cannot sponsor other organizations."); } @@ -64,14 +67,14 @@ namespace Bit.Api.Controllers throw new BadRequestException("Can only sponsor one organization per Organization User."); } - await _organizationsSponsorshipService.OfferSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, model.PlanSponsorshipType, model.sponsoredEmail); + await _organizationsSponsorshipService.OfferSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, + model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName); } [HttpPost("sponsored/redeem/families-for-enterprise")] [SelfHosted(NotSelfHostedOnly = true)] public async Task RedeemSponsorship([FromQuery] string sponsorshipToken, [FromBody] OrganizationSponsorshipRedeemRequestModel model) { - // TODO: parse out sponsorshipInfo if (!await _organizationsSponsorshipService.ValidateRedemptionTokenAsync(sponsorshipToken)) { throw new BadRequestException("Failed to parse sponsorship token."); @@ -99,9 +102,12 @@ namespace Bit.Api.Controllers throw new BadRequestException("Cannot redeem a sponsorship offer for an organization that is already sponsored. Revoke existing sponsorship first."); } + // Check org to sponsor's product type + var requiredSponsoredProductType = StaticStore.GetSponsoredPlan(model.PlanSponsorshipType)?.SponsoredProductType; var organizationToSponsor = await _organizationRepository.GetByIdAsync(model.SponsoredOrganizationId); - // TODO: only current families plan? - if (organizationToSponsor == null || !PlanTypeHelper.HasFamiliesPlan(organizationToSponsor)) + if (requiredSponsoredProductType == null || + organizationToSponsor == null || + StaticStore.GetPlan(organizationToSponsor.PlanType).Product != requiredSponsoredProductType.Value) { throw new BadRequestException("Can only redeem sponsorship offer on families organizations."); } @@ -124,12 +130,19 @@ namespace Bit.Api.Controllers var existingOrgSponsorship = await _organizationSponsorshipRepository .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUserIdGuid); - if (existingOrgSponsorship == null) + if (existingOrgSponsorship == null || existingOrgSponsorship.SponsoredOrganizationId == null) { - throw new BadRequestException("You are not currently sponsoring and organization."); + throw new BadRequestException("You are not currently sponsoring an organization."); } - await _organizationsSponsorshipService.RemoveSponsorshipAsync(existingOrgSponsorship); + var sponsoredOrganization = await _organizationRepository + .GetByIdAsync(existingOrgSponsorship.SponsoredOrganizationId.Value); + if (sponsoredOrganization == null) + { + throw new BadRequestException("Unable to find the sponsored Organization."); + } + + await _organizationsSponsorshipService.RemoveSponsorshipAsync(sponsoredOrganization, existingOrgSponsorship); } [HttpDelete("sponsored/{sponsoredOrgId}")] @@ -146,12 +159,20 @@ namespace Bit.Api.Controllers var existingOrgSponsorship = await _organizationSponsorshipRepository .GetBySponsoredOrganizationIdAsync(sponsoredOrgIdGuid); - if (existingOrgSponsorship == null) + if (existingOrgSponsorship == null || existingOrgSponsorship.SponsoredOrganizationId == null) { throw new BadRequestException("The requested organization is not currently being sponsored."); } - await _organizationsSponsorshipService.RemoveSponsorshipAsync(existingOrgSponsorship); + var sponsoredOrganization = await _organizationRepository + .GetByIdAsync(existingOrgSponsorship.SponsoredOrganizationId.Value); + if (sponsoredOrganization == null) + { + throw new BadRequestException("Unable to find the sponsored Organization."); + } + + + await _organizationsSponsorshipService.RemoveSponsorshipAsync(sponsoredOrganization, existingOrgSponsorship); } } } diff --git a/src/Billing/Controllers/StripeController.cs b/src/Billing/Controllers/StripeController.cs index 12a4c5c97c..ba6a12a6a2 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; @@ -136,6 +139,16 @@ namespace Bit.Billing.Controllers // org if (ids.Item1.HasValue) { + var newEndPeriod = subscription.CurrentPeriodEnd; + + // sponsored org + if (IsSponsoredSubscription(subscription)) + { + var sponsorshipValid = await _organizationSponsorshipService + .ValidateSponsorshipAsync(ids.Item1.Value); + // TODO: How do we return from this? + } + await _organizationService.UpdateExpirationDateAsync(ids.Item1.Value, subscription.CurrentPeriodEnd); } @@ -783,5 +796,8 @@ namespace Bit.Billing.Controllers } return subscription; } + + private static bool IsSponsoredSubscription(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 81b536bd7d..b0290f92b3 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 index 59f778e101..79145bf1eb 100644 --- a/src/Core/Enums/PlanSponsorshipType.cs +++ b/src/Core/Enums/PlanSponsorshipType.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; namespace Bit.Core.Enums { diff --git a/src/Core/Enums/PlanType.cs b/src/Core/Enums/PlanType.cs index 501b8ec3c0..037f1f8938 100644 --- a/src/Core/Enums/PlanType.cs +++ b/src/Core/Enums/PlanType.cs @@ -1,6 +1,4 @@ using System.ComponentModel.DataAnnotations; -using System.Linq; -using Bit.Core.Models.Table; namespace Bit.Core.Enums { @@ -29,26 +27,6 @@ namespace Bit.Core.Enums [Display(Name = "Enterprise (Monthly)")] EnterpriseMonthly = 10, [Display(Name = "Enterprise (Annually)")] - EnterpriseAnnually= 11, - } - - public static class PlanTypeHelper - { - private static readonly PlanType[] _freePlans = new[] { PlanType.Free }; - private static readonly PlanType[] _familiesPlans = new[] { PlanType.FamiliesAnnually, PlanType.FamiliesAnnually2019 }; - private static readonly PlanType[] _teamsPlans = new[] { PlanType.TeamsAnnually, PlanType.TeamsAnnually2019, - PlanType.TeamsMonthly, PlanType.TeamsMonthly2019}; - private static readonly PlanType[] _enterprisePlans = new[] { PlanType.EnterpriseAnnually, - PlanType.EnterpriseAnnually2019, PlanType.EnterpriseMonthly, PlanType.EnterpriseMonthly2019 }; - - private static bool HasPlan(PlanType[] planTypes, PlanType planType) => planTypes.Any(p => p == planType); - public static bool HasFreePlan(Organization org) => IsFree(org.PlanType); - public static bool IsFree(PlanType planType) => HasPlan(_freePlans, planType); - public static bool HasFamiliesPlan(Organization org) => IsFamilies(org.PlanType); - public static bool IsFamilies(PlanType planType) => HasPlan(_familiesPlans, planType); - public static bool HasTeamsPlan(Organization org) => IsTeams(org.PlanType); - public static bool IsTeams(PlanType planType) => HasPlan(_teamsPlans, planType); - public static bool HasEnterprisePlan(Organization org) => IsEnterprise(org.PlanType); - public static bool IsEnterprise(PlanType planType) => HasPlan(_enterprisePlans, planType); + EnterpriseAnnually = 11, } } diff --git a/src/Core/Models/Api/Request/Organizations/OrganizationSponsorshipRedeemRequestModel.cs b/src/Core/Models/Api/Request/Organizations/OrganizationSponsorshipRedeemRequestModel.cs index a23ac5f9a4..08012746e5 100644 --- a/src/Core/Models/Api/Request/Organizations/OrganizationSponsorshipRedeemRequestModel.cs +++ b/src/Core/Models/Api/Request/Organizations/OrganizationSponsorshipRedeemRequestModel.cs @@ -1,10 +1,13 @@ 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 index 646b5d2128..e04e47bc84 100644 --- a/src/Core/Models/Api/Request/Organizations/OrganizationSponsorshipRequestModel.cs +++ b/src/Core/Models/Api/Request/Organizations/OrganizationSponsorshipRequestModel.cs @@ -16,6 +16,9 @@ namespace Bit.Core.Models.Api.Request [Required] [StringLength(256)] [StrictEmailAddress] - public string sponsoredEmail { get; set; } + 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 bd2124fe94..5058f6fa0e 100644 --- a/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs +++ b/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs @@ -38,6 +38,7 @@ namespace Bit.Core.Models.Api UserId = organization.UserId?.ToString(); ProviderId = organization.ProviderId?.ToString(); ProviderName = organization.ProviderName; + FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName; } public string Id { get; set; } @@ -68,5 +69,6 @@ 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; } } } diff --git a/src/Core/Models/Business/SponsoredOrganizationSubscription.cs b/src/Core/Models/Business/SponsoredOrganizationSubscription.cs new file mode 100644 index 0000000000..30ea3e047b --- /dev/null +++ b/src/Core/Models/Business/SponsoredOrganizationSubscription.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using Bit.Core.Models.Table; + +namespace Bit.Core.Models.Business +{ + public class SponsoredOrganizationSubscription + { + public const string OrganizationSponsorhipIdMetadataKey = "OrganizationSponsorshipId"; + private readonly string _customerId; + private readonly Organization _org; + private readonly StaticStore.Plan _plan; + private readonly List _taxRates; + + public SponsoredOrganizationSubscription(Organization org, Stripe.Subscription existingSubscription) + { + _org = org; + _customerId = org.GatewayCustomerId; + _plan = Utilities.StaticStore.GetPlan(org.PlanType); + _taxRates = existingSubscription.DefaultTaxRates; + } + + public SponsorOrganizationSubscriptionOptions GetSponsorSubscriptionOptions(OrganizationSponsorship sponsorship, + int additionalSeats = 0, int additionalStorageGb = 0, bool premiumAccessAddon = false) + { + var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value); + + var subCreateOptions = new SponsorOrganizationSubscriptionOptions(_customerId, _org, _plan, + sponsoredPlan, _taxRates, additionalSeats, additionalStorageGb, premiumAccessAddon); + + subCreateOptions.Metadata.Add(OrganizationSponsorhipIdMetadataKey, sponsorship.Id.ToString()); + return subCreateOptions; + } + + public OrganizationUpgradeSubscriptionOptions RemoveOrganizationSubscriptionOptions(int additionalSeats = 0, + int additionalStorageGb = 0, bool premiumAccessAddon = false) => + new OrganizationUpgradeSubscriptionOptions(_customerId, _org, _plan, _taxRates, + additionalSeats, additionalStorageGb, premiumAccessAddon); + } +} diff --git a/src/Core/Models/Business/SubscriptionCreateOptions.cs b/src/Core/Models/Business/SubscriptionCreateOptions.cs index 43f5cc3672..bcd95c8c25 100644 --- a/src/Core/Models/Business/SubscriptionCreateOptions.cs +++ b/src/Core/Models/Business/SubscriptionCreateOptions.cs @@ -1,12 +1,14 @@ using Bit.Core.Models.Table; using Stripe; using System.Collections.Generic; +using System.Linq; namespace Bit.Core.Models.Business { public class OrganizationSubscriptionOptionsBase : Stripe.SubscriptionCreateOptions { - public OrganizationSubscriptionOptionsBase(Organization org, StaticStore.Plan plan, TaxInfo taxInfo, int additionalSeats, int additionalStorageGb, bool premiumAccessAddon) + public OrganizationSubscriptionOptionsBase(Organization org, StaticStore.Plan plan, + int additionalSeats, int additionalStorageGb, bool premiumAccessAddon) { Items = new List(); Metadata = new Dictionary @@ -14,15 +16,6 @@ namespace Bit.Core.Models.Business [org.GatewayIdField()] = org.Id.ToString() }; - if (plan.StripePlanId != null) - { - Items.Add(new SubscriptionItemOptions - { - Plan = plan.StripePlanId, - Quantity = 1 - }); - } - if (additionalSeats > 0 && plan.StripeSeatPlanId != null) { Items.Add(new SubscriptionItemOptions @@ -49,15 +42,53 @@ namespace Bit.Core.Models.Business Quantity = 1 }); } + } - if (!string.IsNullOrWhiteSpace(taxInfo?.StripeTaxRateId)) + protected void AddPlanItem(StaticStore.Plan plan) => AddPlanItem(plan.StripePlanId); + protected void AddPlanItem(StaticStore.SponsoredPlan sponsoredPlan) => AddPlanItem(sponsoredPlan.StripePlanId); + protected void AddPlanItem(string stripePlanId) + { + if (stripePlanId != null) { - DefaultTaxRates = new List{ taxInfo.StripeTaxRateId }; + Items.Add(new SubscriptionItemOptions + { + Plan = stripePlanId, + Quantity = 1, + }); + } + } + + protected void AddTaxRateItem(TaxInfo taxInfo) => AddTaxRateItem(new List { taxInfo.StripeTaxRateId }); + protected void AddTaxRateItem(List taxRates) => AddTaxRateItem(taxRates?.Select(t => t.Id).ToList()); + protected void AddTaxRateItem(List taxRateIds) + { + if (taxRateIds != null && taxRateIds.Any()) + { + DefaultTaxRates = taxRateIds; } } } - public class OrganizationPurchaseSubscriptionOptions : OrganizationSubscriptionOptionsBase + public abstract class UnsponsoredOrganizationSubscriptionOptionsBase : OrganizationSubscriptionOptionsBase + { + public UnsponsoredOrganizationSubscriptionOptionsBase(Organization org, StaticStore.Plan plan, TaxInfo taxInfo, + int additionalSeats, int additionalStorage, bool premiumAccessAddon) : + base(org, plan, additionalSeats, additionalStorage, premiumAccessAddon) + { + AddPlanItem(plan); + AddTaxRateItem(taxInfo); + } + public UnsponsoredOrganizationSubscriptionOptionsBase(Organization org, StaticStore.Plan plan, List taxInfo, + int additionalSeats, int additionalStorage, bool premiumAccessAddon) : + base(org, plan, additionalSeats, additionalStorage, premiumAccessAddon) + { + AddPlanItem(plan); + AddTaxRateItem(taxInfo); + } + + } + + public class OrganizationPurchaseSubscriptionOptions : UnsponsoredOrganizationSubscriptionOptionsBase { public OrganizationPurchaseSubscriptionOptions( Organization org, StaticStore.Plan plan, @@ -70,7 +101,7 @@ namespace Bit.Core.Models.Business } } - public class OrganizationUpgradeSubscriptionOptions : OrganizationSubscriptionOptionsBase + public class OrganizationUpgradeSubscriptionOptions : UnsponsoredOrganizationSubscriptionOptionsBase { public OrganizationUpgradeSubscriptionOptions( string customerId, Organization org, @@ -81,5 +112,43 @@ namespace Bit.Core.Models.Business { Customer = customerId; } + public OrganizationUpgradeSubscriptionOptions( + string customerId, Organization org, + StaticStore.Plan plan, List taxInfo, + int additionalSeats = 0, int additionalStorageGb = 0, + bool premiumAccessAddon = false) : + base(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon) + { + Customer = customerId; + } + } + + public class RemoveOrganizationSubscriptionOptions : OrganizationSubscriptionOptionsBase + { + public RemoveOrganizationSubscriptionOptions(string customerId, Organization org, + StaticStore.Plan plan, List existingTaxRateStripeIds, + int additionalSeats = 0, int additionalStorageGb = 0, + bool premiumAccessAddon = false) : + base(org, plan, additionalSeats, additionalStorageGb, premiumAccessAddon) + { + Customer = customerId; + AddPlanItem(plan); + AddTaxRateItem(existingTaxRateStripeIds); + } + + } + + public class SponsorOrganizationSubscriptionOptions : OrganizationSubscriptionOptionsBase + { + public SponsorOrganizationSubscriptionOptions( + string customerId, Organization org, StaticStore.Plan existingPlan, + StaticStore.SponsoredPlan sponsorshipPlan, List existingTaxRates, int additionalSeats = 0, + int additionalStorageGb = 0, bool premiumAccessAddon = false) : + base(org, existingPlan, additionalSeats, additionalStorageGb, premiumAccessAddon) + { + Customer = customerId; + AddPlanItem(sponsorshipPlan); + AddTaxRateItem(existingTaxRates); + } } } diff --git a/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs b/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs index dc9e4dbfa4..71aa790033 100644 --- a/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs +++ b/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs @@ -33,5 +33,6 @@ 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; } } } diff --git a/src/Core/Models/StaticStore/SponsoredPlan.cs b/src/Core/Models/StaticStore/SponsoredPlan.cs new file mode 100644 index 0000000000..782775255f --- /dev/null +++ b/src/Core/Models/StaticStore/SponsoredPlan.cs @@ -0,0 +1,12 @@ +using Bit.Core.Enums; + +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; } + } +} diff --git a/src/Core/Models/Table/OrganizationSponsorship.cs b/src/Core/Models/Table/OrganizationSponsorship.cs index 47bded5be2..11be3c66c4 100644 --- a/src/Core/Models/Table/OrganizationSponsorship.cs +++ b/src/Core/Models/Table/OrganizationSponsorship.cs @@ -9,12 +9,12 @@ namespace Bit.Core.Models.Table { public Guid Id { get; set; } public Guid? InstallationId { get; set; } - [Required] - public Guid SponsoringOrganizationId { get; set; } - [Required] - public Guid SponsoringOrganizationUserId { 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] diff --git a/src/Core/Repositories/EntityFramework/DatabaseContext.cs b/src/Core/Repositories/EntityFramework/DatabaseContext.cs index d352e69803..0131ab1c89 100644 --- a/src/Core/Repositories/EntityFramework/DatabaseContext.cs +++ b/src/Core/Repositories/EntityFramework/DatabaseContext.cs @@ -26,7 +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 OrganizationSponsorships { get; set; } public DbSet OrganizationUsers { get; set; } public DbSet Policies { get; set; } public DbSet Providers { get; set; } diff --git a/src/Core/Repositories/EntityFramework/OrganizationRepository.cs b/src/Core/Repositories/EntityFramework/OrganizationRepository.cs index 2f7c5781d5..b7e9ccbaf6 100644 --- a/src/Core/Repositories/EntityFramework/OrganizationRepository.cs +++ b/src/Core/Repositories/EntityFramework/OrganizationRepository.cs @@ -95,5 +95,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 orgUser = 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(orgUser); + await dbContext.SaveChangesAsync(); + } + } } } diff --git a/src/Core/Repositories/EntityFramework/OrganizationSponsorshipRepository.cs b/src/Core/Repositories/EntityFramework/OrganizationSponsorshipRepository.cs index 611ede3cde..ee41c80a57 100644 --- a/src/Core/Repositories/EntityFramework/OrganizationSponsorshipRepository.cs +++ b/src/Core/Repositories/EntityFramework/OrganizationSponsorshipRepository.cs @@ -13,7 +13,7 @@ namespace Bit.Core.Repositories.EntityFramework public class OrganizationSponsorshipRepository : Repository, IOrganizationSponsorshipRepository { public OrganizationSponsorshipRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) : - base(serviceScopeFactory, mapper, (DatabaseContext context) => context.organizationSponsorships) + base(serviceScopeFactory, mapper, (DatabaseContext context) => context.OrganizationSponsorships) { } diff --git a/src/Core/Repositories/EntityFramework/OrganizationUserRepository.cs b/src/Core/Repositories/EntityFramework/OrganizationUserRepository.cs index 17ea260b46..2be24d83b1 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 = 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 1fff508518..95b7a9c99f 100644 --- a/src/Core/Repositories/EntityFramework/Queries/OrganizationUserOrganizationDetailsViewQuery.cs +++ b/src/Core/Repositories/EntityFramework/Queries/OrganizationUserOrganizationDetailsViewQuery.cs @@ -16,8 +16,10 @@ 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() where ((su == null || !su.OrganizationId.HasValue) || su.OrganizationId == ou.OrganizationId) - select new { ou, o, su, p }; + select new { ou, o, su, p, os }; return query.Select(x => new OrganizationUserOrganizationDetails { OrganizationId = x.ou.OrganizationId, @@ -48,6 +50,7 @@ namespace Bit.Core.Repositories.EntityFramework.Queries PrivateKey = x.o.PrivateKey, ProviderId = x.p.Id, ProviderName = x.p.Name, + FamilySponsorshipFriendlyName = x.os.FriendlyName }); } } diff --git a/src/Core/Services/IOrganizationSponsorshipService.cs b/src/Core/Services/IOrganizationSponsorshipService.cs index cd2023f176..78a47092c3 100644 --- a/src/Core/Services/IOrganizationSponsorshipService.cs +++ b/src/Core/Services/IOrganizationSponsorshipService.cs @@ -1,3 +1,4 @@ +using System; using System.Threading.Tasks; using Bit.Core.Enums; using Bit.Core.Models.Table; @@ -7,8 +8,10 @@ namespace Bit.Core.Services public interface IOrganizationSponsorshipService { Task ValidateRedemptionTokenAsync(string encryptedToken); - Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, PlanSponsorshipType sponsorshipType, string sponsoredEmail); + Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, + PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName); Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship, Organization sponsoredOrganization); - Task RemoveSponsorshipAsync(OrganizationSponsorship sponsorship); + Task ValidateSponsorshipAsync(Guid sponsoredOrganizationId); + Task RemoveSponsorshipAsync(Organization sponsoredOrganization, OrganizationSponsorship sponsorship); } } diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index 5f4a5ecc36..4227934528 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); + 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/OrganizationSponsorshipService.cs b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs index 2e4984374f..1e1bff3c62 100644 --- a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs +++ b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Bit.Core.Enums; +using Bit.Core.Exceptions; using Bit.Core.Models.Table; using Bit.Core.Repositories; using Microsoft.AspNetCore.DataProtection; @@ -13,12 +14,18 @@ namespace Bit.Core.Services private const string TokenClearTextPrefix = "BWOrganizationSponsorship_"; private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository; + private readonly IOrganizationRepository _organizationRepository; + private readonly IPaymentService _paymentService; private readonly IDataProtector _dataProtector; public OrganizationSponsorshipService(IOrganizationSponsorshipRepository organizationSponsorshipRepository, + IOrganizationRepository organizationRepository, + IPaymentService paymentService, IDataProtector dataProtector) { _organizationSponsorshipRepository = organizationSponsorshipRepository; + _organizationRepository = organizationRepository; + _paymentService = paymentService; _dataProtector = dataProtector; } @@ -63,13 +70,16 @@ namespace Bit.Core.Services _dataProtector.Protect($"{FamiliesForEnterpriseTokenName} {sponsorshipId} {sponsorshipType}") ); - public async Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, PlanSponsorshipType sponsorshipType, string sponsoredEmail) + public async Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, + PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName) { var sponsorship = new OrganizationSponsorship { SponsoringOrganizationId = sponsoringOrg.Id, SponsoringOrganizationUserId = sponsoringOrgUser.Id, + FriendlyName = friendlyName, OfferedToEmail = sponsoredEmail, + PlanSponsorshipType = sponsorshipType, CloudSponsor = true, }; @@ -78,6 +88,7 @@ namespace Bit.Core.Services sponsorship = await _organizationSponsorshipRepository.CreateAsync(sponsorship); // TODO: send email to sponsoredEmail w/ redemption token link + var _ = RedemptionToken(sponsorship.Id, sponsorshipType); } catch { @@ -91,14 +102,117 @@ namespace Bit.Core.Services public async Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship, Organization sponsoredOrganization) { - // TODO: set up sponsorship, remember remove offeredToEmail from sponsorship - throw new NotImplementedException(); + if (sponsorship.PlanSponsorshipType == null) + { + throw new BadRequestException("Cannot set up sponsorship without a known sponsorship type."); + } + + // TODO: rollback? + await _paymentService.SponsorOrganizationAsync(sponsoredOrganization, sponsorship); + await _organizationRepository.UpsertAsync(sponsoredOrganization); + + sponsorship.SponsoredOrganizationId = sponsoredOrganization.Id; + sponsorship.OfferedToEmail = null; + await _organizationSponsorshipRepository.UpsertAsync(sponsorship); } - public async Task RemoveSponsorshipAsync(OrganizationSponsorship sponsorship) + public async Task ValidateSponsorshipAsync(Guid sponsoredOrganizationId) { - // TODO: remove sponsorship - throw new NotImplementedException(); + var sponsoredOrganization = await _organizationRepository.GetByIdAsync(sponsoredOrganizationId); + var existingSponsorship = await _organizationSponsorshipRepository + .GetBySponsoredOrganizationIdAsync(sponsoredOrganizationId); + + if (existingSponsorship == null) + { + await RemoveSponsorshipAsync(sponsoredOrganization); + // TODO on fail, mark org as disabled. + return false; + } + + var validated = true; + if (existingSponsorship.SponsoringOrganizationId == null || existingSponsorship.SponsoringOrganizationUserId == null) + { + await RemoveSponsorshipAsync(sponsoredOrganization); + validated = false; + } + + var sponsoringOrganization = await _organizationRepository + .GetByIdAsync(existingSponsorship.SponsoringOrganizationId.Value); + if (!sponsoringOrganization.Enabled) + { + await RemoveSponsorshipAsync(sponsoredOrganization); + validated = false; + } + + if (!validated && existingSponsorship.SponsoredOrganizationId != null) + { + existingSponsorship.TimesRenewedWithoutValidation += 1; + existingSponsorship.SponsorshipLapsedDate ??= DateTime.UtcNow; + + await _organizationSponsorshipRepository.UpsertAsync(existingSponsorship); + if (existingSponsorship.TimesRenewedWithoutValidation >= 6) + { + sponsoredOrganization.Enabled = false; + await _organizationRepository.UpsertAsync(sponsoredOrganization); + } + } + + return true; + } + + public async Task RemoveSponsorshipAsync(Organization sponsoredOrganization, OrganizationSponsorship sponsorship = null) + { + var success = await _paymentService.RemoveOrganizationSponsorshipAsync(sponsoredOrganization); + await _organizationRepository.UpsertAsync(sponsoredOrganization); + + if (sponsorship == null) + { + return; + } + + if (success) + { + // 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); + } + } + else + { + sponsorship.SponsoringOrganizationId = null; + sponsorship.SponsoringOrganizationUserId = null; + + if (!sponsorship.CloudSponsor) + { + // Sef-hosted sponsorship record + // we need to make the existing sponsorship available, and add + // a new sponsorship record to record the lapsed sponsorship + var cleanSponsorship = new OrganizationSponsorship + { + InstallationId = sponsorship.InstallationId, + SponsoringOrganizationId = sponsorship.SponsoringOrganizationId, + SponsoringOrganizationUserId = sponsorship.SponsoringOrganizationUserId, + CloudSponsor = sponsorship.CloudSponsor, + }; + await _organizationSponsorshipRepository.UpsertAsync(cleanSponsorship); + } + + sponsorship.SponsorshipLapsedDate ??= DateTime.UtcNow; + await _organizationSponsorshipRepository.UpsertAsync(sponsorship); + } + } } diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 690391d78b..b4460d6020 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -192,6 +192,44 @@ namespace Bit.Core.Services } } + public async Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship) + { + var customer = await _stripeAdapter.CustomerGetAsync(org.GatewayCustomerId); + var sub = await _stripeAdapter.SubscriptionGetAsync(org.GatewaySubscriptionId); + + var sponsoredSubscription = new SponsoredOrganizationSubscription(org, sub); + + var subscription = await ChargeForNewSubscriptionAsync(org, customer, false, + false, PaymentMethodType.None, sponsoredSubscription.GetSponsorSubscriptionOptions(sponsorship), null); + org.GatewaySubscriptionId = subscription.Id; + + org.ExpirationDate = subscription.CurrentPeriodEnd; + } + + public async Task RemoveOrganizationSponsorshipAsync(Organization org) + { + var customer = await _stripeAdapter.CustomerGetAsync(org.GatewayCustomerId); + var sub = await _stripeAdapter.SubscriptionGetAsync(org.GatewaySubscriptionId); + + var sponsoredSubscription = new SponsoredOrganizationSubscription(org, sub); + var subCreateOptions = sponsoredSubscription.RemoveOrganizationSubscriptionOptions(); + + var (stripePaymentMethod, paymentMethodType) = IdentifyPaymentMethod(customer, subCreateOptions); + var subscription = await ChargeForNewSubscriptionAsync(org, customer, false, + stripePaymentMethod, paymentMethodType, subCreateOptions, null); + + if (subscription.Status == "incomplete") + { + // TODO: revert + return false; + } + org.GatewaySubscriptionId = subscription.Id; + org.Enabled = true; + org.ExpirationDate = subscription.CurrentPeriodEnd; + + return true; + } + public async Task UpgradeFreeOrganizationAsync(Organization org, StaticStore.Plan plan, short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo) { @@ -227,6 +265,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 +326,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, diff --git a/src/Core/Utilities/StaticStore.cs b/src/Core/Utilities/StaticStore.cs index f809adc062..3607332fcc 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.StaticStore; +using Bit.Core.Models.Table; using System.Collections.Generic; +using System.Linq; namespace Bit.Core.Utilities { @@ -475,5 +477,19 @@ 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-enterprise-sponsored-families-org-monthly" + } + }; + 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/dbo/Stored Procedures/OrganizationSponsorship_OrganizationDeleted.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationDeleted.sql new file mode 100644 index 0000000000..f7685b7e17 --- /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 0000000000..a324b76d32 --- /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 0000000000..80457b3fd5 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_OrganizationUsersDeleted.sql @@ -0,0 +1,24 @@ +CREATE PROCEDURE [dbo].[OrganizationSponsorship_OrganizationUsersDeleted] + @SponsoringOrganizationUserIds [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + SET @BatchSize = 100; + + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION OrganizationSponsorship_DeleteOUs + + DELETE TOP(@BatchSize) OS + FROM + [dbo].[OrganiozationSponsorship] OS + INNER JOIN + @Ids I ON I.Id = OS.SponsoringOrganizationUserId + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION OrganizationSponsorship_DeleteOUs + END +END +GO diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteById.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteById.sql index be2a12eeb0..5817e49394 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteById.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteById.sql @@ -34,9 +34,19 @@ BEGIN WHERE [OrganizationUserId] = @Id + EXEC [dbo].[OrganizationUser_DeleteById] @Id + DELETE FROM [dbo].[OrganizationUser] WHERE [Id] = @Id -END \ No newline at end of file +END +GO + + +IF OBJECT_ID('[dbo].[OrganizationUser_DeleteByIds]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationUser_DeleteByIds] +END +GO diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteByIds.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteByIds.sql index 049a2c5c0c..4930a8fbed 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 f19b050e72..3015179290 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] @@ -65,3 +67,4 @@ BEGIN COMMIT TRANSACTION Organization_DeleteById END +GO diff --git a/src/Sql/dbo/Tables/OrganizationSponsorship.sql b/src/Sql/dbo/Tables/OrganizationSponsorship.sql index 9467297b88..391ab599b6 100644 --- a/src/Sql/dbo/Tables/OrganizationSponsorship.sql +++ b/src/Sql/dbo/Tables/OrganizationSponsorship.sql @@ -1,8 +1,8 @@ CREATE TABLE [dbo].[OrganizationSponsorship] ( [Id] UNIQUEIDENTIFIER NOT NULL, [InstallationId] UNIQUEIDENTIFIER NULL, - [SponsoringOrganizationId] UNIQUEIDENTIFIER NOT NULL, - [SponsoringOrganizationUserID] UNIQUEIDENTIFIER NOT NULL, + [SponsoringOrganizationId] UNIQUEIDENTIFIER NULL, + [SponsoringOrganizationUserID] UNIQUEIDENTIFIER NULL, [SponsoredOrganizationId] UNIQUEIDENTIFIER NULL, [OfferedToEmail] NVARCHAR (256) NULL, [PlanSponsorshipType] TINYINT NULL, @@ -19,24 +19,25 @@ CREATE TABLE [dbo].[OrganizationSponsorship] ( GO CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_InstallationId] - ON [dbo].[Organization]([Id] ASC, [InstallationId] ASC) + ON [dbo].[OrganizationSponsorship]([InstallationId] ASC) WHERE [InstallationId] IS NOT NULL; GO CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationId] - ON [dbo].[Organization]([Id] ASC, [SponsoringOrganizationId] ASC) + ON [dbo].[OrganizationSponsorship]([SponsoringOrganizationId] ASC) + WHERE [SponsoringOrganizationId] IS NOT NULL; GO CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationUserId] - ON [dbo].[Organization]([Id] ASC, [SponsorginOrganizationUserID] ASC) + ON [dbo].[OrganizationSponsorship]([SponsoringOrganizationUserID] ASC) + WHERE [SponsoringOrganizationUserID] IS NOT NULL; GO CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_OfferedToEmail] - ON [dbo].[Organization]([Id] ASC, [OfferedToEmail] ASC) + ON [dbo].[OrganizationSponsorship]([OfferedToEmail] ASC) WHERE [OfferedToEmail] IS NOT NULL; GO CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoredOrganizationID] - ON [dbo].[Organization]([Id] ASC, [SponsoredOrganizationId] ASC) + ON [dbo].[OrganizationSponsorship]([SponsoredOrganizationId] ASC) WHERE [SponsoredOrganizationId] IS NOT NULL; - diff --git a/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs index 2f1434ba97..f019ade201 100644 --- a/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs +++ b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs @@ -16,6 +16,7 @@ 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 { @@ -24,27 +25,29 @@ namespace Bit.Api.Test.Controllers public class OrganizationSponsorshipsControllerTests { public static IEnumerable EnterprisePlanTypes => - Enum.GetValues().Where(p => PlanTypeHelper.IsEnterprise(p)).Select(p => new object[] { p }); + Enum.GetValues().Where(p => StaticStore.GetPlan(p).Product == ProductType.Enterprise).Select(p => new object[] { p }); public static IEnumerable NonEnterprisePlanTypes => - Enum.GetValues().Where(p => !PlanTypeHelper.IsEnterprise(p)).Select(p => new object[] { p }); + Enum.GetValues().Where(p => StaticStore.GetPlan(p).Product != ProductType.Enterprise).Select(p => new object[] { p }); public static IEnumerable NonFamiliesPlanTypes => - Enum.GetValues().Where(p => !PlanTypeHelper.IsFamilies(p)).Select(p => new object[] { p }); + Enum.GetValues().Where(p => StaticStore.GetPlan(p).Product != ProductType.Families).Select(p => new object[] { p }); [Theory] [BitMemberAutoData(nameof(NonEnterprisePlanTypes))] public async Task CreateSponsorship_BadSponsoringOrgPlan_ThrowsBadRequest(PlanType sponsoringOrgPlan, Organization org, - SutProvider sutProvider) + OrganizationSponsorshipRequestModel model, SutProvider sutProvider) { org.PlanType = sponsoringOrgPlan; + model.PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise; + sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.CreateSponsorship(org.Id.ToString(), null)); + sutProvider.Sut.CreateSponsorship(org.Id.ToString(), model)); Assert.Contains("Specified Organization cannot sponsor other organizations.", exception.Message); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() - .OfferSponsorshipAsync(default, default, default, default); + .OfferSponsorshipAsync(default, default, default, default, default); } public static IEnumerable NonConfirmedOrganizationUsersStatuses => @@ -73,7 +76,7 @@ namespace Bit.Api.Test.Controllers Assert.Contains("Only confirm users can sponsor other organizations.", exception.Message); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() - .OfferSponsorshipAsync(default, default, default, default); + .OfferSponsorshipAsync(default, default, default, default, default); } [Theory] @@ -96,7 +99,7 @@ namespace Bit.Api.Test.Controllers Assert.Contains("Can only create organization sponsorships for yourself.", exception.Message); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() - .OfferSponsorshipAsync(default, default, default, default); + .OfferSponsorshipAsync(default, default, default, default, default); } [Theory] @@ -121,7 +124,7 @@ namespace Bit.Api.Test.Controllers Assert.Contains("Can only sponsor one organization per Organization User.", exception.Message); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() - .OfferSponsorshipAsync(default, default, default, default); + .OfferSponsorshipAsync(default, default, default, default, default); } [Theory] @@ -272,7 +275,7 @@ namespace Bit.Api.Test.Controllers Assert.Contains("Can only revoke a sponsorship you granted.", exception.Message); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() - .RemoveSponsorshipAsync(default); + .RemoveSponsorshipAsync(default, default); } [Theory] @@ -293,10 +296,58 @@ namespace Bit.Api.Test.Controllers var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.RevokeSponsorship(sponsoringOrgUser.Id.ToString())); - Assert.Contains("You are not currently sponsoring and organization.", exception.Message); + Assert.Contains("You are not currently sponsoring an organization.", exception.Message); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() - .RemoveSponsorshipAsync(default); + .RemoveSponsorshipAsync(default, default); + } + + [Theory] + [BitAutoData] + public async Task RevokeSponsorship_SponsorshipNotRedeemed_ThrowsBadRequest(OrganizationUser sponsoringOrgUser, + OrganizationSponsorship sponsorship, SutProvider sutProvider) + { + sponsorship.SponsoredOrganizationId = null; + + sutProvider.GetDependency().UserId.Returns(sponsoringOrgUser.UserId); + sutProvider.GetDependency().GetByIdAsync(sponsoringOrgUser.Id) + .Returns(sponsoringOrgUser); + sutProvider.GetDependency() + .GetBySponsoringOrganizationUserIdAsync(Arg.Is(v => v != sponsoringOrgUser.Id)) + .Returns(sponsorship); + sutProvider.GetDependency() + .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id) + .Returns((OrganizationSponsorship)sponsorship); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RevokeSponsorship(sponsoringOrgUser.Id.ToString())); + + Assert.Contains("You are not currently sponsoring an organization.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .RemoveSponsorshipAsync(default, default); + } + + [Theory] + [BitAutoData] + public async Task RevokeSponsorship_SponsoredOrgNotFound_ThrowsBadRequest(OrganizationUser sponsoringOrgUser, + OrganizationSponsorship sponsorship, SutProvider sutProvider) + { + + sutProvider.GetDependency().UserId.Returns(sponsoringOrgUser.UserId); + sutProvider.GetDependency().GetByIdAsync(sponsoringOrgUser.Id) + .Returns(sponsoringOrgUser); + sutProvider.GetDependency() + .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id) + .Returns(sponsorship); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RevokeSponsorship(sponsoringOrgUser.Id.ToString())); + + Assert.Contains("Unable to find the sponsored Organization.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .RemoveSponsorshipAsync(default, default); } [Theory] @@ -312,7 +363,7 @@ namespace Bit.Api.Test.Controllers Assert.Contains("Only the owner of an organization can remove sponsorship.", exception.Message); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() - .RemoveSponsorshipAsync(default); + .RemoveSponsorshipAsync(default, default); } [Theory] @@ -334,7 +385,26 @@ namespace Bit.Api.Test.Controllers Assert.Contains("The requested organization is not currently being sponsored.", exception.Message); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() - .RemoveSponsorshipAsync(default); + .RemoveSponsorshipAsync(default, default); + } + + [Theory] + [BitAutoData] + public async Task RemoveSponsorship_SponsoredOrgNotFound_ThrowsBadRequest(Organization sponsoredOrg, + OrganizationSponsorship sponsorship, SutProvider sutProvider) + { + sutProvider.GetDependency().OrganizationOwner(Arg.Any()).Returns(true); + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id) + .Returns(sponsorship); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RemoveSponsorship(sponsoredOrg.Id.ToString())); + + Assert.Contains("Unable to find the sponsored Organization.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .RemoveSponsorshipAsync(default, default); } } } diff --git a/test/Core.Test/Enums/PlanTypeHelperTests.cs b/test/Core.Test/Enums/PlanTypeHelperTests.cs deleted file mode 100644 index d8af51f1cb..0000000000 --- a/test/Core.Test/Enums/PlanTypeHelperTests.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Linq; -using Bit.Core.Enums; -using Bit.Core.Models.Table; -using Xunit; - -namespace Bit.Core.Test.Enums -{ - public class PlanTypeHelperTests - { - private static IEnumerable PlanArchetypeArray(PlanType planType) => new PlanType?[] { - PlanTypeHelper.HasFreePlan(new Organization {PlanType = planType}) ? planType : null, - PlanTypeHelper.HasFamiliesPlan(new Organization {PlanType = planType}) ? planType : null, - PlanTypeHelper.HasTeamsPlan(new Organization {PlanType = planType}) ? planType : null, - PlanTypeHelper.HasEnterprisePlan(new Organization {PlanType = planType}) ? planType : null, - }.Where(v => v.HasValue).Select(v => (PlanType)v); - - public static IEnumerable PlanTypes => Enum.GetValues().Select(p => new object[] { p }); - public static IEnumerable PlanTypesExceptCustom => - Enum.GetValues().Except(new[] { PlanType.Custom }).Select(p => new object[] { p }); - - [Theory] - [MemberData(nameof(PlanTypesExceptCustom))] - public void NonCustomPlanTypesBelongToPlanArchetype(PlanType planType) - { - Assert.Contains(planType, PlanArchetypeArray(planType)); - } - - [Theory] - [MemberData(nameof(PlanTypesExceptCustom))] - public void PlanTypesBelongToOnlyOneArchetype(PlanType planType) - { - Console.WriteLine(planType); - Assert.Single(PlanArchetypeArray(planType)); - } - } -} diff --git a/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs b/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs index f916756919..4fc25b7f8c 100644 --- a/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs +++ b/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs @@ -34,15 +34,16 @@ namespace Bit.Core.Test.Services [Theory] [BitAutoData] public async Task OfferSponsorship_CreatesSponsorship(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, - string sponsoredEmail, SutProvider sutProvider) + string sponsoredEmail, string friendlyName, SutProvider sutProvider) { await sutProvider.Sut.OfferSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, - PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail); + PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName); var expectedSponsorship = new OrganizationSponsorship { SponsoringOrganizationId = sponsoringOrg.Id, SponsoringOrganizationUserId = sponsoringOrgUser.Id, + FriendlyName = friendlyName, OfferedToEmail = sponsoredEmail, CloudSponsor = true, }; @@ -55,7 +56,7 @@ namespace Bit.Core.Test.Services [Theory] [BitAutoData] public async Task OfferSponsorship_CreateSponsorshipThrows_RevertsDatabase(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, - string sponsoredEmail, SutProvider sutProvider) + string sponsoredEmail, string friendlyName, SutProvider sutProvider) { var expectedException = new Exception(); OrganizationSponsorship createdSponsorship = null; @@ -68,7 +69,7 @@ namespace Bit.Core.Test.Services var actualException = await Assert.ThrowsAsync(() => sutProvider.Sut.OfferSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, - PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail)); + PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName)); Assert.Same(expectedException, actualException); await sutProvider.GetDependency().Received(1) diff --git a/util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql b/util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql index 8a0bc78093..7c10a47bb7 100644 --- a/util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql +++ b/util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql @@ -4,9 +4,10 @@ BEGIN CREATE TABLE [dbo].[OrganizationSponsorship] ( [Id] UNIQUEIDENTIFIER NOT NULL, [InstallationId] UNIQUEIDENTIFIER NULL, - [SponsoringOrganizationId] UNIQUEIDENTIFIER NOT NULL, - [SponsoringOrganizationUserID] UNIQUEIDENTIFIER NOT NULL, + [SponsoringOrganizationId] UNIQUEIDENTIFIER NULL, + [SponsoringOrganizationUserID] UNIQUEIDENTIFIER NULL, [SponsoredOrganizationId] UNIQUEIDENTIFIER NULL, + [FriendlyName] NVARCHAR(256) NULL, [OfferedToEmail] NVARCHAR (256) NULL, [PlanSponsorshipType] TINYINT NULL, [CloudSponsor] BIT NULL, @@ -35,6 +36,7 @@ IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationSponsors BEGIN CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationId] ON [dbo].[OrganizationSponsorship]([SponsoringOrganizationId] ASC) + WHERE [SponsoringOrganizationId] IS NOT NULL; END GO @@ -42,6 +44,7 @@ IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationSponsors BEGIN CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationUserId] ON [dbo].[OrganizationSponsorship]([SponsoringOrganizationUserID] ASC) + WHERE [SponsoringOrganizationUserID] IS NOT NULL; END GO @@ -114,6 +117,7 @@ CREATE PROCEDURE [dbo].[OrganizationSponsorship_Create] @SponsoringOrganizationId UNIQUEIDENTIFIER, @SponsoringOrganizationUserID UNIQUEIDENTIFIER, @SponsoredOrganizationId UNIQUEIDENTIFIER, + @FriendlyName NVARCHAR(256), @OfferedToEmail NVARCHAR(256), @PlanSponsorshipType TINYINT, @CloudSponsor BIT, @@ -131,6 +135,7 @@ BEGIN [SponsoringOrganizationId], [SponsoringOrganizationUserID], [SponsoredOrganizationId], + [FriendlyName], [OfferedToEmail], [PlanSponsorshipType], [CloudSponsor], @@ -145,6 +150,7 @@ BEGIN @SponsoringOrganizationId, @SponsoringOrganizationUserID, @SponsoredOrganizationId, + @FriendlyName, @OfferedToEmail, @PlanSponsorshipType, @CloudSponsor, @@ -168,6 +174,7 @@ CREATE PROCEDURE [dbo].[OrganizationSponsorship_Update] @SponsoringOrganizationId UNIQUEIDENTIFIER, @SponsoringOrganizationUserID UNIQUEIDENTIFIER, @SponsoredOrganizationId UNIQUEIDENTIFIER, + @FriendlyName NVARCHAR(256), @OfferedToEmail NVARCHAR(256), @PlanSponsorshipType TINYINT, @CloudSponsor BIT, @@ -185,6 +192,7 @@ BEGIN [SponsoringOrganizationId] = @SponsoringOrganizationId, [SponsoringOrganizationUserID] = @SponsoringOrganizationUserID, [SponsoredOrganizationId] = @SponsoredOrganizationId, + [FriendlyName] = @FriendlyName, [OfferedToEmail] = @OfferedToEmail, [PlanSponsorshipType] = @PlanSponsorshipType, [CloudSponsor] = @CloudSponsor, @@ -290,3 +298,365 @@ BEGIN [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].[OrganizationUser_DeleteById] @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.[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/MySqlMigrations/Migrations/20211104164838_OrganizationSponsorship.Designer.cs b/util/MySqlMigrations/Migrations/20211108225243_OrganizationSponsorship.Designer.cs similarity index 99% rename from util/MySqlMigrations/Migrations/20211104164838_OrganizationSponsorship.Designer.cs rename to util/MySqlMigrations/Migrations/20211108225243_OrganizationSponsorship.Designer.cs index 39646fa67b..4631f9e776 100644 --- a/util/MySqlMigrations/Migrations/20211104164838_OrganizationSponsorship.Designer.cs +++ b/util/MySqlMigrations/Migrations/20211108225243_OrganizationSponsorship.Designer.cs @@ -9,7 +9,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion; namespace Bit.MySqlMigrations.Migrations { [DbContext(typeof(DatabaseContext))] - [Migration("20211104164838_OrganizationSponsorship")] + [Migration("20211108225243_OrganizationSponsorship")] partial class OrganizationSponsorship { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -595,6 +595,10 @@ namespace Bit.MySqlMigrations.Migrations b.Property("CloudSponsor") .HasColumnType("tinyint(1)"); + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + b.Property("InstallationId") .HasColumnType("char(36)"); @@ -611,10 +615,10 @@ namespace Bit.MySqlMigrations.Migrations b.Property("SponsoredOrganizationId") .HasColumnType("char(36)"); - b.Property("SponsoringOrganizationId") + b.Property("SponsoringOrganizationId") .HasColumnType("char(36)"); - b.Property("SponsoringOrganizationUserId") + b.Property("SponsoringOrganizationUserId") .HasColumnType("char(36)"); b.Property("SponsorshipLapsedDate") @@ -1356,9 +1360,7 @@ namespace Bit.MySqlMigrations.Migrations b.HasOne("Bit.Core.Models.EntityFramework.Organization", "SponsoringOrganization") .WithMany() - .HasForeignKey("SponsoringOrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("SponsoringOrganizationId"); b.Navigation("Installation"); diff --git a/util/MySqlMigrations/Migrations/20211104164838_OrganizationSponsorship.cs b/util/MySqlMigrations/Migrations/20211108225243_OrganizationSponsorship.cs similarity index 91% rename from util/MySqlMigrations/Migrations/20211104164838_OrganizationSponsorship.cs rename to util/MySqlMigrations/Migrations/20211108225243_OrganizationSponsorship.cs index 0af0aca109..ae63c29587 100644 --- a/util/MySqlMigrations/Migrations/20211104164838_OrganizationSponsorship.cs +++ b/util/MySqlMigrations/Migrations/20211108225243_OrganizationSponsorship.cs @@ -20,9 +20,11 @@ namespace Bit.MySqlMigrations.Migrations { 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: false, collation: "ascii_general_ci"), - SponsoringOrganizationUserId = table.Column(type: "char(36)", nullable: false, 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), @@ -51,7 +53,7 @@ namespace Bit.MySqlMigrations.Migrations column: x => x.SponsoringOrganizationId, principalTable: "Organization", principalColumn: "Id", - onDelete: ReferentialAction.Cascade); + onDelete: ReferentialAction.Restrict); }) .Annotation("MySql:CharSet", "utf8mb4"); diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index 06545d15f1..1a96cdd89f 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -593,6 +593,10 @@ namespace Bit.MySqlMigrations.Migrations b.Property("CloudSponsor") .HasColumnType("tinyint(1)"); + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + b.Property("InstallationId") .HasColumnType("char(36)"); @@ -609,10 +613,10 @@ namespace Bit.MySqlMigrations.Migrations b.Property("SponsoredOrganizationId") .HasColumnType("char(36)"); - b.Property("SponsoringOrganizationId") + b.Property("SponsoringOrganizationId") .HasColumnType("char(36)"); - b.Property("SponsoringOrganizationUserId") + b.Property("SponsoringOrganizationUserId") .HasColumnType("char(36)"); b.Property("SponsorshipLapsedDate") @@ -1354,9 +1358,7 @@ namespace Bit.MySqlMigrations.Migrations b.HasOne("Bit.Core.Models.EntityFramework.Organization", "SponsoringOrganization") .WithMany() - .HasForeignKey("SponsoringOrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("SponsoringOrganizationId"); b.Navigation("Installation"); diff --git a/util/MySqlMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.sql b/util/MySqlMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.sql index cb1f7c0ffd..5e442e48cd 100644 --- a/util/MySqlMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.sql +++ b/util/MySqlMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.sql @@ -5,9 +5,10 @@ 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 NOT NULL, - `SponsoringOrganizationUserId` char(36) COLLATE ascii_general_ci NOT 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, @@ -17,7 +18,7 @@ CREATE TABLE `OrganizationSponsorship` ( 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 CASCADE + 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`); @@ -27,6 +28,6 @@ CREATE INDEX `IX_OrganizationSponsorship_SponsoredOrganizationId` ON `Organizati CREATE INDEX `IX_OrganizationSponsorship_SponsoringOrganizationId` ON `OrganizationSponsorship` (`SponsoringOrganizationId`); INSERT INTO `__EFMigrationsHistory` (`MigrationId`, `ProductVersion`) -VALUES ('20211104164838_OrganizationSponsorship', '5.0.9'); +VALUES ('20211108225243_OrganizationSponsorship', '5.0.9'); COMMIT; diff --git a/util/PostgresMigrations/Migrations/20211104164532_OrganizationSponsorship.Designer.cs b/util/PostgresMigrations/Migrations/20211108225011_OrganizationSponsorship.Designer.cs similarity index 99% rename from util/PostgresMigrations/Migrations/20211104164532_OrganizationSponsorship.Designer.cs rename to util/PostgresMigrations/Migrations/20211108225011_OrganizationSponsorship.Designer.cs index d383ce3669..d512fb4022 100644 --- a/util/PostgresMigrations/Migrations/20211104164532_OrganizationSponsorship.Designer.cs +++ b/util/PostgresMigrations/Migrations/20211108225011_OrganizationSponsorship.Designer.cs @@ -10,7 +10,7 @@ using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; namespace Bit.PostgresMigrations.Migrations { [DbContext(typeof(DatabaseContext))] - [Migration("20211104164532_OrganizationSponsorship")] + [Migration("20211108225011_OrganizationSponsorship")] partial class OrganizationSponsorship { protected override void BuildTargetModel(ModelBuilder modelBuilder) @@ -599,6 +599,10 @@ namespace Bit.PostgresMigrations.Migrations b.Property("CloudSponsor") .HasColumnType("boolean"); + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + b.Property("InstallationId") .HasColumnType("uuid"); @@ -615,10 +619,10 @@ namespace Bit.PostgresMigrations.Migrations b.Property("SponsoredOrganizationId") .HasColumnType("uuid"); - b.Property("SponsoringOrganizationId") + b.Property("SponsoringOrganizationId") .HasColumnType("uuid"); - b.Property("SponsoringOrganizationUserId") + b.Property("SponsoringOrganizationUserId") .HasColumnType("uuid"); b.Property("SponsorshipLapsedDate") @@ -1365,9 +1369,7 @@ namespace Bit.PostgresMigrations.Migrations b.HasOne("Bit.Core.Models.EntityFramework.Organization", "SponsoringOrganization") .WithMany() - .HasForeignKey("SponsoringOrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("SponsoringOrganizationId"); b.Navigation("Installation"); diff --git a/util/PostgresMigrations/Migrations/20211104164532_OrganizationSponsorship.cs b/util/PostgresMigrations/Migrations/20211108225011_OrganizationSponsorship.cs similarity index 93% rename from util/PostgresMigrations/Migrations/20211104164532_OrganizationSponsorship.cs rename to util/PostgresMigrations/Migrations/20211108225011_OrganizationSponsorship.cs index 0de10d335d..162792a6eb 100644 --- a/util/PostgresMigrations/Migrations/20211104164532_OrganizationSponsorship.cs +++ b/util/PostgresMigrations/Migrations/20211108225011_OrganizationSponsorship.cs @@ -20,9 +20,10 @@ namespace Bit.PostgresMigrations.Migrations { Id = table.Column(type: "uuid", nullable: false), InstallationId = table.Column(type: "uuid", nullable: true), - SponsoringOrganizationId = table.Column(type: "uuid", nullable: false), - SponsoringOrganizationUserId = table.Column(type: "uuid", nullable: false), + 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), @@ -50,7 +51,7 @@ namespace Bit.PostgresMigrations.Migrations column: x => x.SponsoringOrganizationId, principalTable: "Organization", principalColumn: "Id", - onDelete: ReferentialAction.Cascade); + onDelete: ReferentialAction.Restrict); }); migrationBuilder.CreateIndex( diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index 920d128d6a..675c599ca6 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -597,6 +597,10 @@ namespace Bit.PostgresMigrations.Migrations b.Property("CloudSponsor") .HasColumnType("boolean"); + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + b.Property("InstallationId") .HasColumnType("uuid"); @@ -613,10 +617,10 @@ namespace Bit.PostgresMigrations.Migrations b.Property("SponsoredOrganizationId") .HasColumnType("uuid"); - b.Property("SponsoringOrganizationId") + b.Property("SponsoringOrganizationId") .HasColumnType("uuid"); - b.Property("SponsoringOrganizationUserId") + b.Property("SponsoringOrganizationUserId") .HasColumnType("uuid"); b.Property("SponsorshipLapsedDate") @@ -1363,9 +1367,7 @@ namespace Bit.PostgresMigrations.Migrations b.HasOne("Bit.Core.Models.EntityFramework.Organization", "SponsoringOrganization") .WithMany() - .HasForeignKey("SponsoringOrganizationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); + .HasForeignKey("SponsoringOrganizationId"); b.Navigation("Installation"); diff --git a/util/PostgresMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.psql b/util/PostgresMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.psql index 870aff88a1..24d5eaa080 100644 --- a/util/PostgresMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.psql +++ b/util/PostgresMigrations/Scripts/2021-11-02_00_OrganizationSponsorship.psql @@ -5,9 +5,10 @@ ALTER TABLE "User" ADD "UsesCryptoAgent" boolean NOT NULL DEFAULT FALSE; CREATE TABLE "OrganizationSponsorship" ( "Id" uuid NOT NULL, "InstallationId" uuid NULL, - "SponsoringOrganizationId" uuid NOT NULL, - "SponsoringOrganizationUserId" uuid NOT 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, @@ -17,7 +18,7 @@ CREATE TABLE "OrganizationSponsorship" ( 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 CASCADE + CONSTRAINT "FK_OrganizationSponsorship_Organization_SponsoringOrganization~" FOREIGN KEY ("SponsoringOrganizationId") REFERENCES "Organization" ("Id") ON DELETE RESTRICT ); CREATE INDEX "IX_OrganizationSponsorship_InstallationId" ON "OrganizationSponsorship" ("InstallationId"); @@ -27,6 +28,6 @@ CREATE INDEX "IX_OrganizationSponsorship_SponsoredOrganizationId" ON "Organizati CREATE INDEX "IX_OrganizationSponsorship_SponsoringOrganizationId" ON "OrganizationSponsorship" ("SponsoringOrganizationId"); INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") -VALUES ('20211104164532_OrganizationSponsorship', '5.0.9'); +VALUES ('20211108225011_OrganizationSponsorship', '5.0.9'); COMMIT; From b61227d87ebe1ec37697d393cee3aa254c2df3ea Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Mon, 8 Nov 2021 19:06:32 -0500 Subject: [PATCH 15/34] Add sponsorship available boolean to orgDetails --- .../Models/Api/Response/ProfileOrganizationResponseModel.cs | 4 ++++ src/Core/Models/Data/OrganizationUserOrganizationDetails.cs | 1 + src/Core/Models/StaticStore/SponsoredPlan.cs | 3 +++ .../Queries/OrganizationUserOrganizationDetailsViewQuery.cs | 1 + src/Core/Utilities/StaticStore.cs | 6 ++++-- .../DbScripts/2021-11-02_00_OrganizationSponsorship.sql | 1 + 6 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs b/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs index 5058f6fa0e..3a4f90e430 100644 --- a/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs +++ b/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs @@ -39,6 +39,9 @@ namespace Bit.Core.Models.Api ProviderId = organization.ProviderId?.ToString(); ProviderName = organization.ProviderName; FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName; + FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null && + Utilities.StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise) + .UsersCanSponsor(organization); } public string Id { get; set; } @@ -70,5 +73,6 @@ namespace Bit.Core.Models.Api public string ProviderId { get; set; } public string ProviderName { get; set; } public string FamilySponsorshipFriendlyName { get; set; } + public bool FamilySponsorshipAvailable { get; set; } } } diff --git a/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs b/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs index 71aa790033..1fa5369aad 100644 --- a/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs +++ b/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs @@ -25,6 +25,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; } diff --git a/src/Core/Models/StaticStore/SponsoredPlan.cs b/src/Core/Models/StaticStore/SponsoredPlan.cs index 782775255f..f9c7b2b6b5 100644 --- a/src/Core/Models/StaticStore/SponsoredPlan.cs +++ b/src/Core/Models/StaticStore/SponsoredPlan.cs @@ -1,4 +1,6 @@ +using System; using Bit.Core.Enums; +using Bit.Core.Models.Data; namespace Bit.Core.Models.StaticStore { @@ -8,5 +10,6 @@ namespace Bit.Core.Models.StaticStore 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/Repositories/EntityFramework/Queries/OrganizationUserOrganizationDetailsViewQuery.cs b/src/Core/Repositories/EntityFramework/Queries/OrganizationUserOrganizationDetailsViewQuery.cs index 95b7a9c99f..05fb280c87 100644 --- a/src/Core/Repositories/EntityFramework/Queries/OrganizationUserOrganizationDetailsViewQuery.cs +++ b/src/Core/Repositories/EntityFramework/Queries/OrganizationUserOrganizationDetailsViewQuery.cs @@ -26,6 +26,7 @@ namespace Bit.Core.Repositories.EntityFramework.Queries UserId = x.ou.UserId, Name = x.o.Name, Enabled = x.o.Enabled, + PlanType = x.o.PlanType, UsePolicies = x.o.UsePolicies, UseSso = x.o.UseSso, UseGroups = x.o.UseGroups, diff --git a/src/Core/Utilities/StaticStore.cs b/src/Core/Utilities/StaticStore.cs index 3607332fcc..68a3e88574 100644 --- a/src/Core/Utilities/StaticStore.cs +++ b/src/Core/Utilities/StaticStore.cs @@ -1,6 +1,6 @@ using Bit.Core.Enums; +using Bit.Core.Models.Data; using Bit.Core.Models.StaticStore; -using Bit.Core.Models.Table; using System.Collections.Generic; using System.Linq; @@ -484,7 +484,9 @@ namespace Bit.Core.Utilities PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise, SponsoredProductType = ProductType.Families, SponsoringProductType = ProductType.Enterprise, - StripePlanId = "2021-enterprise-sponsored-families-org-monthly" + StripePlanId = "2021-enterprise-sponsored-families-org-monthly", + UsersCanSponsor = (OrganizationUserOrganizationDetails org) => + GetPlan(org.PlanType).Product == ProductType.Enterprise, } }; public static Plan GetPlan(PlanType planType) => diff --git a/util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql b/util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql index 7c10a47bb7..079aacd924 100644 --- a/util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql +++ b/util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql @@ -622,6 +622,7 @@ SELECT OU.[OrganizationId], O.[Name], O.[Enabled], + O.[PlanType], O.[UsePolicies], O.[UseSso], O.[UseGroups], From e0768ef6c7e63d524f363867f66c04e9a8e3177d Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Mon, 8 Nov 2021 20:23:09 -0500 Subject: [PATCH 16/34] Add sponsorship service to DI --- .../Implementations/OrganizationSponsorshipService.cs | 4 ++-- src/Core/Utilities/ServiceCollectionExtensions.cs | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs index 1e1bff3c62..78c6a84bb1 100644 --- a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs +++ b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs @@ -21,12 +21,12 @@ namespace Bit.Core.Services public OrganizationSponsorshipService(IOrganizationSponsorshipRepository organizationSponsorshipRepository, IOrganizationRepository organizationRepository, IPaymentService paymentService, - IDataProtector dataProtector) + IDataProtectionProvider dataProtectionProvider) { _organizationSponsorshipRepository = organizationSponsorshipRepository; _organizationRepository = organizationRepository; _paymentService = paymentService; - _dataProtector = dataProtector; + _dataProtector = dataProtectionProvider.CreateProtector("OrganizationSponsorshipServiceDataProtector"); } public async Task ValidateRedemptionTokenAsync(string encryptedToken) diff --git a/src/Core/Utilities/ServiceCollectionExtensions.cs b/src/Core/Utilities/ServiceCollectionExtensions.cs index 24a5093f8d..60f649ceb3 100644 --- a/src/Core/Utilities/ServiceCollectionExtensions.cs +++ b/src/Core/Utilities/ServiceCollectionExtensions.cs @@ -170,6 +170,7 @@ namespace Bit.Core.Utilities services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); From e820aac719786107e9496c12c5cd87763e888c81 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 9 Nov 2021 10:40:31 -0500 Subject: [PATCH 17/34] Use userId to find org users --- .../OrganizationSponsorshipsController.cs | 8 +--- .../OrganizationSponsorshipRequestModel.cs | 3 -- .../OrganizationSponsorshipService.cs | 2 +- ...OrganizationSponsorshipsControllerTests.cs | 38 ++++--------------- .../OrganizationSponsorshipServiceTests.cs | 1 + 5 files changed, 11 insertions(+), 41 deletions(-) diff --git a/src/Api/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Controllers/OrganizationSponsorshipsController.cs index d8707960d3..9cded94a6d 100644 --- a/src/Api/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Controllers/OrganizationSponsorshipsController.cs @@ -51,14 +51,10 @@ namespace Bit.Api.Controllers throw new BadRequestException("Specified Organization cannot sponsor other organizations."); } - var sponsoringOrgUser = await _organizationUserRepository.GetByIdAsync(model.OrganizationUserId); + var sponsoringOrgUser = await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgIdGuid, _currentContext.UserId ?? default); if (sponsoringOrgUser == null || sponsoringOrgUser.Status != OrganizationUserStatusType.Confirmed) { - throw new BadRequestException("Only confirm users can sponsor other organizations."); - } - if (sponsoringOrgUser.UserId != _currentContext.UserId) - { - throw new BadRequestException("Can only create organization sponsorships for yourself."); + throw new BadRequestException("Only confirmed users can sponsor other organizations."); } var existingOrgSponsorship = await _organizationSponsorshipRepository.GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id); diff --git a/src/Core/Models/Api/Request/Organizations/OrganizationSponsorshipRequestModel.cs b/src/Core/Models/Api/Request/Organizations/OrganizationSponsorshipRequestModel.cs index e04e47bc84..deb8d07f3f 100644 --- a/src/Core/Models/Api/Request/Organizations/OrganizationSponsorshipRequestModel.cs +++ b/src/Core/Models/Api/Request/Organizations/OrganizationSponsorshipRequestModel.cs @@ -10,9 +10,6 @@ namespace Bit.Core.Models.Api.Request [Required] public PlanSponsorshipType PlanSponsorshipType { get; set; } - [Required] - public Guid OrganizationUserId { get; set; } - [Required] [StringLength(256)] [StrictEmailAddress] diff --git a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs index 78c6a84bb1..72128389a1 100644 --- a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs +++ b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs @@ -88,7 +88,7 @@ namespace Bit.Core.Services sponsorship = await _organizationSponsorshipRepository.CreateAsync(sponsorship); // TODO: send email to sponsoredEmail w/ redemption token link - var _ = RedemptionToken(sponsorship.Id, sponsorshipType); + // var _ = RedemptionToken(sponsorship.Id, sponsorshipType); } catch { diff --git a/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs index f019ade201..a944c56f20 100644 --- a/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs +++ b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs @@ -58,45 +58,21 @@ namespace Bit.Api.Test.Controllers [Theory] [BitMemberAutoData(nameof(NonConfirmedOrganizationUsersStatuses))] public async Task CreateSponsorship_BadSponsoringUserStatus_ThrowsBadRequest( - OrganizationUserStatusType statusType, Guid userId, Organization org, OrganizationUser orgUser, + OrganizationUserStatusType statusType, Organization org, OrganizationUser orgUser, OrganizationSponsorshipRequestModel model, SutProvider sutProvider) { org.PlanType = PlanType.EnterpriseAnnually; orgUser.Status = statusType; - orgUser.UserId = userId; - model.OrganizationUserId = orgUser.Id; sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); - sutProvider.GetDependency().UserId.Returns(userId); - sutProvider.GetDependency().GetByIdAsync(orgUser.Id).Returns(orgUser); + sutProvider.GetDependency().UserId.Returns(orgUser.UserId); + sutProvider.GetDependency().GetByOrganizationAsync(org.Id, orgUser.UserId.Value) + .Returns(orgUser); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.CreateSponsorship(org.Id.ToString(), model)); - Assert.Contains("Only confirm users can sponsor other organizations.", exception.Message); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .OfferSponsorshipAsync(default, default, default, default, default); - } - - [Theory] - [BitAutoData("c56c7ab4-a174-412a-a822-abe53ea71d50")] - public async Task CreateSponsorship_CreateSponsorshipAsDifferentUser_ThrowsBadRequest(Guid userId, - Organization org, OrganizationUser orgUser, OrganizationSponsorshipRequestModel model, - SutProvider sutProvider) - { - org.PlanType = PlanType.EnterpriseAnnually; - orgUser.Status = OrganizationUserStatusType.Confirmed; - model.OrganizationUserId = orgUser.Id; - - sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); - sutProvider.GetDependency().UserId.Returns(userId); - sutProvider.GetDependency().GetByIdAsync(orgUser.Id).Returns(orgUser); - - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.CreateSponsorship(org.Id.ToString(), model)); - - Assert.Contains("Can only create organization sponsorships for yourself.", exception.Message); + Assert.Contains("Only confirmed users can sponsor other organizations.", exception.Message); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() .OfferSponsorshipAsync(default, default, default, default, default); @@ -110,11 +86,11 @@ namespace Bit.Api.Test.Controllers { org.PlanType = PlanType.EnterpriseAnnually; orgUser.Status = OrganizationUserStatusType.Confirmed; - model.OrganizationUserId = orgUser.Id; sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); sutProvider.GetDependency().UserId.Returns(orgUser.UserId); - sutProvider.GetDependency().GetByIdAsync(orgUser.Id).Returns(orgUser); + sutProvider.GetDependency().GetByOrganizationAsync(org.Id, orgUser.UserId.Value) + .Returns(orgUser); sutProvider.GetDependency() .GetBySponsoringOrganizationUserIdAsync(orgUser.Id).Returns(sponsorship); diff --git a/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs b/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs index 4fc25b7f8c..2b0af9b7c8 100644 --- a/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs +++ b/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs @@ -45,6 +45,7 @@ namespace Bit.Core.Test.Services SponsoringOrganizationUserId = sponsoringOrgUser.Id, FriendlyName = friendlyName, OfferedToEmail = sponsoredEmail, + PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise, CloudSponsor = true, }; From 455e486eda13dac9e475fb88d78aa0903cf11263 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 9 Nov 2021 17:51:36 -0500 Subject: [PATCH 18/34] Send f4e offer email --- src/Core/Services/IMailService.cs | 6 ++---- .../Implementations/HandlebarsMailService.cs | 16 ++++++++++++--- .../OrganizationSponsorshipService.cs | 8 ++++++-- .../NoopImplementations/NoopMailService.cs | 9 ++------- .../OrganizationSponsorshipServiceTests.cs | 20 +++++++++++++++++-- 5 files changed, 41 insertions(+), 18 deletions(-) diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index c0eb047c1e..5e8ea8f3b3 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -50,10 +50,8 @@ namespace Bit.Core.Services Task SendProviderUserRemoved(string providerName, string email); Task SendUpdatedTempPasswordEmailAsync(string email, string userName); // TODO: Change signature to hold data needed for email - Task SendFamiliesForEnterpriseInviteRedeemableEmailAsync(string email, string organizationName, string token); - // NOTE: Not married to these next two names - Task SendFamiliesForEnterpriseInviteRedeemedToFamilyUserEmailAsync(string email); - Task SendFamiliesForEnterpriseInviteRedeemedToOrgUserEmailAsync(string email, string organizationName); + Task SendFamiliesForEnterpriseOfferEmailAsync(string email, string organizationName, string token); + Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail, string sponsorOrgName); Task SendFamiliesForEnterpriseReconfirmationRequiredEmailAsync(string email); Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email); Task SendFamiliesForEnterpriseSponsorshipEndingEmailAsync(string email); diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index ada022a195..a48b458a07 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -757,7 +757,7 @@ namespace Bit.Core.Services await _mailDeliveryService.SendEmailAsync(message); } - public async Task SendFamiliesForEnterpriseInviteRedeemableEmailAsync(string email, string organizationName, string token) + public async Task SendFamiliesForEnterpriseOfferEmailAsync(string email, string organizationName, string token) { // TODO: Complete emails var message = CreateDefaultMessage("A Family Organization Invite Is Redeemable", email); @@ -780,7 +780,17 @@ namespace Bit.Core.Services await _mailDeliveryService.SendEmailAsync(message); } - public async Task SendFamiliesForEnterpriseInviteRedeemedToFamilyUserEmailAsync(string email) + public async Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail, string sponsorOrgName) + { + // TODO: complete emails + // Email family user + await SendFamiliesForEnterpriseInviteRedeemedToFamilyUserEmailAsync(familyUserEmail); + + // Email enterprise org user + await SendFamiliesForEnterpriseInviteRedeemedToOrgUserEmailAsync(sponsorEmail, sponsorOrgName); + } + + private async Task SendFamiliesForEnterpriseInviteRedeemedToFamilyUserEmailAsync(string email) { // TODO: Complete emails var message = CreateDefaultMessage("You Have Redeemed A Family Organization Sponsorship", email); @@ -793,7 +803,7 @@ namespace Bit.Core.Services await _mailDeliveryService.SendEmailAsync(message); } - public async Task SendFamiliesForEnterpriseInviteRedeemedToOrgUserEmailAsync(string email, string organizationName) + private async Task SendFamiliesForEnterpriseInviteRedeemedToOrgUserEmailAsync(string email, string organizationName) { // TODO: Complete emails var message = CreateDefaultMessage("A User Has Redeemeed Your Sponsorship", email); diff --git a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs index 72128389a1..79250323c0 100644 --- a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs +++ b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs @@ -16,16 +16,20 @@ namespace Bit.Core.Services private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository; private readonly IOrganizationRepository _organizationRepository; private readonly IPaymentService _paymentService; + private readonly IMailService _mailService; + private readonly IDataProtector _dataProtector; public OrganizationSponsorshipService(IOrganizationSponsorshipRepository organizationSponsorshipRepository, IOrganizationRepository organizationRepository, IPaymentService paymentService, + IMailService mailService, IDataProtectionProvider dataProtectionProvider) { _organizationSponsorshipRepository = organizationSponsorshipRepository; _organizationRepository = organizationRepository; _paymentService = paymentService; + _mailService = mailService; _dataProtector = dataProtectionProvider.CreateProtector("OrganizationSponsorshipServiceDataProtector"); } @@ -87,8 +91,8 @@ namespace Bit.Core.Services { sponsorship = await _organizationSponsorshipRepository.CreateAsync(sponsorship); - // TODO: send email to sponsoredEmail w/ redemption token link - // var _ = RedemptionToken(sponsorship.Id, sponsorshipType); + await _mailService.SendFamiliesForEnterpriseOfferEmailAsync(sponsoredEmail, sponsoringOrg.Name, + RedemptionToken(sponsorship.Id, sponsorshipType)); } catch { diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index 7a648a4477..f32e9d96ae 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -201,17 +201,12 @@ namespace Bit.Core.Services return Task.FromResult(0); } - public Task SendFamiliesForEnterpriseInviteRedeemableEmailAsync(string email, string organizationName, string token) + public Task SendFamiliesForEnterpriseOfferEmailAsync(string email, string organizationName, string token) { return Task.FromResult(0); } - public Task SendFamiliesForEnterpriseInviteRedeemedToFamilyUserEmailAsync(string email) - { - return Task.FromResult(0); - } - - public Task SendFamiliesForEnterpriseInviteRedeemedToOrgUserEmailAsync(string email, string organizationName) + public Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail, string sponsorOrgName) { return Task.FromResult(0); } diff --git a/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs b/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs index 2b0af9b7c8..87914910f1 100644 --- a/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs +++ b/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs @@ -8,9 +8,11 @@ using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.Helpers; +using Microsoft.AspNetCore.DataProtection; using Microsoft.IdentityModel.Tokens; using NSubstitute; using NSubstitute.ExceptionExtensions; +using NSubstitute.Extensions; using Xunit; namespace Bit.Core.Test.Services @@ -34,13 +36,24 @@ namespace Bit.Core.Test.Services [Theory] [BitAutoData] public async Task OfferSponsorship_CreatesSponsorship(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, - string sponsoredEmail, string friendlyName, SutProvider sutProvider) + string sponsoredEmail, string friendlyName, Guid sponsorshipId, + SutProvider sutProvider) { + 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); var expectedSponsorship = new OrganizationSponsorship { + Id = sponsorshipId, SponsoringOrganizationId = sponsoringOrg.Id, SponsoringOrganizationUserId = sponsoringOrgUser.Id, FriendlyName = friendlyName, @@ -51,7 +64,10 @@ namespace Bit.Core.Test.Services await sutProvider.GetDependency().Received(1) .CreateAsync(Arg.Is(s => sponsorshipValidator(s, expectedSponsorship))); - // TODO: Validate email called with appropriate token.s + + await sutProvider.GetDependency().Received(1). + SendFamiliesForEnterpriseOfferEmailAsync(sponsoredEmail, sponsoringOrg.Name, + Arg.Any()); } [Theory] From 234800d969318ea4f184c3e7707a029b5b7a48a3 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 9 Nov 2021 18:27:51 -0500 Subject: [PATCH 19/34] Simplify names of f4e mail messages --- ...bs => FamiliesForEnterpriseOffer.html.hbs} | 0 ...bs => FamiliesForEnterpriseOffer.text.hbs} | 0 ...orEnterpriseRedeemedToFamilyUser.html.hbs} | 0 ...orEnterpriseRedeemedToFamilyUser.text.hbs} | 0 ...esForEnterpriseRedeemedToOrgUser.html.hbs} | 0 ...esForEnterpriseRedeemedToOrgUser.text.hbs} | 0 ...sForEnterpriseInviteRedeemableViewModel.cs | 8 ----- .../FamiliesForEnterpriseOfferViewModel.cs | 8 +++++ ...nterpriseRedeemedToFamilyUserViewModel.cs} | 2 +- ...orEnterpriseRedeemedToOrgUserViewModel.cs} | 2 +- .../Implementations/HandlebarsMailService.cs | 34 +++++++------------ 11 files changed, 23 insertions(+), 31 deletions(-) rename src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/{FamiliesForEnterpriseInviteRedeemable.html.hbs => FamiliesForEnterpriseOffer.html.hbs} (100%) rename src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/{FamiliesForEnterpriseInviteRedeemable.text.hbs => FamiliesForEnterpriseOffer.text.hbs} (100%) rename src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/{FamiliesForEnterpriseInviteRedeemedToFamilyUser.html.hbs => FamiliesForEnterpriseRedeemedToFamilyUser.html.hbs} (100%) rename src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/{FamiliesForEnterpriseInviteRedeemedToFamilyUser.text.hbs => FamiliesForEnterpriseRedeemedToFamilyUser.text.hbs} (100%) rename src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/{FamiliesForEnterpriseInviteRedeemedToOrgUser.html.hbs => FamiliesForEnterpriseRedeemedToOrgUser.html.hbs} (100%) rename src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/{FamiliesForEnterpriseInviteRedeemedToOrgUser.text.hbs => FamiliesForEnterpriseRedeemedToOrgUser.text.hbs} (100%) delete mode 100644 src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemableViewModel.cs create mode 100644 src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseOfferViewModel.cs rename src/Core/Models/Mail/FamiliesForEnterprise/{FamiliesForEnterpriseInviteRedeemedToOrgUserViewModel.cs => FamiliesForEnterpriseRedeemedToFamilyUserViewModel.cs} (58%) rename src/Core/Models/Mail/FamiliesForEnterprise/{FamiliesForEnterpriseInviteRedeemedToFamilyUserViewModel.cs => FamiliesForEnterpriseRedeemedToOrgUserViewModel.cs} (57%) diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemable.html.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOffer.html.hbs similarity index 100% rename from src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemable.html.hbs rename to src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOffer.html.hbs diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemable.text.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOffer.text.hbs similarity index 100% rename from src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemable.text.hbs rename to src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOffer.text.hbs diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToFamilyUser.html.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToFamilyUser.html.hbs similarity index 100% rename from src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToFamilyUser.html.hbs rename to src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToFamilyUser.html.hbs diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToFamilyUser.text.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToFamilyUser.text.hbs similarity index 100% rename from src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToFamilyUser.text.hbs rename to src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToFamilyUser.text.hbs diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToOrgUser.html.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToOrgUser.html.hbs similarity index 100% rename from src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToOrgUser.html.hbs rename to src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToOrgUser.html.hbs diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToOrgUser.text.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToOrgUser.text.hbs similarity index 100% rename from src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToOrgUser.text.hbs rename to src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToOrgUser.text.hbs diff --git a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemableViewModel.cs b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemableViewModel.cs deleted file mode 100644 index 57e01e7108..0000000000 --- a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemableViewModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace Bit.Core.Models.Mail.FamiliesForEnterprise -{ - public class FamiliesForEnterpriseInviteRedeemableViewModel : BaseMailModel - { - public string OrganizationName { get; set; } - public string Url { get; set; } - } -} diff --git a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseOfferViewModel.cs b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseOfferViewModel.cs new file mode 100644 index 0000000000..4c8e41c2a4 --- /dev/null +++ b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseOfferViewModel.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Models.Mail.FamiliesForEnterprise +{ + public class FamiliesForEnterpriseOfferViewModel : BaseMailModel + { + public string SponsorshipToken { get; set; } + public string Url => $"{WebVaultUrl}/sponsored/families-for-enterprise?token={SponsorshipToken}"; + } +} diff --git a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToOrgUserViewModel.cs b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToFamilyUserViewModel.cs similarity index 58% rename from src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToOrgUserViewModel.cs rename to src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToFamilyUserViewModel.cs index a547b4f35d..d34e90d972 100644 --- a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToOrgUserViewModel.cs +++ b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToFamilyUserViewModel.cs @@ -1,6 +1,6 @@ namespace Bit.Core.Models.Mail.FamiliesForEnterprise { - public class FamiliesForEnterpriseInviteRedeemedToOrgUserViewModel : BaseMailModel + public class FamiliesForEnterpriseRedeemedToFamilyUserViewModel : BaseMailModel { public string OrganizationName { get; set; } } diff --git a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToFamilyUserViewModel.cs b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToOrgUserViewModel.cs similarity index 57% rename from src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToFamilyUserViewModel.cs rename to src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToOrgUserViewModel.cs index bda9c907cb..53ca9e38f9 100644 --- a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseInviteRedeemedToFamilyUserViewModel.cs +++ b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseRedeemedToOrgUserViewModel.cs @@ -1,6 +1,6 @@ namespace Bit.Core.Models.Mail.FamiliesForEnterprise { - public class FamiliesForEnterpriseInviteRedeemedToFamilyUserViewModel : BaseMailModel + public class FamiliesForEnterpriseRedeemedToOrgUserViewModel : BaseMailModel { public string OrganizationName { get; set; } } diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index a48b458a07..301f89e1ac 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -759,30 +759,22 @@ namespace Bit.Core.Services public async Task SendFamiliesForEnterpriseOfferEmailAsync(string email, string organizationName, string token) { - // TODO: Complete emails - var message = CreateDefaultMessage("A Family Organization Invite Is Redeemable", email); + var message = CreateDefaultMessage("Free Bitwarden Family Plan Offer", email); - // NOTE: If somehow cloud vault changes this will need to change/be injected - var url = CoreHelpers.ExtendQuery(new Uri($"https://vault.bitwarden.com/#/sponsored/families-for-enterprise"), - new Dictionary - { - ["sponsorshipToken"] = token, - }); - - var model = new FamiliesForEnterpriseInviteRedeemableViewModel + var model = new FamiliesForEnterpriseOfferViewModel { - Url = url.ToString(), - OrganizationName = organizationName, + WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, + SiteName = _globalSettings.SiteName, + SponsorshipToken = token, }; - await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseInviteRedeemable", model); - message.Category = "FamiliesForEnterpriseInviteRedeemable"; + await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseOffer", model); + message.Category = "FamiliesForEnterpriseOffer"; await _mailDeliveryService.SendEmailAsync(message); } public async Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail, string sponsorOrgName) { - // TODO: complete emails // Email family user await SendFamiliesForEnterpriseInviteRedeemedToFamilyUserEmailAsync(familyUserEmail); @@ -794,12 +786,12 @@ namespace Bit.Core.Services { // TODO: Complete emails var message = CreateDefaultMessage("You Have Redeemed A Family Organization Sponsorship", email); - var model = new FamiliesForEnterpriseInviteRedeemedToFamilyUserViewModel + var model = new FamiliesForEnterpriseRedeemedToFamilyUserViewModel { }; - await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseInviteRedeemedToFamilyUser", model); - message.Category = "FamilyForEnterpriseInviteRedeemedToFamilyUser"; + await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseRedeemedToFamilyUser", model); + message.Category = "FamilyForEnterpriseRedeemedToFamilyUser"; await _mailDeliveryService.SendEmailAsync(message); } @@ -807,12 +799,12 @@ namespace Bit.Core.Services { // TODO: Complete emails var message = CreateDefaultMessage("A User Has Redeemeed Your Sponsorship", email); - var model = new FamiliesForEnterpriseInviteRedeemedToOrgUserViewModel + var model = new FamiliesForEnterpriseRedeemedToOrgUserViewModel { OrganizationName = organizationName, }; - await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseInviteRedeemedToOrgUser", model); - message.Category = "FamilyForEnterpriseInviteRedeemedToOrgUser"; + await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseRedeemedToOrgUser", model); + message.Category = "FamilyForEnterpriseRedeemedToOrgUser"; await _mailDeliveryService.SendEmailAsync(message); } From 2ad7c2894bfb0bc123dec7c7c757992aa8f3a94f Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 9 Nov 2021 19:39:41 -0500 Subject: [PATCH 20/34] Fix Stripe org default tax rates --- src/Core/Models/Business/SubscriptionCreateOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Models/Business/SubscriptionCreateOptions.cs b/src/Core/Models/Business/SubscriptionCreateOptions.cs index bcd95c8c25..2cc01fb689 100644 --- a/src/Core/Models/Business/SubscriptionCreateOptions.cs +++ b/src/Core/Models/Business/SubscriptionCreateOptions.cs @@ -62,7 +62,7 @@ namespace Bit.Core.Models.Business protected void AddTaxRateItem(List taxRates) => AddTaxRateItem(taxRates?.Select(t => t.Id).ToList()); protected void AddTaxRateItem(List taxRateIds) { - if (taxRateIds != null && taxRateIds.Any()) + if (taxRateIds != null && taxRateIds.Any(tax => !string.IsNullOrWhiteSpace(tax))) { DefaultTaxRates = taxRateIds; } From 68e1aacda92d531a1aa8a6e012faa8bb8050c7b1 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 9 Nov 2021 19:42:31 -0500 Subject: [PATCH 21/34] Universal sponsorship redeem api --- src/Api/Controllers/OrganizationSponsorshipsController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Api/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Controllers/OrganizationSponsorshipsController.cs index 9cded94a6d..4b7dca085d 100644 --- a/src/Api/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Controllers/OrganizationSponsorshipsController.cs @@ -67,7 +67,7 @@ namespace Bit.Api.Controllers model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName); } - [HttpPost("sponsored/redeem/families-for-enterprise")] + [HttpPost("sponsored/redeem")] [SelfHosted(NotSelfHostedOnly = true)] public async Task RedeemSponsorship([FromQuery] string sponsorshipToken, [FromBody] OrganizationSponsorshipRedeemRequestModel model) { From 991fb40007e1a1f8767c6dbe133ff84b3c996454 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 9 Nov 2021 21:12:13 -0500 Subject: [PATCH 22/34] Populate user in current context --- .../OrganizationSponsorshipsController.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Api/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Controllers/OrganizationSponsorshipsController.cs index 4b7dca085d..58759f4e8c 100644 --- a/src/Api/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Controllers/OrganizationSponsorshipsController.cs @@ -6,6 +6,7 @@ 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; @@ -23,16 +24,20 @@ namespace Bit.Api.Controllers 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; } @@ -67,7 +72,7 @@ namespace Bit.Api.Controllers model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName); } - [HttpPost("sponsored/redeem")] + [HttpPost("redeem")] [SelfHosted(NotSelfHostedOnly = true)] public async Task RedeemSponsorship([FromQuery] string sponsorshipToken, [FromBody] OrganizationSponsorshipRedeemRequestModel model) { @@ -81,12 +86,12 @@ namespace Bit.Api.Controllers throw new BadRequestException("Can only redeem sponsorship for an organization you own."); } var existingSponsorshipOffer = await _organizationSponsorshipRepository - .GetByOfferedToEmailAsync(_currentContext.User.Email); + .GetByOfferedToEmailAsync((await CurrentUser).Email); if (existingSponsorshipOffer == null) { throw new BadRequestException("No unredeemed sponsorship offer exists for you."); } - if (_currentContext.User.Email != existingSponsorshipOffer.OfferedToEmail) + if ((await CurrentUser).Email != existingSponsorshipOffer.OfferedToEmail) { throw new BadRequestException("This sponsorship offer was issued to a different user email address."); } @@ -170,5 +175,7 @@ namespace Bit.Api.Controllers await _organizationsSponsorshipService.RemoveSponsorshipAsync(sponsoredOrganization, existingOrgSponsorship); } + + private Task CurrentUser => _userService.GetUserByIdAsync(_currentContext.UserId.Value); } } From dc790dbdaf052a25a46e3e13e7cb3a2f6530eb2a Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Tue, 9 Nov 2021 21:12:27 -0500 Subject: [PATCH 23/34] Add product type to organization details --- .../Models/Api/Response/ProfileOrganizationResponseModel.cs | 2 ++ .../Implementations/OrganizationSponsorshipService.cs | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs b/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs index 3a4f90e430..d094a323b2 100644 --- a/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs +++ b/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs @@ -42,6 +42,7 @@ namespace Bit.Core.Models.Api FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null && Utilities.StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise) .UsersCanSponsor(organization); + PlanProductType = Utilities.StaticStore.GetPlan(organization.PlanType).Product; } public string Id { get; set; } @@ -74,5 +75,6 @@ namespace Bit.Core.Models.Api public string ProviderName { get; set; } public string FamilySponsorshipFriendlyName { get; set; } public bool FamilySponsorshipAvailable { get; set; } + public ProductType PlanProductType { get; set; } } } diff --git a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs index 79250323c0..6e870a88b3 100644 --- a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs +++ b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs @@ -40,10 +40,10 @@ namespace Bit.Core.Services return false; } - var decryptedToken = _dataProtector.Unprotect(encryptedToken); + var decryptedToken = _dataProtector.Unprotect(encryptedToken[TokenClearTextPrefix.Length..]); var dataParts = decryptedToken.Split(' '); - if (dataParts.Length != 2) + if (dataParts.Length != 3) { return false; } From 339248ffaf701a845fd8eca653df2e3f7b11fbab Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 10 Nov 2021 14:10:30 -0500 Subject: [PATCH 24/34] Use upgrade path to change sponsorship Sponsorships need to be annual to match the GB add-on charge rate --- .../SponsoredOrganizationSubscription.cs | 39 ---- .../Business/SubscriptionCreateOptions.cs | 97 ++-------- .../Models/Business/SubscriptionUpdate.cs | 167 ++++++++++++++---- src/Core/Services/IPaymentService.cs | 2 +- .../OrganizationSponsorshipService.cs | 56 ++---- .../Implementations/StripePaymentService.cs | 58 ++---- src/Core/Utilities/StaticStore.cs | 2 +- 7 files changed, 178 insertions(+), 243 deletions(-) delete mode 100644 src/Core/Models/Business/SponsoredOrganizationSubscription.cs diff --git a/src/Core/Models/Business/SponsoredOrganizationSubscription.cs b/src/Core/Models/Business/SponsoredOrganizationSubscription.cs deleted file mode 100644 index 30ea3e047b..0000000000 --- a/src/Core/Models/Business/SponsoredOrganizationSubscription.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System.Collections.Generic; -using Bit.Core.Models.Table; - -namespace Bit.Core.Models.Business -{ - public class SponsoredOrganizationSubscription - { - public const string OrganizationSponsorhipIdMetadataKey = "OrganizationSponsorshipId"; - private readonly string _customerId; - private readonly Organization _org; - private readonly StaticStore.Plan _plan; - private readonly List _taxRates; - - public SponsoredOrganizationSubscription(Organization org, Stripe.Subscription existingSubscription) - { - _org = org; - _customerId = org.GatewayCustomerId; - _plan = Utilities.StaticStore.GetPlan(org.PlanType); - _taxRates = existingSubscription.DefaultTaxRates; - } - - public SponsorOrganizationSubscriptionOptions GetSponsorSubscriptionOptions(OrganizationSponsorship sponsorship, - int additionalSeats = 0, int additionalStorageGb = 0, bool premiumAccessAddon = false) - { - var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value); - - var subCreateOptions = new SponsorOrganizationSubscriptionOptions(_customerId, _org, _plan, - sponsoredPlan, _taxRates, additionalSeats, additionalStorageGb, premiumAccessAddon); - - subCreateOptions.Metadata.Add(OrganizationSponsorhipIdMetadataKey, sponsorship.Id.ToString()); - return subCreateOptions; - } - - public OrganizationUpgradeSubscriptionOptions RemoveOrganizationSubscriptionOptions(int additionalSeats = 0, - int additionalStorageGb = 0, bool premiumAccessAddon = false) => - new OrganizationUpgradeSubscriptionOptions(_customerId, _org, _plan, _taxRates, - additionalSeats, additionalStorageGb, premiumAccessAddon); - } -} diff --git a/src/Core/Models/Business/SubscriptionCreateOptions.cs b/src/Core/Models/Business/SubscriptionCreateOptions.cs index 2cc01fb689..75086c6543 100644 --- a/src/Core/Models/Business/SubscriptionCreateOptions.cs +++ b/src/Core/Models/Business/SubscriptionCreateOptions.cs @@ -1,14 +1,12 @@ using Bit.Core.Models.Table; using Stripe; using System.Collections.Generic; -using System.Linq; namespace Bit.Core.Models.Business { public class OrganizationSubscriptionOptionsBase : Stripe.SubscriptionCreateOptions { - public OrganizationSubscriptionOptionsBase(Organization org, StaticStore.Plan plan, - int additionalSeats, int additionalStorageGb, bool premiumAccessAddon) + public OrganizationSubscriptionOptionsBase(Organization org, StaticStore.Plan plan, TaxInfo taxInfo, int additionalSeats, int additionalStorageGb, bool premiumAccessAddon) { Items = new List(); Metadata = new Dictionary @@ -16,6 +14,15 @@ namespace Bit.Core.Models.Business [org.GatewayIdField()] = org.Id.ToString() }; + if (plan.StripePlanId != null) + { + Items.Add(new SubscriptionItemOptions + { + Plan = plan.StripePlanId, + Quantity = 1 + }); + } + if (additionalSeats > 0 && plan.StripeSeatPlanId != null) { Items.Add(new SubscriptionItemOptions @@ -42,53 +49,15 @@ namespace Bit.Core.Models.Business Quantity = 1 }); } - } - protected void AddPlanItem(StaticStore.Plan plan) => AddPlanItem(plan.StripePlanId); - protected void AddPlanItem(StaticStore.SponsoredPlan sponsoredPlan) => AddPlanItem(sponsoredPlan.StripePlanId); - protected void AddPlanItem(string stripePlanId) - { - if (stripePlanId != null) + if (!string.IsNullOrWhiteSpace(taxInfo?.StripeTaxRateId)) { - Items.Add(new SubscriptionItemOptions - { - Plan = stripePlanId, - Quantity = 1, - }); - } - } - - protected void AddTaxRateItem(TaxInfo taxInfo) => AddTaxRateItem(new List { taxInfo.StripeTaxRateId }); - protected void AddTaxRateItem(List taxRates) => AddTaxRateItem(taxRates?.Select(t => t.Id).ToList()); - protected void AddTaxRateItem(List taxRateIds) - { - if (taxRateIds != null && taxRateIds.Any(tax => !string.IsNullOrWhiteSpace(tax))) - { - DefaultTaxRates = taxRateIds; + DefaultTaxRates = new List { taxInfo.StripeTaxRateId }; } } } - public abstract class UnsponsoredOrganizationSubscriptionOptionsBase : OrganizationSubscriptionOptionsBase - { - public UnsponsoredOrganizationSubscriptionOptionsBase(Organization org, StaticStore.Plan plan, TaxInfo taxInfo, - int additionalSeats, int additionalStorage, bool premiumAccessAddon) : - base(org, plan, additionalSeats, additionalStorage, premiumAccessAddon) - { - AddPlanItem(plan); - AddTaxRateItem(taxInfo); - } - public UnsponsoredOrganizationSubscriptionOptionsBase(Organization org, StaticStore.Plan plan, List taxInfo, - int additionalSeats, int additionalStorage, bool premiumAccessAddon) : - base(org, plan, additionalSeats, additionalStorage, premiumAccessAddon) - { - AddPlanItem(plan); - AddTaxRateItem(taxInfo); - } - - } - - public class OrganizationPurchaseSubscriptionOptions : UnsponsoredOrganizationSubscriptionOptionsBase + public class OrganizationPurchaseSubscriptionOptions : OrganizationSubscriptionOptionsBase { public OrganizationPurchaseSubscriptionOptions( Organization org, StaticStore.Plan plan, @@ -101,54 +70,16 @@ namespace Bit.Core.Models.Business } } - public class OrganizationUpgradeSubscriptionOptions : UnsponsoredOrganizationSubscriptionOptionsBase + public class OrganizationUpgradeSubscriptionOptions : OrganizationSubscriptionOptionsBase { public OrganizationUpgradeSubscriptionOptions( string customerId, Organization org, StaticStore.Plan plan, TaxInfo taxInfo, int additionalSeats = 0, int additionalStorageGb = 0, - bool premiumAccessAddon = false) : - base(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon) - { - Customer = customerId; - } - public OrganizationUpgradeSubscriptionOptions( - string customerId, Organization org, - StaticStore.Plan plan, List taxInfo, - int additionalSeats = 0, int additionalStorageGb = 0, bool premiumAccessAddon = false) : base(org, plan, taxInfo, additionalSeats, additionalStorageGb, premiumAccessAddon) { Customer = customerId; } } - - public class RemoveOrganizationSubscriptionOptions : OrganizationSubscriptionOptionsBase - { - public RemoveOrganizationSubscriptionOptions(string customerId, Organization org, - StaticStore.Plan plan, List existingTaxRateStripeIds, - int additionalSeats = 0, int additionalStorageGb = 0, - bool premiumAccessAddon = false) : - base(org, plan, additionalSeats, additionalStorageGb, premiumAccessAddon) - { - Customer = customerId; - AddPlanItem(plan); - AddTaxRateItem(existingTaxRateStripeIds); - } - - } - - public class SponsorOrganizationSubscriptionOptions : OrganizationSubscriptionOptionsBase - { - public SponsorOrganizationSubscriptionOptions( - string customerId, Organization org, StaticStore.Plan existingPlan, - StaticStore.SponsoredPlan sponsorshipPlan, List existingTaxRates, int additionalSeats = 0, - int additionalStorageGb = 0, bool premiumAccessAddon = false) : - base(org, existingPlan, additionalSeats, additionalStorageGb, premiumAccessAddon) - { - Customer = customerId; - AddPlanItem(sponsorshipPlan); - AddTaxRateItem(existingTaxRates); - } - } } diff --git a/src/Core/Models/Business/SubscriptionUpdate.cs b/src/Core/Models/Business/SubscriptionUpdate.cs index f4aaedf8b7..4f0b2cc2ca 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) => + 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,102 @@ 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 string _existingPlanStripeId; + private string _sponsoredPlanStripeId; + private 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; + } + + public override List RevertItemsOptions(Subscription subscription) + { + return new() + { + new SubscriptionItemOptions + { + Id = AddStripeItem(subscription)?.Id, + Plan = AddStripePlanId, + Quantity = 0, + Deleted = true, + }, + new SubscriptionItemOptions + { + Id = RemoveStripeItem(subscription)?.Id, + Plan = RemoveStripePlanId, + Quantity = 1, + Deleted = false, + }, + }; + } + + public override List UpgradeItemsOptions(Subscription subscription) + { + return new() + { + new SubscriptionItemOptions + { + Id = RemoveStripeItem(subscription)?.Id, + Plan = RemoveStripePlanId, + Quantity = 0, + Deleted = true, + }, + new SubscriptionItemOptions + { + Id = AddStripeItem(subscription)?.Id, + Plan = AddStripePlanId, + Quantity = 1, + Deleted = false, + }, + }; + } + + 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/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index 4227934528..2bb515a21a 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -14,7 +14,7 @@ namespace Bit.Core.Services string paymentToken, Plan plan, short additionalStorageGb, int additionalSeats, bool premiumAccessAddon, TaxInfo taxInfo); Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship); - Task RemoveOrganizationSponsorshipAsync(Organization org); + 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, diff --git a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs index 6e870a88b3..cac483f2e2 100644 --- a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs +++ b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs @@ -128,7 +128,8 @@ namespace Bit.Core.Services if (existingSponsorship == null) { - await RemoveSponsorshipAsync(sponsoredOrganization); + // TODO: null safe this method + await RemoveSponsorshipAsync(sponsoredOrganization, null); // TODO on fail, mark org as disabled. return false; } @@ -136,7 +137,7 @@ namespace Bit.Core.Services var validated = true; if (existingSponsorship.SponsoringOrganizationId == null || existingSponsorship.SponsoringOrganizationUserId == null) { - await RemoveSponsorshipAsync(sponsoredOrganization); + await RemoveSponsorshipAsync(sponsoredOrganization, existingSponsorship); validated = false; } @@ -144,7 +145,7 @@ namespace Bit.Core.Services .GetByIdAsync(existingSponsorship.SponsoringOrganizationId.Value); if (!sponsoringOrganization.Enabled) { - await RemoveSponsorshipAsync(sponsoredOrganization); + await RemoveSponsorshipAsync(sponsoredOrganization, existingSponsorship); validated = false; } @@ -166,7 +167,7 @@ namespace Bit.Core.Services public async Task RemoveSponsorshipAsync(Organization sponsoredOrganization, OrganizationSponsorship sponsorship = null) { - var success = await _paymentService.RemoveOrganizationSponsorshipAsync(sponsoredOrganization); + await _paymentService.RemoveOrganizationSponsorshipAsync(sponsoredOrganization, sponsorship); await _organizationRepository.UpsertAsync(sponsoredOrganization); if (sponsorship == null) @@ -174,49 +175,22 @@ namespace Bit.Core.Services return; } - if (success) - { - // Initialize the record as available - sponsorship.SponsoredOrganizationId = null; - sponsorship.FriendlyName = null; - sponsorship.OfferedToEmail = null; - sponsorship.PlanSponsorshipType = null; - sponsorship.TimesRenewedWithoutValidation = 0; - sponsorship.SponsorshipLapsedDate = null; + // 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); - } + if (sponsorship.CloudSponsor || sponsorship.SponsorshipLapsedDate.HasValue) + { + await _organizationSponsorshipRepository.DeleteAsync(sponsorship); } else { - sponsorship.SponsoringOrganizationId = null; - sponsorship.SponsoringOrganizationUserId = null; - - if (!sponsorship.CloudSponsor) - { - // Sef-hosted sponsorship record - // we need to make the existing sponsorship available, and add - // a new sponsorship record to record the lapsed sponsorship - var cleanSponsorship = new OrganizationSponsorship - { - InstallationId = sponsorship.InstallationId, - SponsoringOrganizationId = sponsorship.SponsoringOrganizationId, - SponsoringOrganizationUserId = sponsorship.SponsoringOrganizationUserId, - CloudSponsor = sponsorship.CloudSponsor, - }; - await _organizationSponsorshipRepository.UpsertAsync(cleanSponsorship); - } - - sponsorship.SponsorshipLapsedDate ??= DateTime.UtcNow; await _organizationSponsorshipRepository.UpsertAsync(sponsorship); } - } } diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index b4460d6020..31aa08dc55 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -192,43 +192,25 @@ namespace Bit.Core.Services } } - public async Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship) + private async Task ChangeOrganizationSponsorship(Organization org, OrganizationSponsorship sponsorship, bool applySponsorship) { - var customer = await _stripeAdapter.CustomerGetAsync(org.GatewayCustomerId); + var existingPlan = Utilities.StaticStore.GetPlan(org.PlanType); + var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value); + var subscriptionUpdate = new SponsorOrganizationSubscriptionUpdate(existingPlan, sponsoredPlan, applySponsorship); + + var prorationTime = DateTime.UtcNow; + await FinalizeSubscriptionChangeAsync(org, subscriptionUpdate, prorationTime); + var sub = await _stripeAdapter.SubscriptionGetAsync(org.GatewaySubscriptionId); + org.ExpirationDate = sub.CurrentPeriodEnd; - var sponsoredSubscription = new SponsoredOrganizationSubscription(org, sub); - - var subscription = await ChargeForNewSubscriptionAsync(org, customer, false, - false, PaymentMethodType.None, sponsoredSubscription.GetSponsorSubscriptionOptions(sponsorship), null); - org.GatewaySubscriptionId = subscription.Id; - - org.ExpirationDate = subscription.CurrentPeriodEnd; } - public async Task RemoveOrganizationSponsorshipAsync(Organization org) - { - var customer = await _stripeAdapter.CustomerGetAsync(org.GatewayCustomerId); - var sub = await _stripeAdapter.SubscriptionGetAsync(org.GatewaySubscriptionId); + public Task SponsorOrganizationAsync(Organization org, OrganizationSponsorship sponsorship) => + ChangeOrganizationSponsorship(org, sponsorship, true); - var sponsoredSubscription = new SponsoredOrganizationSubscription(org, sub); - var subCreateOptions = sponsoredSubscription.RemoveOrganizationSubscriptionOptions(); - - var (stripePaymentMethod, paymentMethodType) = IdentifyPaymentMethod(customer, subCreateOptions); - var subscription = await ChargeForNewSubscriptionAsync(org, customer, false, - stripePaymentMethod, paymentMethodType, subCreateOptions, null); - - if (subscription.Status == "incomplete") - { - // TODO: revert - return false; - } - org.GatewaySubscriptionId = subscription.Id; - org.Enabled = true; - org.ExpirationDate = subscription.CurrentPeriodEnd; - - return 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) @@ -736,11 +718,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", @@ -783,14 +765,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 { @@ -814,7 +790,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/Utilities/StaticStore.cs b/src/Core/Utilities/StaticStore.cs index 68a3e88574..d905bd0a7f 100644 --- a/src/Core/Utilities/StaticStore.cs +++ b/src/Core/Utilities/StaticStore.cs @@ -484,7 +484,7 @@ namespace Bit.Core.Utilities PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise, SponsoredProductType = ProductType.Families, SponsoringProductType = ProductType.Enterprise, - StripePlanId = "2021-enterprise-sponsored-families-org-monthly", + StripePlanId = "2021-family-for-enterprise-annually", UsersCanSponsor = (OrganizationUserOrganizationDetails org) => GetPlan(org.PlanType).Product == ProductType.Enterprise, } From 5cb6930fd7c4b4249e1b8eddfeeec3051228077c Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 10 Nov 2021 15:39:39 -0500 Subject: [PATCH 25/34] Use organization and auth to find organization sponsorship --- .../OrganizationSponsorshipsController.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Api/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Controllers/OrganizationSponsorshipsController.cs index 58759f4e8c..64964df13d 100644 --- a/src/Api/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Controllers/OrganizationSponsorshipsController.cs @@ -116,21 +116,21 @@ namespace Bit.Api.Controllers await _organizationsSponsorshipService.SetUpSponsorshipAsync(existingSponsorshipOffer, organizationToSponsor); } - [HttpDelete("{sponsoringOrgUserId}")] - [HttpPost("{sponsoringOrgUserId}/delete")] + [HttpDelete("{sponsoringOrganizationId}")] + [HttpPost("{sponsoringOrganizationId}/delete")] [SelfHosted(NotSelfHostedOnly = true)] - public async Task RevokeSponsorship(string sponsoringOrgUserId) + public async Task RevokeSponsorship(string sponsoringOrganizationId) { - var sponsoringOrgUserIdGuid = new Guid(sponsoringOrgUserId); + var sponsoringOrganizationIdGuid = new Guid(sponsoringOrganizationId); - var orgUser = await _organizationUserRepository.GetByIdAsync(sponsoringOrgUserIdGuid); + var orgUser = await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrganizationIdGuid, _currentContext.UserId ?? default); if (_currentContext.UserId != orgUser?.UserId) { throw new BadRequestException("Can only revoke a sponsorship you granted."); } var existingOrgSponsorship = await _organizationSponsorshipRepository - .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUserIdGuid); + .GetBySponsoringOrganizationUserIdAsync(orgUser.Id); if (existingOrgSponsorship == null || existingOrgSponsorship.SponsoredOrganizationId == null) { throw new BadRequestException("You are not currently sponsoring an organization."); From 61d91ad6c07fdb1f92d6ac0e61ecebf33696c509 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 10 Nov 2021 17:00:48 -0500 Subject: [PATCH 26/34] Add resend sponsorship offer api endpoint --- .../OrganizationSponsorshipsController.cs | 27 ++++ .../IOrganizationSponsorshipService.cs | 1 + .../OrganizationSponsorshipService.cs | 7 + ...OrganizationSponsorshipsControllerTests.cs | 146 +++++++++++++++--- .../OrganizationSponsorshipServiceTests.cs | 15 ++ 5 files changed, 172 insertions(+), 24 deletions(-) diff --git a/src/Api/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Controllers/OrganizationSponsorshipsController.cs index 64964df13d..97e7bf763e 100644 --- a/src/Api/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Controllers/OrganizationSponsorshipsController.cs @@ -72,6 +72,33 @@ namespace Bit.Api.Controllers model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName); } + [HttpPost("{sponsoringOrgId}/families-for-enterprise/resend")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task ResendSponsorshipOffer(string sponsoringOrgId) + { + // TODO: validate has right to sponsor, send sponsorship email + var sponsoringOrgIdGuid = new Guid(sponsoringOrgId); + var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgIdGuid); + if (sponsoringOrg == null) + { + throw new BadRequestException("Cannot find the requested sponsoring organization."); + } + + var sponsoringOrgUser = await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgIdGuid, _currentContext.UserId ?? default); + 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 || existingOrgSponsorship.OfferedToEmail == null) + { + throw new BadRequestException("Cannot find an outstanding sponsorship offer for this organization."); + } + + await _organizationsSponsorshipService.SendSponsorshipOfferAsync(sponsoringOrg, existingOrgSponsorship); + } + [HttpPost("redeem")] [SelfHosted(NotSelfHostedOnly = true)] public async Task RedeemSponsorship([FromQuery] string sponsorshipToken, [FromBody] OrganizationSponsorshipRedeemRequestModel model) diff --git a/src/Core/Services/IOrganizationSponsorshipService.cs b/src/Core/Services/IOrganizationSponsorshipService.cs index 78a47092c3..4bb551f4ca 100644 --- a/src/Core/Services/IOrganizationSponsorshipService.cs +++ b/src/Core/Services/IOrganizationSponsorshipService.cs @@ -10,6 +10,7 @@ namespace Bit.Core.Services Task ValidateRedemptionTokenAsync(string encryptedToken); Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName); + Task SendSponsorshipOfferAsync(Organization sponsoringOrg, OrganizationSponsorship sponsorship); Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship, Organization sponsoredOrganization); Task ValidateSponsorshipAsync(Guid sponsoredOrganizationId); Task RemoveSponsorshipAsync(Organization sponsoredOrganization, OrganizationSponsorship sponsorship); diff --git a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs index cac483f2e2..84b4381c77 100644 --- a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs +++ b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs @@ -91,6 +91,7 @@ namespace Bit.Core.Services { sponsorship = await _organizationSponsorshipRepository.CreateAsync(sponsorship); + await SendSponsorshipOfferAsync(sponsoringOrg, sponsorship); await _mailService.SendFamiliesForEnterpriseOfferEmailAsync(sponsoredEmail, sponsoringOrg.Name, RedemptionToken(sponsorship.Id, sponsorshipType)); } @@ -104,6 +105,12 @@ namespace Bit.Core.Services } } + public async Task SendSponsorshipOfferAsync(Organization sponsoringOrg, OrganizationSponsorship sponsorship) + { + await _mailService.SendFamiliesForEnterpriseOfferEmailAsync(sponsorship.OfferedToEmail, sponsoringOrg.Name, + RedemptionToken(sponsorship.Id, sponsorship.PlanSponsorshipType.Value)); + } + public async Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship, Organization sponsoredOrganization) { if (sponsorship.PlanSponsorshipType == null) diff --git a/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs index a944c56f20..b427bcaf22 100644 --- a/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs +++ b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs @@ -103,6 +103,100 @@ namespace Bit.Api.Test.Controllers .OfferSponsorshipAsync(default, default, default, default, default); } + [Theory] + [BitAutoData] + public async Task ResendSponsorshipOffer_SponsoringOrgNotFound_ThrowsBadRequest(Guid sponsoringOrgId, + SutProvider sutProvider) + { + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.ResendSponsorshipOffer(sponsoringOrgId.ToString())); + + Assert.Contains("Cannot find the requested sponsoring organization.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendSponsorshipOfferAsync(default, default); + } + + [Theory] + [BitAutoData] + public async Task ResendSponsorshipOffer_SponsoringOrgUserNotFound_ThrowsBadRequest(Organization org, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.ResendSponsorshipOffer(org.Id.ToString())); + + Assert.Contains("Only confirmed users can sponsor other organizations.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendSponsorshipOfferAsync(default, default); + } + + [Theory] + [BitAutoData] + [BitMemberAutoData(nameof(NonConfirmedOrganizationUsersStatuses))] + public async Task ResendSponsorshipOffer_SponsoringOrgUserNotConfirmed_ThrowsBadRequest(OrganizationUserStatusType status, + Organization org, OrganizationUser orgUser, + SutProvider sutProvider) + { + orgUser.Status = status; + + sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); + sutProvider.GetDependency().UserId.Returns(orgUser.UserId); + sutProvider.GetDependency().GetByOrganizationAsync(org.Id, orgUser.UserId.Value) + .Returns(orgUser); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.ResendSponsorshipOffer(org.Id.ToString())); + + Assert.Contains("Only confirmed users can sponsor other organizations.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendSponsorshipOfferAsync(default, default); + } + + [Theory] + [BitAutoData] + public async Task ResendSponsorshipOffer_SponsorshipNotFound_ThrowsBadRequest(Organization org, + OrganizationUser orgUser, SutProvider sutProvider) + { + orgUser.Status = OrganizationUserStatusType.Confirmed; + + sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); + sutProvider.GetDependency().UserId.Returns(orgUser.UserId); + sutProvider.GetDependency().GetByOrganizationAsync(org.Id, orgUser.UserId.Value) + .Returns(orgUser); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.ResendSponsorshipOffer(org.Id.ToString())); + + Assert.Contains("Cannot find an outstanding sponsorship offer for this organization.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendSponsorshipOfferAsync(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().GetByIdAsync(org.Id).Returns(org); + sutProvider.GetDependency().UserId.Returns(orgUser.UserId); + sutProvider.GetDependency().GetByOrganizationAsync(org.Id, orgUser.UserId.Value) + .Returns(orgUser); + sutProvider.GetDependency().GetBySponsoringOrganizationUserIdAsync(orgUser.Id) + .Returns(sponsorship); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.ResendSponsorshipOffer(org.Id.ToString())); + + Assert.Contains("Cannot find an outstanding sponsorship offer for this organization.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendSponsorshipOfferAsync(default, default); + } + [Theory] [BitAutoData] public async Task RedeemSponsorship_BadToken_ThrowsBadRequest(string sponsorshipToken, @@ -147,7 +241,8 @@ namespace Bit.Api.Test.Controllers sutProvider.GetDependency().ValidateRedemptionTokenAsync(sponsorshipToken) .Returns(true); sutProvider.GetDependency().OrganizationOwner(model.SponsoredOrganizationId).Returns(true); - sutProvider.GetDependency().User.Returns(user); + sutProvider.GetDependency().UserId.Returns(user.Id); + sutProvider.GetDependency().GetUserByIdAsync(user.Id).Returns(user); sutProvider.GetDependency().GetByOfferedToEmailAsync(user.Email) .Returns((OrganizationSponsorship)null); @@ -169,7 +264,8 @@ namespace Bit.Api.Test.Controllers sutProvider.GetDependency().ValidateRedemptionTokenAsync(sponsorshipToken) .Returns(true); sutProvider.GetDependency().OrganizationOwner(model.SponsoredOrganizationId).Returns(true); - sutProvider.GetDependency().User.Returns(user); + sutProvider.GetDependency().UserId.Returns(user.Id); + sutProvider.GetDependency().GetUserByIdAsync(user.Id).Returns(user); sutProvider.GetDependency().GetByOfferedToEmailAsync(user.Email) .Returns(sponsorship); @@ -193,7 +289,8 @@ namespace Bit.Api.Test.Controllers sutProvider.GetDependency().ValidateRedemptionTokenAsync(sponsorshipToken) .Returns(true); sutProvider.GetDependency().OrganizationOwner(model.SponsoredOrganizationId).Returns(true); - sutProvider.GetDependency().User.Returns(user); + sutProvider.GetDependency().UserId.Returns(user.Id); + sutProvider.GetDependency().GetUserByIdAsync(user.Id).Returns(user); sutProvider.GetDependency() .GetByOfferedToEmailAsync(sponsorship.OfferedToEmail).Returns(sponsorship); sutProvider.GetDependency() @@ -220,7 +317,8 @@ namespace Bit.Api.Test.Controllers sutProvider.GetDependency().ValidateRedemptionTokenAsync(sponsorshipToken) .Returns(true); sutProvider.GetDependency().OrganizationOwner(model.SponsoredOrganizationId).Returns(true); - sutProvider.GetDependency().User.Returns(user); + sutProvider.GetDependency().UserId.Returns(user.Id); + sutProvider.GetDependency().GetUserByIdAsync(user.Id).Returns(user); sutProvider.GetDependency() .GetByOfferedToEmailAsync(sponsorship.OfferedToEmail).Returns(sponsorship); sutProvider.GetDependency() @@ -256,21 +354,21 @@ namespace Bit.Api.Test.Controllers [Theory] [BitAutoData] - public async Task RevokeSponsorship_NoExistingSponsorship_ThrowsBadRequest(OrganizationUser sponsoringOrgUser, + public async Task RevokeSponsorship_NoExistingSponsorship_ThrowsBadRequest(OrganizationUser orgUser, OrganizationSponsorship sponsorship, SutProvider sutProvider) { - sutProvider.GetDependency().UserId.Returns(sponsoringOrgUser.UserId); - sutProvider.GetDependency().GetByIdAsync(sponsoringOrgUser.Id) - .Returns(sponsoringOrgUser); + sutProvider.GetDependency().UserId.Returns(orgUser.UserId); + sutProvider.GetDependency().GetByOrganizationAsync(orgUser.OrganizationId, orgUser.UserId.Value) + .Returns(orgUser); sutProvider.GetDependency() - .GetBySponsoringOrganizationUserIdAsync(Arg.Is(v => v != sponsoringOrgUser.Id)) + .GetBySponsoringOrganizationUserIdAsync(Arg.Is(v => v != orgUser.Id)) .Returns(sponsorship); sutProvider.GetDependency() - .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id) + .GetBySponsoringOrganizationUserIdAsync(orgUser.Id) .Returns((OrganizationSponsorship)null); var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.RevokeSponsorship(sponsoringOrgUser.Id.ToString())); + sutProvider.Sut.RevokeSponsorship(orgUser.OrganizationId.ToString())); Assert.Contains("You are not currently sponsoring an organization.", exception.Message); await sutProvider.GetDependency() @@ -280,23 +378,23 @@ namespace Bit.Api.Test.Controllers [Theory] [BitAutoData] - public async Task RevokeSponsorship_SponsorshipNotRedeemed_ThrowsBadRequest(OrganizationUser sponsoringOrgUser, + public async Task RevokeSponsorship_SponsorshipNotRedeemed_ThrowsBadRequest(OrganizationUser orgUser, OrganizationSponsorship sponsorship, SutProvider sutProvider) { sponsorship.SponsoredOrganizationId = null; - sutProvider.GetDependency().UserId.Returns(sponsoringOrgUser.UserId); - sutProvider.GetDependency().GetByIdAsync(sponsoringOrgUser.Id) - .Returns(sponsoringOrgUser); + sutProvider.GetDependency().UserId.Returns(orgUser.UserId); + sutProvider.GetDependency().GetByOrganizationAsync(orgUser.OrganizationId, orgUser.UserId.Value) + .Returns(orgUser); sutProvider.GetDependency() - .GetBySponsoringOrganizationUserIdAsync(Arg.Is(v => v != sponsoringOrgUser.Id)) + .GetBySponsoringOrganizationUserIdAsync(Arg.Is(v => v != orgUser.Id)) .Returns(sponsorship); sutProvider.GetDependency() - .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id) + .GetBySponsoringOrganizationUserIdAsync(orgUser.Id) .Returns((OrganizationSponsorship)sponsorship); var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.RevokeSponsorship(sponsoringOrgUser.Id.ToString())); + sutProvider.Sut.RevokeSponsorship(orgUser.OrganizationId.ToString())); Assert.Contains("You are not currently sponsoring an organization.", exception.Message); await sutProvider.GetDependency() @@ -306,19 +404,19 @@ namespace Bit.Api.Test.Controllers [Theory] [BitAutoData] - public async Task RevokeSponsorship_SponsoredOrgNotFound_ThrowsBadRequest(OrganizationUser sponsoringOrgUser, + public async Task RevokeSponsorship_SponsoredOrgNotFound_ThrowsBadRequest(OrganizationUser orgUser, OrganizationSponsorship sponsorship, SutProvider sutProvider) { - sutProvider.GetDependency().UserId.Returns(sponsoringOrgUser.UserId); - sutProvider.GetDependency().GetByIdAsync(sponsoringOrgUser.Id) - .Returns(sponsoringOrgUser); + sutProvider.GetDependency().UserId.Returns(orgUser.UserId); + sutProvider.GetDependency().GetByOrganizationAsync(orgUser.OrganizationId, orgUser.UserId.Value) + .Returns(orgUser); sutProvider.GetDependency() - .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id) + .GetBySponsoringOrganizationUserIdAsync(orgUser.Id) .Returns(sponsorship); var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.RevokeSponsorship(sponsoringOrgUser.Id.ToString())); + sutProvider.Sut.RevokeSponsorship(orgUser.OrganizationId.ToString())); Assert.Contains("Unable to find the sponsored Organization.", exception.Message); await sutProvider.GetDependency() diff --git a/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs b/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs index 87914910f1..9e595d3121 100644 --- a/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs +++ b/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs @@ -92,5 +92,20 @@ namespace Bit.Core.Test.Services await sutProvider.GetDependency().Received(1) .DeleteAsync(createdSponsorship); } + + [Theory] + [BitAutoData] + public async Task SendSponsorshipOfferAsync(Organization org, OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + await sutProvider.Sut.SendSponsorshipOfferAsync(org, sponsorship); + + await sutProvider.GetDependency().Received(1) + .SendFamiliesForEnterpriseOfferEmailAsync(sponsorship.OfferedToEmail, org.Name, Arg.Any()); + } + + // TODO: test validateSponsorshipAsync + + // TODO: test RemoveSponsorshipAsync } } From 750c5735576f90c57c95537c6fdcb8e422fca4bc Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 10 Nov 2021 17:33:16 -0500 Subject: [PATCH 27/34] Fix double email send --- .../Services/Implementations/OrganizationSponsorshipService.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs index 84b4381c77..2ab815e523 100644 --- a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs +++ b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs @@ -92,8 +92,6 @@ namespace Bit.Core.Services sponsorship = await _organizationSponsorshipRepository.CreateAsync(sponsorship); await SendSponsorshipOfferAsync(sponsoringOrg, sponsorship); - await _mailService.SendFamiliesForEnterpriseOfferEmailAsync(sponsoredEmail, sponsoringOrg.Name, - RedemptionToken(sponsorship.Id, sponsorshipType)); } catch { From 0ae3f1c67e6bba2ce3db372ffebc70665b2fe463 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 10 Nov 2021 18:05:00 -0500 Subject: [PATCH 28/34] Fix sponsorship upgrade options --- src/Core/Models/Business/SubscriptionUpdate.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Models/Business/SubscriptionUpdate.cs b/src/Core/Models/Business/SubscriptionUpdate.cs index 4f0b2cc2ca..36646aef02 100644 --- a/src/Core/Models/Business/SubscriptionUpdate.cs +++ b/src/Core/Models/Business/SubscriptionUpdate.cs @@ -131,6 +131,7 @@ namespace Bit.Core.Models.Business { _existingPlanStripeId = existingPlan.StripePlanId; _sponsoredPlanStripeId = sponsoredPlan.StripePlanId; + _applySponsorship = applySponsorship; } public override List RevertItemsOptions(Subscription subscription) From 77fe21a3326df39a1bebf7c109d5573cfdc74aed Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Wed, 10 Nov 2021 18:05:31 -0500 Subject: [PATCH 29/34] Add is sponsored item to subscription response --- src/Api/Controllers/OrganizationSponsorshipsController.cs | 7 ++++++- src/Core/Models/Api/Response/SubscriptionResponseModel.cs | 2 ++ src/Core/Models/Business/SubscriptionInfo.cs | 2 ++ src/Core/Services/Implementations/StripePaymentService.cs | 3 +-- .../Controllers/OrganizationSponsorshipsControllerTests.cs | 6 +++--- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Api/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Controllers/OrganizationSponsorshipsController.cs index 97e7bf763e..9bd945107d 100644 --- a/src/Api/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Controllers/OrganizationSponsorshipsController.cs @@ -158,10 +158,15 @@ namespace Bit.Api.Controllers var existingOrgSponsorship = await _organizationSponsorshipRepository .GetBySponsoringOrganizationUserIdAsync(orgUser.Id); - if (existingOrgSponsorship == null || existingOrgSponsorship.SponsoredOrganizationId == null) + if (existingOrgSponsorship == null) { throw new BadRequestException("You are not currently sponsoring an organization."); } + if (existingOrgSponsorship.SponsoredOrganizationId == null) + { + await _organizationSponsorshipRepository.DeleteAsync(existingOrgSponsorship); + return; + } var sponsoredOrganization = await _organizationRepository .GetByIdAsync(existingOrgSponsorship.SponsoredOrganizationId.Value); diff --git a/src/Core/Models/Api/Response/SubscriptionResponseModel.cs b/src/Core/Models/Api/Response/SubscriptionResponseModel.cs index d18b3c1cbd..dcc592c7a4 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/SubscriptionInfo.cs b/src/Core/Models/Business/SubscriptionInfo.cs index 2d39894502..57b102ed6c 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/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 31aa08dc55..fcc55c3cac 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -198,8 +198,7 @@ namespace Bit.Core.Services var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value); var subscriptionUpdate = new SponsorOrganizationSubscriptionUpdate(existingPlan, sponsoredPlan, applySponsorship); - var prorationTime = DateTime.UtcNow; - await FinalizeSubscriptionChangeAsync(org, subscriptionUpdate, prorationTime); + await FinalizeSubscriptionChangeAsync(org, subscriptionUpdate, DateTime.UtcNow); var sub = await _stripeAdapter.SubscriptionGetAsync(org.GatewaySubscriptionId); org.ExpirationDate = sub.CurrentPeriodEnd; diff --git a/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs index b427bcaf22..8ddc65ab34 100644 --- a/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs +++ b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs @@ -393,10 +393,10 @@ namespace Bit.Api.Test.Controllers .GetBySponsoringOrganizationUserIdAsync(orgUser.Id) .Returns((OrganizationSponsorship)sponsorship); - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.RevokeSponsorship(orgUser.OrganizationId.ToString())); + await sutProvider.Sut.RevokeSponsorship(orgUser.OrganizationId.ToString()); + + await sutProvider.GetDependency().Received(1).DeleteAsync(sponsorship); - Assert.Contains("You are not currently sponsoring an organization.", exception.Message); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() .RemoveSponsorshipAsync(default, default); From ff55cd5a21f3fb484d21be939a8b9fe823433f98 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Thu, 11 Nov 2021 14:13:45 -0500 Subject: [PATCH 30/34] Add sponsorship validation to upcoming invoice webhook --- src/Billing/Controllers/StripeController.cs | 15 +++--- .../Models/Business/SubscriptionUpdate.cs | 46 ++++++++++++------- ...EnterpriseSponsorshipRevertingViewModel.cs | 2 +- src/Core/Services/IMailService.cs | 2 +- .../Implementations/HandlebarsMailService.cs | 8 ++-- .../OrganizationSponsorshipService.cs | 31 ++++++------- .../Implementations/StripePaymentService.cs | 4 +- 7 files changed, 58 insertions(+), 50 deletions(-) diff --git a/src/Billing/Controllers/StripeController.cs b/src/Billing/Controllers/StripeController.cs index ba6a12a6a2..5adcb1db02 100644 --- a/src/Billing/Controllers/StripeController.cs +++ b/src/Billing/Controllers/StripeController.cs @@ -141,14 +141,6 @@ namespace Bit.Billing.Controllers { var newEndPeriod = subscription.CurrentPeriodEnd; - // sponsored org - if (IsSponsoredSubscription(subscription)) - { - var sponsorshipValid = await _organizationSponsorshipService - .ValidateSponsorshipAsync(ids.Item1.Value); - // TODO: How do we return from this? - } - await _organizationService.UpdateExpirationDateAsync(ids.Item1.Value, subscription.CurrentPeriodEnd); } @@ -177,6 +169,13 @@ namespace Bit.Billing.Controllers // org if (ids.Item1.HasValue) { + // sponsored org + if (IsSponsoredSubscription(subscription)) + { + await _organizationSponsorshipService + .ValidateSponsorshipAsync(ids.Item1.Value); + } + var org = await _organizationRepository.GetByIdAsync(ids.Item1.Value); if (org != null && OrgPlanForInvoiceNotifications(org)) { diff --git a/src/Core/Models/Business/SubscriptionUpdate.cs b/src/Core/Models/Business/SubscriptionUpdate.cs index 36646aef02..93991b78a8 100644 --- a/src/Core/Models/Business/SubscriptionUpdate.cs +++ b/src/Core/Models/Business/SubscriptionUpdate.cs @@ -28,7 +28,7 @@ namespace Bit.Core.Models.Business } protected static SubscriptionItem SubscriptionItem(Subscription subscription, string planId) => - subscription.Items?.Data?.FirstOrDefault(i => i.Plan.Id == planId); + planId == null ? null : subscription.Items?.Data?.FirstOrDefault(i => i.Plan.Id == planId); } @@ -122,58 +122,70 @@ namespace Bit.Core.Models.Business public class SponsorOrganizationSubscriptionUpdate : SubscriptionUpdate { - private string _existingPlanStripeId; - private string _sponsoredPlanStripeId; - private bool _applySponsorship; + 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; + _sponsoredPlanStripeId = sponsoredPlan?.StripePlanId; _applySponsorship = applySponsorship; } public override List RevertItemsOptions(Subscription subscription) { - return new() + var result = new List(); + if (AddStripeItem(subscription) != null) { - new SubscriptionItemOptions + result.Add(new SubscriptionItemOptions { Id = AddStripeItem(subscription)?.Id, Plan = AddStripePlanId, Quantity = 0, Deleted = true, - }, - new SubscriptionItemOptions + }); + } + + if (RemoveStripeItem(subscription) != null) + { + result.Add(new SubscriptionItemOptions { Id = RemoveStripeItem(subscription)?.Id, Plan = RemoveStripePlanId, Quantity = 1, Deleted = false, - }, - }; + }); + } + return result; } public override List UpgradeItemsOptions(Subscription subscription) { - return new() + var result = new List(); + if (RemoveStripeItem(subscription) != null) { - new SubscriptionItemOptions + result.Add(new SubscriptionItemOptions { Id = RemoveStripeItem(subscription)?.Id, Plan = RemoveStripePlanId, Quantity = 0, Deleted = true, - }, - new SubscriptionItemOptions + }); + } + + if (AddStripeItem(subscription) != null) + { + result.Add(new SubscriptionItemOptions { Id = AddStripeItem(subscription)?.Id, Plan = AddStripePlanId, Quantity = 1, Deleted = false, - }, - }; + }); + } + return result; } private string RemoveStripePlanId => _applySponsorship ? _existingPlanStripeId : _sponsoredPlanStripeId; diff --git a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipRevertingViewModel.cs b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipRevertingViewModel.cs index cef96099f8..e8185000bd 100644 --- a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipRevertingViewModel.cs +++ b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipRevertingViewModel.cs @@ -2,6 +2,6 @@ namespace Bit.Core.Models.Mail.FamiliesForEnterprise { public class FamiliesForEnterpriseSponsorshipRevertingViewModel : BaseMailModel { - + public string OrganizationName { get; set; } } } diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 5e8ea8f3b3..6c23ab1d03 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -53,7 +53,7 @@ namespace Bit.Core.Services Task SendFamiliesForEnterpriseOfferEmailAsync(string email, string organizationName, string token); Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail, string sponsorOrgName); Task SendFamiliesForEnterpriseReconfirmationRequiredEmailAsync(string email); - Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email); + Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, string familyOrgName); Task SendFamiliesForEnterpriseSponsorshipEndingEmailAsync(string email); } } diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 301f89e1ac..fdc2afe5d0 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -801,7 +801,7 @@ namespace Bit.Core.Services var message = CreateDefaultMessage("A User Has Redeemeed Your Sponsorship", email); var model = new FamiliesForEnterpriseRedeemedToOrgUserViewModel { - OrganizationName = organizationName, + OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false), }; await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseRedeemedToOrgUser", model); message.Category = "FamilyForEnterpriseRedeemedToOrgUser"; @@ -821,13 +821,13 @@ namespace Bit.Core.Services await _mailDeliveryService.SendEmailAsync(message); } - public async Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email) + public async Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, string familyOrgName) { // TODO: Complete emails - var message = CreateDefaultMessage("A Family Organization Sponsorship Is Reverting", email); + 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"; diff --git a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs index 2ab815e523..8dde09d9e3 100644 --- a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs +++ b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs @@ -133,38 +133,30 @@ namespace Bit.Core.Services if (existingSponsorship == null) { - // TODO: null safe this method await RemoveSponsorshipAsync(sponsoredOrganization, null); - // TODO on fail, mark org as disabled. return false; } - var validated = true; - if (existingSponsorship.SponsoringOrganizationId == null || existingSponsorship.SponsoringOrganizationUserId == null) + if (existingSponsorship.SponsoringOrganizationId == null || existingSponsorship.SponsoringOrganizationUserId == null || existingSponsorship.PlanSponsorshipType == null) { await RemoveSponsorshipAsync(sponsoredOrganization, existingSponsorship); - validated = false; + return false; } + var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(existingSponsorship.PlanSponsorshipType.Value); var sponsoringOrganization = await _organizationRepository .GetByIdAsync(existingSponsorship.SponsoringOrganizationId.Value); - if (!sponsoringOrganization.Enabled) + if (sponsoringOrganization == null) { await RemoveSponsorshipAsync(sponsoredOrganization, existingSponsorship); - validated = false; + return false; } - if (!validated && existingSponsorship.SponsoredOrganizationId != null) + var sponsoringOrgPlan = Utilities.StaticStore.GetPlan(sponsoringOrganization.PlanType); + if (!sponsoringOrganization.Enabled || sponsoredPlan.SponsoringProductType != sponsoringOrgPlan.Product) { - existingSponsorship.TimesRenewedWithoutValidation += 1; - existingSponsorship.SponsorshipLapsedDate ??= DateTime.UtcNow; - - await _organizationSponsorshipRepository.UpsertAsync(existingSponsorship); - if (existingSponsorship.TimesRenewedWithoutValidation >= 6) - { - sponsoredOrganization.Enabled = false; - await _organizationRepository.UpsertAsync(sponsoredOrganization); - } + await RemoveSponsorshipAsync(sponsoredOrganization, existingSponsorship); + return false; } return true; @@ -175,6 +167,10 @@ namespace Bit.Core.Services await _paymentService.RemoveOrganizationSponsorshipAsync(sponsoredOrganization, sponsorship); await _organizationRepository.UpsertAsync(sponsoredOrganization); + await _mailService.SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync( + sponsoredOrganization.BillingEmailAddress(), + sponsoredOrganization.Name); + if (sponsorship == null) { return; @@ -197,6 +193,5 @@ namespace Bit.Core.Services await _organizationSponsorshipRepository.UpsertAsync(sponsorship); } } - } } diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index fcc55c3cac..7ca89b2012 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -195,7 +195,9 @@ namespace Bit.Core.Services private async Task ChangeOrganizationSponsorship(Organization org, OrganizationSponsorship sponsorship, bool applySponsorship) { var existingPlan = Utilities.StaticStore.GetPlan(org.PlanType); - var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value); + var sponsoredPlan = sponsorship != null ? + Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value) : + null; var subscriptionUpdate = new SponsorOrganizationSubscriptionUpdate(existingPlan, sponsoredPlan, applySponsorship); await FinalizeSubscriptionChangeAsync(org, subscriptionUpdate, DateTime.UtcNow); From da4388be4cb6c1b2cc3c19402e8fde290d59a387 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Thu, 11 Nov 2021 14:13:45 -0500 Subject: [PATCH 31/34] Add sponsorship validation to upcoming invoice webhook --- src/Billing/Controllers/StripeController.cs | 15 +++--- .../Models/Business/SubscriptionUpdate.cs | 46 ++++++++++++------- ...EnterpriseSponsorshipRevertingViewModel.cs | 2 +- src/Core/Services/IMailService.cs | 2 +- .../Implementations/HandlebarsMailService.cs | 8 ++-- .../OrganizationSponsorshipService.cs | 31 ++++++------- .../Implementations/StripePaymentService.cs | 4 +- .../NoopImplementations/NoopMailService.cs | 2 +- 8 files changed, 59 insertions(+), 51 deletions(-) diff --git a/src/Billing/Controllers/StripeController.cs b/src/Billing/Controllers/StripeController.cs index ba6a12a6a2..5adcb1db02 100644 --- a/src/Billing/Controllers/StripeController.cs +++ b/src/Billing/Controllers/StripeController.cs @@ -141,14 +141,6 @@ namespace Bit.Billing.Controllers { var newEndPeriod = subscription.CurrentPeriodEnd; - // sponsored org - if (IsSponsoredSubscription(subscription)) - { - var sponsorshipValid = await _organizationSponsorshipService - .ValidateSponsorshipAsync(ids.Item1.Value); - // TODO: How do we return from this? - } - await _organizationService.UpdateExpirationDateAsync(ids.Item1.Value, subscription.CurrentPeriodEnd); } @@ -177,6 +169,13 @@ namespace Bit.Billing.Controllers // org if (ids.Item1.HasValue) { + // sponsored org + if (IsSponsoredSubscription(subscription)) + { + await _organizationSponsorshipService + .ValidateSponsorshipAsync(ids.Item1.Value); + } + var org = await _organizationRepository.GetByIdAsync(ids.Item1.Value); if (org != null && OrgPlanForInvoiceNotifications(org)) { diff --git a/src/Core/Models/Business/SubscriptionUpdate.cs b/src/Core/Models/Business/SubscriptionUpdate.cs index 36646aef02..93991b78a8 100644 --- a/src/Core/Models/Business/SubscriptionUpdate.cs +++ b/src/Core/Models/Business/SubscriptionUpdate.cs @@ -28,7 +28,7 @@ namespace Bit.Core.Models.Business } protected static SubscriptionItem SubscriptionItem(Subscription subscription, string planId) => - subscription.Items?.Data?.FirstOrDefault(i => i.Plan.Id == planId); + planId == null ? null : subscription.Items?.Data?.FirstOrDefault(i => i.Plan.Id == planId); } @@ -122,58 +122,70 @@ namespace Bit.Core.Models.Business public class SponsorOrganizationSubscriptionUpdate : SubscriptionUpdate { - private string _existingPlanStripeId; - private string _sponsoredPlanStripeId; - private bool _applySponsorship; + 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; + _sponsoredPlanStripeId = sponsoredPlan?.StripePlanId; _applySponsorship = applySponsorship; } public override List RevertItemsOptions(Subscription subscription) { - return new() + var result = new List(); + if (AddStripeItem(subscription) != null) { - new SubscriptionItemOptions + result.Add(new SubscriptionItemOptions { Id = AddStripeItem(subscription)?.Id, Plan = AddStripePlanId, Quantity = 0, Deleted = true, - }, - new SubscriptionItemOptions + }); + } + + if (RemoveStripeItem(subscription) != null) + { + result.Add(new SubscriptionItemOptions { Id = RemoveStripeItem(subscription)?.Id, Plan = RemoveStripePlanId, Quantity = 1, Deleted = false, - }, - }; + }); + } + return result; } public override List UpgradeItemsOptions(Subscription subscription) { - return new() + var result = new List(); + if (RemoveStripeItem(subscription) != null) { - new SubscriptionItemOptions + result.Add(new SubscriptionItemOptions { Id = RemoveStripeItem(subscription)?.Id, Plan = RemoveStripePlanId, Quantity = 0, Deleted = true, - }, - new SubscriptionItemOptions + }); + } + + if (AddStripeItem(subscription) != null) + { + result.Add(new SubscriptionItemOptions { Id = AddStripeItem(subscription)?.Id, Plan = AddStripePlanId, Quantity = 1, Deleted = false, - }, - }; + }); + } + return result; } private string RemoveStripePlanId => _applySponsorship ? _existingPlanStripeId : _sponsoredPlanStripeId; diff --git a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipRevertingViewModel.cs b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipRevertingViewModel.cs index cef96099f8..e8185000bd 100644 --- a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipRevertingViewModel.cs +++ b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipRevertingViewModel.cs @@ -2,6 +2,6 @@ namespace Bit.Core.Models.Mail.FamiliesForEnterprise { public class FamiliesForEnterpriseSponsorshipRevertingViewModel : BaseMailModel { - + public string OrganizationName { get; set; } } } diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 5e8ea8f3b3..6c23ab1d03 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -53,7 +53,7 @@ namespace Bit.Core.Services Task SendFamiliesForEnterpriseOfferEmailAsync(string email, string organizationName, string token); Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail, string sponsorOrgName); Task SendFamiliesForEnterpriseReconfirmationRequiredEmailAsync(string email); - Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email); + Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, string familyOrgName); Task SendFamiliesForEnterpriseSponsorshipEndingEmailAsync(string email); } } diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 301f89e1ac..fdc2afe5d0 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -801,7 +801,7 @@ namespace Bit.Core.Services var message = CreateDefaultMessage("A User Has Redeemeed Your Sponsorship", email); var model = new FamiliesForEnterpriseRedeemedToOrgUserViewModel { - OrganizationName = organizationName, + OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false), }; await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseRedeemedToOrgUser", model); message.Category = "FamilyForEnterpriseRedeemedToOrgUser"; @@ -821,13 +821,13 @@ namespace Bit.Core.Services await _mailDeliveryService.SendEmailAsync(message); } - public async Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email) + public async Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, string familyOrgName) { // TODO: Complete emails - var message = CreateDefaultMessage("A Family Organization Sponsorship Is Reverting", email); + 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"; diff --git a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs index 2ab815e523..8dde09d9e3 100644 --- a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs +++ b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs @@ -133,38 +133,30 @@ namespace Bit.Core.Services if (existingSponsorship == null) { - // TODO: null safe this method await RemoveSponsorshipAsync(sponsoredOrganization, null); - // TODO on fail, mark org as disabled. return false; } - var validated = true; - if (existingSponsorship.SponsoringOrganizationId == null || existingSponsorship.SponsoringOrganizationUserId == null) + if (existingSponsorship.SponsoringOrganizationId == null || existingSponsorship.SponsoringOrganizationUserId == null || existingSponsorship.PlanSponsorshipType == null) { await RemoveSponsorshipAsync(sponsoredOrganization, existingSponsorship); - validated = false; + return false; } + var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(existingSponsorship.PlanSponsorshipType.Value); var sponsoringOrganization = await _organizationRepository .GetByIdAsync(existingSponsorship.SponsoringOrganizationId.Value); - if (!sponsoringOrganization.Enabled) + if (sponsoringOrganization == null) { await RemoveSponsorshipAsync(sponsoredOrganization, existingSponsorship); - validated = false; + return false; } - if (!validated && existingSponsorship.SponsoredOrganizationId != null) + var sponsoringOrgPlan = Utilities.StaticStore.GetPlan(sponsoringOrganization.PlanType); + if (!sponsoringOrganization.Enabled || sponsoredPlan.SponsoringProductType != sponsoringOrgPlan.Product) { - existingSponsorship.TimesRenewedWithoutValidation += 1; - existingSponsorship.SponsorshipLapsedDate ??= DateTime.UtcNow; - - await _organizationSponsorshipRepository.UpsertAsync(existingSponsorship); - if (existingSponsorship.TimesRenewedWithoutValidation >= 6) - { - sponsoredOrganization.Enabled = false; - await _organizationRepository.UpsertAsync(sponsoredOrganization); - } + await RemoveSponsorshipAsync(sponsoredOrganization, existingSponsorship); + return false; } return true; @@ -175,6 +167,10 @@ namespace Bit.Core.Services await _paymentService.RemoveOrganizationSponsorshipAsync(sponsoredOrganization, sponsorship); await _organizationRepository.UpsertAsync(sponsoredOrganization); + await _mailService.SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync( + sponsoredOrganization.BillingEmailAddress(), + sponsoredOrganization.Name); + if (sponsorship == null) { return; @@ -197,6 +193,5 @@ namespace Bit.Core.Services await _organizationSponsorshipRepository.UpsertAsync(sponsorship); } } - } } diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index fcc55c3cac..7ca89b2012 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -195,7 +195,9 @@ namespace Bit.Core.Services private async Task ChangeOrganizationSponsorship(Organization org, OrganizationSponsorship sponsorship, bool applySponsorship) { var existingPlan = Utilities.StaticStore.GetPlan(org.PlanType); - var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value); + var sponsoredPlan = sponsorship != null ? + Utilities.StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value) : + null; var subscriptionUpdate = new SponsorOrganizationSubscriptionUpdate(existingPlan, sponsoredPlan, applySponsorship); await FinalizeSubscriptionChangeAsync(org, subscriptionUpdate, DateTime.UtcNow); diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index f32e9d96ae..0896ac544f 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -216,7 +216,7 @@ namespace Bit.Core.Services return Task.FromResult(0); } - public Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email) + public Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, string familyOrgName) { return Task.FromResult(0); } From 1d11dac2cc37ec678fed180a7e4cabf86c319a00 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Thu, 11 Nov 2021 15:00:16 -0500 Subject: [PATCH 32/34] Fix organization delete sponsorship hooks --- .../Repositories/EntityFramework/OrganizationRepository.cs | 4 ++-- .../EntityFramework/OrganizationUserRepository.cs | 2 +- src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteById.sql | 2 +- .../DbScripts/2021-11-02_00_OrganizationSponsorship.sql | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Core/Repositories/EntityFramework/OrganizationRepository.cs b/src/Core/Repositories/EntityFramework/OrganizationRepository.cs index b7e9ccbaf6..ea5cbc50fb 100644 --- a/src/Core/Repositories/EntityFramework/OrganizationRepository.cs +++ b/src/Core/Repositories/EntityFramework/OrganizationRepository.cs @@ -101,7 +101,7 @@ namespace Bit.Core.Repositories.EntityFramework using (var scope = ServiceScopeFactory.CreateScope()) { var dbContext = GetDatabaseContext(scope); - var orgUser = dbContext.FindAsync(organization.Id); + var orgEntity = await dbContext.FindAsync(organization.Id); var sponsorships = dbContext.OrganizationSponsorships .Where(os => os.SponsoringOrganizationId == organization.Id || @@ -115,7 +115,7 @@ namespace Bit.Core.Repositories.EntityFramework sponsorship.SponsoringOrganizationId = UpdatedOrgId(sponsorship.SponsoringOrganizationId); } - dbContext.Remove(orgUser); + dbContext.Remove(orgEntity); await dbContext.SaveChangesAsync(); } } diff --git a/src/Core/Repositories/EntityFramework/OrganizationUserRepository.cs b/src/Core/Repositories/EntityFramework/OrganizationUserRepository.cs index 2be24d83b1..8cc3f4f2eb 100644 --- a/src/Core/Repositories/EntityFramework/OrganizationUserRepository.cs +++ b/src/Core/Repositories/EntityFramework/OrganizationUserRepository.cs @@ -73,7 +73,7 @@ namespace Bit.Core.Repositories.EntityFramework using (var scope = ServiceScopeFactory.CreateScope()) { var dbContext = GetDatabaseContext(scope); - var orgUser = dbContext.FindAsync(organizationUserId); + var orgUser = await dbContext.FindAsync(organizationUserId); var sponsorships = dbContext.OrganizationSponsorships .Where(os => os.SponsoringOrganizationUserId != default && os.SponsoringOrganizationUserId.Value == organizationUserId); diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteById.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteById.sql index 5817e49394..0b4611a7d2 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteById.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteById.sql @@ -34,7 +34,7 @@ BEGIN WHERE [OrganizationUserId] = @Id - EXEC [dbo].[OrganizationUser_DeleteById] @Id + EXEC [dbo].[OrganizationSponsorship_OrganizationUserDeleted] @Id DELETE FROM diff --git a/util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql b/util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql index 079aacd924..9cb6ff1ae7 100644 --- a/util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql +++ b/util/Migrator/DbScripts/2021-11-02_00_OrganizationSponsorship.sql @@ -512,7 +512,7 @@ BEGIN WHERE [OrganizationUserId] = @Id - EXEC [dbo].[OrganizationUser_DeleteById] @Id + EXEC [dbo].[OrganizationSponsorship_OrganizationUserDeleted] @Id DELETE FROM From f1832e899f0e2653ff19992769f7bcbbfc978b77 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Fri, 12 Nov 2021 11:37:13 -0500 Subject: [PATCH 33/34] Test org sponsorship service --- .../OrganizationSponsorshipService.cs | 18 +- .../Helpers/BitAutoDataAttributeHelpers.cs | 8 +- .../OrganizationSponsorshipServiceTests.cs | 260 +++++++++++++++++- 3 files changed, 273 insertions(+), 13 deletions(-) diff --git a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs index 8dde09d9e3..b3ddc7a400 100644 --- a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs +++ b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs @@ -128,6 +128,11 @@ namespace Bit.Core.Services public async Task ValidateSponsorshipAsync(Guid sponsoredOrganizationId) { var sponsoredOrganization = await _organizationRepository.GetByIdAsync(sponsoredOrganizationId); + if (sponsoredOrganization == null) + { + return false; + } + var existingSponsorship = await _organizationSponsorshipRepository .GetBySponsoredOrganizationIdAsync(sponsoredOrganizationId); @@ -164,12 +169,15 @@ namespace Bit.Core.Services public async Task RemoveSponsorshipAsync(Organization sponsoredOrganization, OrganizationSponsorship sponsorship = null) { - await _paymentService.RemoveOrganizationSponsorshipAsync(sponsoredOrganization, sponsorship); - await _organizationRepository.UpsertAsync(sponsoredOrganization); + if (sponsoredOrganization != null) + { + await _paymentService.RemoveOrganizationSponsorshipAsync(sponsoredOrganization, sponsorship); + await _organizationRepository.UpsertAsync(sponsoredOrganization); - await _mailService.SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync( - sponsoredOrganization.BillingEmailAddress(), - sponsoredOrganization.Name); + await _mailService.SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync( + sponsoredOrganization.BillingEmailAddress(), + sponsoredOrganization.Name); + } if (sponsorship == null) { diff --git a/test/Common/Helpers/BitAutoDataAttributeHelpers.cs b/test/Common/Helpers/BitAutoDataAttributeHelpers.cs index 394a03ed86..635d996719 100644 --- a/test/Common/Helpers/BitAutoDataAttributeHelpers.cs +++ b/test/Common/Helpers/BitAutoDataAttributeHelpers.cs @@ -11,18 +11,18 @@ namespace Bit.Test.Common.Helpers { public static class BitAutoDataAttributeHelpers { - public static IEnumerable GetData(MethodInfo testMethod, IFixture fixture, object[] fixedTestParamters) + 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()); - fixedTestParamters = fixedTestParamters ?? Array.Empty(); + fixedTestParameters = fixedTestParameters ?? Array.Empty(); fixture = ApplyCustomizations(ApplyCustomizations(fixture, classCustomizations), methodCustomizations); - var missingParameters = methodParameters.Skip(fixedTestParamters.Length).Select(p => CustomizeAndCreate(p, fixture)); + var missingParameters = methodParameters.Skip(fixedTestParameters.Length).Select(p => CustomizeAndCreate(p, fixture)); - return new object[1][] { fixedTestParamters.Concat(missingParameters).ToArray() }; + return new object[1][] { fixedTestParameters.Concat(missingParameters).ToArray() }; } public static object CustomizeAndCreate(ParameterInfo p, IFixture fixture) diff --git a/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs b/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs index 9e595d3121..162cb2a6e6 100644 --- a/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs +++ b/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs @@ -1,18 +1,18 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Bit.Core.Enums; 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 Microsoft.IdentityModel.Tokens; using NSubstitute; using NSubstitute.ExceptionExtensions; -using NSubstitute.Extensions; using Xunit; namespace Bit.Core.Test.Services @@ -33,6 +33,12 @@ namespace Bit.Core.Test.Services } } + 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 }); + [Theory] [BitAutoData] public async Task OfferSponsorship_CreatesSponsorship(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, @@ -104,8 +110,254 @@ namespace Bit.Core.Test.Services .SendFamiliesForEnterpriseOfferEmailAsync(sponsorship.OfferedToEmail, org.Name, Arg.Any()); } - // TODO: test validateSponsorshipAsync + 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); + } - // TODO: test RemoveSponsorshipAsync + 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 RemoveSponsorshipAsync_NullDoNothing(SutProvider sutProvider) + { + await sutProvider.Sut.RemoveSponsorshipAsync(null, null); + + await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); + await AssertDidNotRemoveSponsorshipAsync(sutProvider); + } + + [Theory] + [BitAutoData] + public async Task RemoveSponsorshipAsync_NullSponsoredOrg(OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + await sutProvider.Sut.RemoveSponsorshipAsync(null, sponsorship); + + await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); + await AssertRemovedSponsorshipAsync(sponsorship, sutProvider); + } + + [Theory] + [BitAutoData] + public async Task RemoveSponsorshipAsync_NullSponsorship(Organization sponsoredOrg, + SutProvider sutProvider) + { + await sutProvider.Sut.RemoveSponsorshipAsync(sponsoredOrg, null); + + await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, null, sutProvider); + await AssertDidNotRemoveSponsorshipAsync(sutProvider); + } + + [Theory] + [BitAutoData] + public async Task RemoveSponsorshipAsync_RemoveBoth(Organization sponsoredOrg, + OrganizationSponsorship sponsorship, SutProvider sutProvider) + { + await sutProvider.Sut.RemoveSponsorshipAsync(sponsoredOrg, sponsorship); + + await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, sponsorship, sutProvider); + await AssertRemovedSponsorshipAsync(sponsorship, sutProvider); + } } } From 79f9bd45457b06a8f379545fc68042c228a4dd1b Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Fri, 12 Nov 2021 17:47:57 -0500 Subject: [PATCH 34/34] Fix sproc --- .../dbo/Stored Procedures/OrganizationUser_DeleteById.sql | 8 -------- src/Sql/dbo/Stored Procedures/Organization_DeleteById.sql | 1 - 2 files changed, 9 deletions(-) diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteById.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteById.sql index 0b4611a7d2..9b7f8187e8 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteById.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_DeleteById.sql @@ -42,11 +42,3 @@ BEGIN WHERE [Id] = @Id END -GO - - -IF OBJECT_ID('[dbo].[OrganizationUser_DeleteByIds]') IS NOT NULL -BEGIN - DROP PROCEDURE [dbo].[OrganizationUser_DeleteByIds] -END -GO diff --git a/src/Sql/dbo/Stored Procedures/Organization_DeleteById.sql b/src/Sql/dbo/Stored Procedures/Organization_DeleteById.sql index 3015179290..d32f7d3606 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_DeleteById.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_DeleteById.sql @@ -67,4 +67,3 @@ BEGIN COMMIT TRANSACTION Organization_DeleteById END -GO