From 70ab5b25a15fe0ca57cc48a7cfd05b75488da4db Mon Sep 17 00:00:00 2001 From: Vincent Salucci <26154748+vincentsalucci@users.noreply.github.com> Date: Thu, 6 May 2021 14:53:12 -0500 Subject: [PATCH] [Reset Password] Organization Key Pair (#1292) * [Reset Password] Organization Key Pair * Fixed type in Organization_ReadAbilites sproc * Fixed broken unit test by making sure premium addon was false * Updated PublicKey decorator and removed unecessary validation --- .../Controllers/OrganizationsController.cs | 3 +- src/Core/Models/Api/Public/MemberBaseModel.cs | 7 + .../OrganizationCreateLicenseRequestModel.cs | 1 + .../OrganizationCreateRequestModel.cs | 7 +- .../OrganizationKeysRequestModel.cs | 59 +++ .../OrganizationUpdateRequestModel.cs | 3 +- .../OrganizationUpgradeRequestModel.cs | 7 +- .../Api/Response/OrganizationResponseModel.cs | 4 + .../Models/Api/Response/PlanResponseModel.cs | 2 + .../ProfileOrganizationResponseModel.cs | 4 + .../Models/Business/OrganizationLicense.cs | 17 +- .../Models/Business/OrganizationUpgrade.cs | 2 + src/Core/Models/Data/OrganizationAbility.cs | 2 + .../OrganizationUserOrganizationDetails.cs | 3 + src/Core/Models/StaticStore/Plan.cs | 1 + src/Core/Models/Table/Organization.cs | 3 + src/Core/Services/IOrganizationService.cs | 2 +- .../Implementations/OrganizationService.cs | 18 +- src/Core/Utilities/StaticStore.cs | 2 + .../Stored Procedures/Organization_Create.sql | 9 + .../Organization_ReadAbilities.sql | 1 + .../Stored Procedures/Organization_Update.sql | 6 + src/Sql/dbo/Tables/Organization.sql | 3 + ...rganizationUserOrganizationDetailsView.sql | 3 + .../AutoFixture/OrganizationFixtures.cs | 6 +- ...8_00_OrgResetPasswordAbilityAndRsaKeys.sql | 372 ++++++++++++++++++ 26 files changed, 535 insertions(+), 12 deletions(-) create mode 100644 src/Core/Models/Api/Request/Organizations/OrganizationKeysRequestModel.cs create mode 100644 util/Migrator/DbScripts/2021-04-28_00_OrgResetPasswordAbilityAndRsaKeys.sql diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index 43a484facf..70c356fc1f 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -206,7 +206,8 @@ namespace Bit.Api.Controllers "which has a policy that prohibits you from being a member of any other organization."); } - var result = await _organizationService.SignUpAsync(license, user, model.Key, model.CollectionName); + var result = await _organizationService.SignUpAsync(license, user, model.Key, + model.CollectionName, model.Keys?.PublicKey, model.Keys?.EncryptedPrivateKey); return new OrganizationResponseModel(result.Item1); } diff --git a/src/Core/Models/Api/Public/MemberBaseModel.cs b/src/Core/Models/Api/Public/MemberBaseModel.cs index 3786d0bd1b..503abe1b0f 100644 --- a/src/Core/Models/Api/Public/MemberBaseModel.cs +++ b/src/Core/Models/Api/Public/MemberBaseModel.cs @@ -20,6 +20,7 @@ namespace Bit.Core.Models.Api.Public Type = user.Type; AccessAll = user.AccessAll; ExternalId = user.ExternalId; + ResetPasswordEnrolled = user.ResetPasswordKey != null; } public MemberBaseModel(OrganizationUserUserDetails user) @@ -32,6 +33,7 @@ namespace Bit.Core.Models.Api.Public Type = user.Type; AccessAll = user.AccessAll; ExternalId = user.ExternalId; + ResetPasswordEnrolled = user.ResetPasswordKey != null; } /// @@ -51,5 +53,10 @@ namespace Bit.Core.Models.Api.Public /// external_id_123456 [StringLength(300)] public string ExternalId { get; set; } + /// + /// Returns true if the member has enrolled in Password Reset assistance within the organization + /// + [Required] + public bool ResetPasswordEnrolled { get; set; } } } diff --git a/src/Core/Models/Api/Request/Organizations/OrganizationCreateLicenseRequestModel.cs b/src/Core/Models/Api/Request/Organizations/OrganizationCreateLicenseRequestModel.cs index 97cff0a6a6..0ccd89e670 100644 --- a/src/Core/Models/Api/Request/Organizations/OrganizationCreateLicenseRequestModel.cs +++ b/src/Core/Models/Api/Request/Organizations/OrganizationCreateLicenseRequestModel.cs @@ -10,5 +10,6 @@ namespace Bit.Core.Models.Api [EncryptedString] [EncryptedStringLength(1000)] public string CollectionName { get; set; } + public OrganizationKeysRequestModel Keys { get; set; } } } diff --git a/src/Core/Models/Api/Request/Organizations/OrganizationCreateRequestModel.cs b/src/Core/Models/Api/Request/Organizations/OrganizationCreateRequestModel.cs index e144d31c5f..592572b674 100644 --- a/src/Core/Models/Api/Request/Organizations/OrganizationCreateRequestModel.cs +++ b/src/Core/Models/Api/Request/Organizations/OrganizationCreateRequestModel.cs @@ -21,6 +21,7 @@ namespace Bit.Core.Models.Api public PlanType PlanType { get; set; } [Required] public string Key { get; set; } + public OrganizationKeysRequestModel Keys { get; set; } public PaymentMethodType? PaymentMethodType { get; set; } public string PaymentToken { get; set; } [Range(0, double.MaxValue)] @@ -42,7 +43,7 @@ namespace Bit.Core.Models.Api public virtual OrganizationSignup ToOrganizationSignup(User user) { - return new OrganizationSignup + var orgSignup = new OrganizationSignup { Owner = user, OwnerKey = Key, @@ -67,6 +68,10 @@ namespace Bit.Core.Models.Api BillingAddressCountry = BillingAddressCountry, }, }; + + Keys?.ToOrganizationSignup(orgSignup); + + return orgSignup; } public IEnumerable Validate(ValidationContext validationContext) diff --git a/src/Core/Models/Api/Request/Organizations/OrganizationKeysRequestModel.cs b/src/Core/Models/Api/Request/Organizations/OrganizationKeysRequestModel.cs new file mode 100644 index 0000000000..760e7c5d7b --- /dev/null +++ b/src/Core/Models/Api/Request/Organizations/OrganizationKeysRequestModel.cs @@ -0,0 +1,59 @@ +using Bit.Core.Models.Table; +using System.ComponentModel.DataAnnotations; +using Bit.Core.Models.Business; + +namespace Bit.Core.Models.Api +{ + public class OrganizationKeysRequestModel + { + [Required] + public string PublicKey { get; set; } + [Required] + public string EncryptedPrivateKey { get; set; } + + public OrganizationSignup ToOrganizationSignup(OrganizationSignup existingSignup) + { + if (string.IsNullOrWhiteSpace(existingSignup.PublicKey)) + { + existingSignup.PublicKey = PublicKey; + } + + if (string.IsNullOrWhiteSpace(existingSignup.PrivateKey)) + { + existingSignup.PrivateKey = EncryptedPrivateKey; + } + + return existingSignup; + } + + public OrganizationUpgrade ToOrganizationUpgrade(OrganizationUpgrade existingUpgrade) + { + if (string.IsNullOrWhiteSpace(existingUpgrade.PublicKey)) + { + existingUpgrade.PublicKey = PublicKey; + } + + if (string.IsNullOrWhiteSpace(existingUpgrade.PrivateKey)) + { + existingUpgrade.PrivateKey = EncryptedPrivateKey; + } + + return existingUpgrade; + } + + public Organization ToOrganization(Organization existingOrg) + { + if (string.IsNullOrWhiteSpace(existingOrg.PublicKey)) + { + existingOrg.PublicKey = PublicKey; + } + + if (string.IsNullOrWhiteSpace(existingOrg.PrivateKey)) + { + existingOrg.PrivateKey = EncryptedPrivateKey; + } + + return existingOrg; + } + } +} diff --git a/src/Core/Models/Api/Request/Organizations/OrganizationUpdateRequestModel.cs b/src/Core/Models/Api/Request/Organizations/OrganizationUpdateRequestModel.cs index 8e3dea6624..eec08c9aa8 100644 --- a/src/Core/Models/Api/Request/Organizations/OrganizationUpdateRequestModel.cs +++ b/src/Core/Models/Api/Request/Organizations/OrganizationUpdateRequestModel.cs @@ -18,8 +18,8 @@ namespace Bit.Core.Models.Api [Required] [StringLength(256)] public string BillingEmail { get; set; } - public Permissions Permissions { get; set; } + public OrganizationKeysRequestModel Keys { get; set; } public virtual Organization ToOrganization(Organization existingOrganization, GlobalSettings globalSettings) { @@ -31,6 +31,7 @@ namespace Bit.Core.Models.Api existingOrganization.BillingEmail = BillingEmail?.ToLowerInvariant()?.Trim(); } existingOrganization.Identifier = Identifier; + Keys?.ToOrganization(existingOrganization); return existingOrganization; } } diff --git a/src/Core/Models/Api/Request/Organizations/OrganizationUpgradeRequestModel.cs b/src/Core/Models/Api/Request/Organizations/OrganizationUpgradeRequestModel.cs index f11fff0cc5..2283ed18d5 100644 --- a/src/Core/Models/Api/Request/Organizations/OrganizationUpgradeRequestModel.cs +++ b/src/Core/Models/Api/Request/Organizations/OrganizationUpgradeRequestModel.cs @@ -16,10 +16,11 @@ namespace Bit.Core.Models.Api public bool PremiumAccessAddon { get; set; } public string BillingAddressCountry { get; set; } public string BillingAddressPostalCode { get; set; } + public OrganizationKeysRequestModel Keys { get; set; } public OrganizationUpgrade ToOrganizationUpgrade() { - return new OrganizationUpgrade + var orgUpgrade = new OrganizationUpgrade { AdditionalSeats = AdditionalSeats, AdditionalStorageGb = AdditionalStorageGb.GetValueOrDefault(), @@ -32,6 +33,10 @@ namespace Bit.Core.Models.Api BillingAddressPostalCode = BillingAddressPostalCode } }; + + Keys.ToOrganizationUpgrade(orgUpgrade); + + return orgUpgrade; } } } diff --git a/src/Core/Models/Api/Response/OrganizationResponseModel.cs b/src/Core/Models/Api/Response/OrganizationResponseModel.cs index 1c65ece17a..fadaae9aaf 100644 --- a/src/Core/Models/Api/Response/OrganizationResponseModel.cs +++ b/src/Core/Models/Api/Response/OrganizationResponseModel.cs @@ -40,8 +40,10 @@ namespace Bit.Core.Models.Api UseTotp = organization.UseTotp; Use2fa = organization.Use2fa; UseApi = organization.UseApi; + UseResetPassword = organization.UseResetPassword; UsersGetPremium = organization.UsersGetPremium; SelfHost = organization.SelfHost; + HasPublicAndPrivateKeys = organization.PublicKey != null && organization.PrivateKey != null; } public string Id { get; set; } @@ -67,8 +69,10 @@ namespace Bit.Core.Models.Api public bool UseTotp { get; set; } public bool Use2fa { get; set; } public bool UseApi { get; set; } + public bool UseResetPassword { get; set; } public bool UsersGetPremium { get; set; } public bool SelfHost { get; set; } + public bool HasPublicAndPrivateKeys { get; set; } } public class OrganizationSubscriptionResponseModel : OrganizationResponseModel diff --git a/src/Core/Models/Api/Response/PlanResponseModel.cs b/src/Core/Models/Api/Response/PlanResponseModel.cs index ba939cafcd..0277a7ea9e 100644 --- a/src/Core/Models/Api/Response/PlanResponseModel.cs +++ b/src/Core/Models/Api/Response/PlanResponseModel.cs @@ -39,6 +39,7 @@ namespace Bit.Core.Models.Api HasTotp = plan.HasTotp; Has2fa = plan.Has2fa; HasSso = plan.HasSso; + HasResetPassword = plan.HasResetPassword; UsersGetPremium = plan.UsersGetPremium; UpgradeSortOrder = plan.UpgradeSortOrder; DisplaySortOrder = plan.DisplaySortOrder; @@ -81,6 +82,7 @@ namespace Bit.Core.Models.Api public bool Has2fa { get; set; } public bool HasApi { get; set; } public bool HasSso { get; set; } + public bool HasResetPassword { get; set; } public bool UsersGetPremium { get; set; } public int UpgradeSortOrder { get; set; } diff --git a/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs b/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs index 949bc0ce4f..c0e6532ed9 100644 --- a/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs +++ b/src/Core/Models/Api/Response/ProfileOrganizationResponseModel.cs @@ -18,12 +18,14 @@ namespace Bit.Core.Models.Api UseTotp = organization.UseTotp; Use2fa = organization.Use2fa; UseApi = organization.UseApi; + UseResetPassword = organization.UseResetPassword; UsersGetPremium = organization.UsersGetPremium; SelfHost = organization.SelfHost; Seats = organization.Seats; MaxCollections = organization.MaxCollections; MaxStorageGb = organization.MaxStorageGb; Key = organization.Key; + HasPublicAndPrivateKeys = organization.PublicKey != null && organization.PrivateKey != null; Status = organization.Status; Type = organization.Type; Enabled = organization.Enabled; @@ -44,6 +46,7 @@ namespace Bit.Core.Models.Api public bool UseTotp { get; set; } public bool Use2fa { get; set; } public bool UseApi { get; set; } + public bool UseResetPassword { get; set; } public bool UseBusinessPortal => UsePolicies || UseSso; // TODO add events if needed public bool UsersGetPremium { get; set; } public bool SelfHost { get; set; } @@ -59,5 +62,6 @@ namespace Bit.Core.Models.Api public Permissions Permissions { get; set; } public bool ResetPasswordEnrolled { get; set; } public string UserId { get; set; } + public bool HasPublicAndPrivateKeys { get; set; } } } diff --git a/src/Core/Models/Business/OrganizationLicense.cs b/src/Core/Models/Business/OrganizationLicense.cs index f425ec540e..8e9dace41e 100644 --- a/src/Core/Models/Business/OrganizationLicense.cs +++ b/src/Core/Models/Business/OrganizationLicense.cs @@ -20,7 +20,7 @@ namespace Bit.Core.Models.Business public OrganizationLicense(Organization org, SubscriptionInfo subscriptionInfo, Guid installationId, ILicensingService licenseService, int? version = null) { - Version = version.GetValueOrDefault(6); // TODO: bump to version 7 + Version = version.GetValueOrDefault(7); // TODO: bump to version 8 LicenseKey = org.LicenseKey; InstallationId = installationId; Id = org.Id; @@ -40,6 +40,7 @@ namespace Bit.Core.Models.Business UseTotp = org.UseTotp; Use2fa = org.Use2fa; UseApi = org.UseApi; + UseResetPassword = org.UseResetPassword; MaxStorageGb = org.MaxStorageGb; SelfHost = org.SelfHost; UsersGetPremium = org.UsersGetPremium; @@ -109,6 +110,7 @@ namespace Bit.Core.Models.Business public bool UseTotp { get; set; } public bool Use2fa { get; set; } public bool UseApi { get; set; } + public bool UseResetPassword { get; set; } public short? MaxStorageGb { get; set; } public bool SelfHost { get; set; } public bool UsersGetPremium { get; set; } @@ -125,7 +127,7 @@ namespace Bit.Core.Models.Business public byte[] GetDataBytes(bool forHash = false) { string data = null; - if (Version >= 1 && Version <= 7) + if (Version >= 1 && Version <= 8) { var props = typeof(OrganizationLicense) .GetProperties(BindingFlags.Public | BindingFlags.Instance) @@ -144,6 +146,8 @@ namespace Bit.Core.Models.Business (Version >= 6 || !p.Name.Equals(nameof(UsePolicies))) && // UseSso was added in Version 7 (Version >= 7 || !p.Name.Equals(nameof(UseSso))) && + // UseResetPassword was added in Version 8 + (Version >= 8 || !p.Name.Equals(nameof(UseResetPassword))) && ( !forHash || ( @@ -180,7 +184,7 @@ namespace Bit.Core.Models.Business return false; } - if (Version >= 1 && Version <= 7) + if (Version >= 1 && Version <= 8) { return InstallationId == globalSettings.Installation.Id && SelfHost; } @@ -197,7 +201,7 @@ namespace Bit.Core.Models.Business return false; } - if (Version >= 1 && Version <= 7) + if (Version >= 1 && Version <= 8) { var valid = globalSettings.Installation.Id == InstallationId && @@ -241,6 +245,11 @@ namespace Bit.Core.Models.Business { valid = organization.UseSso == UseSso; } + + if (valid && Version >= 8) + { + valid = organization.UseResetPassword == UseResetPassword; + } return valid; } diff --git a/src/Core/Models/Business/OrganizationUpgrade.cs b/src/Core/Models/Business/OrganizationUpgrade.cs index 9eed8240f8..06153c50b1 100644 --- a/src/Core/Models/Business/OrganizationUpgrade.cs +++ b/src/Core/Models/Business/OrganizationUpgrade.cs @@ -10,5 +10,7 @@ namespace Bit.Core.Models.Business public short AdditionalStorageGb { get; set; } public bool PremiumAccessAddon { get; set; } public TaxInfo TaxInfo { get; set; } + public string PublicKey { get; set; } + public string PrivateKey { get; set; } } } diff --git a/src/Core/Models/Data/OrganizationAbility.cs b/src/Core/Models/Data/OrganizationAbility.cs index 9f2e1389b1..0433f38805 100644 --- a/src/Core/Models/Data/OrganizationAbility.cs +++ b/src/Core/Models/Data/OrganizationAbility.cs @@ -17,6 +17,7 @@ namespace Bit.Core.Models.Data UsersGetPremium = organization.UsersGetPremium; Enabled = organization.Enabled; UseSso = organization.UseSso; + UseResetPassword = organization.UseResetPassword; } public Guid Id { get; set; } @@ -26,5 +27,6 @@ namespace Bit.Core.Models.Data public bool UsersGetPremium { get; set; } public bool Enabled { get; set; } public bool UseSso { get; set; } + public bool UseResetPassword { get; set; } } } diff --git a/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs b/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs index 8b504f3390..1811be0291 100644 --- a/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs +++ b/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs @@ -15,6 +15,7 @@ namespace Bit.Core.Models.Data public bool UseTotp { get; set; } public bool Use2fa { get; set; } public bool UseApi{ get; set; } + public bool UseResetPassword { get; set; } public bool UseBusinessPortal => UsePolicies || UseSso; public bool SelfHost { get; set; } public bool UsersGetPremium { get; set; } @@ -29,5 +30,7 @@ namespace Bit.Core.Models.Data public string Identifier { get; set; } public string Permissions { get; set; } public string ResetPasswordKey { get; set; } + public string PublicKey { get; set; } + public string PrivateKey { get; set; } } } diff --git a/src/Core/Models/StaticStore/Plan.cs b/src/Core/Models/StaticStore/Plan.cs index 2cba2835fc..2cb38e753d 100644 --- a/src/Core/Models/StaticStore/Plan.cs +++ b/src/Core/Models/StaticStore/Plan.cs @@ -32,6 +32,7 @@ namespace Bit.Core.Models.StaticStore public bool Has2fa { get; set; } public bool HasApi { get; set; } public bool HasSso { get; set; } + public bool HasResetPassword { get; set; } public bool UsersGetPremium { get; set; } public int UpgradeSortOrder { get; set; } diff --git a/src/Core/Models/Table/Organization.cs b/src/Core/Models/Table/Organization.cs index cd0f3546a9..6acdb06db1 100644 --- a/src/Core/Models/Table/Organization.cs +++ b/src/Core/Models/Table/Organization.cs @@ -33,6 +33,7 @@ namespace Bit.Core.Models.Table public bool UseTotp { get; set; } public bool Use2fa { get; set; } public bool UseApi { get; set; } + public bool UseResetPassword { get; set; } public bool SelfHost { get; set; } public bool UsersGetPremium { get; set; } public long? Storage { get; set; } @@ -44,6 +45,8 @@ namespace Bit.Core.Models.Table public bool Enabled { get; set; } = true; public string LicenseKey { get; set; } public string ApiKey { get; set; } + public string PublicKey { get; set; } + public string PrivateKey { get; set; } public string TwoFactorProviders { get; set; } public DateTime? ExpirationDate { get; set; } public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; diff --git a/src/Core/Services/IOrganizationService.cs b/src/Core/Services/IOrganizationService.cs index f51b3a31bf..1381b00146 100644 --- a/src/Core/Services/IOrganizationService.cs +++ b/src/Core/Services/IOrganizationService.cs @@ -20,7 +20,7 @@ namespace Bit.Core.Services Task VerifyBankAsync(Guid organizationId, int amount1, int amount2); Task> SignUpAsync(OrganizationSignup organizationSignup); Task> SignUpAsync(OrganizationLicense license, User owner, - string ownerKey, string collectionName); + string ownerKey, string collectionName, string publicKey, string privateKey); Task UpdateLicenseAsync(Guid organizationId, OrganizationLicense license); Task DeleteAsync(Organization organization); Task EnableAsync(Guid organizationId, DateTime? expirationDate); diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index ea74bd4107..d41c0357b1 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -237,6 +237,8 @@ namespace Bit.Core.Services $"Disable your SSO configuration."); } } + + // TODO Reset Password - Throw error if policy enabled and new pland doesn't allow // TODO: Check storage? @@ -275,10 +277,13 @@ namespace Bit.Core.Services organization.Use2fa = newPlan.Has2fa; organization.UseApi = newPlan.HasApi; organization.UseSso = newPlan.HasSso; + organization.UseResetPassword = newPlan.HasResetPassword; organization.SelfHost = newPlan.HasSelfHost; organization.UsersGetPremium = newPlan.UsersGetPremium || upgrade.PremiumAccessAddon; organization.Plan = newPlan.Name; organization.Enabled = success; + organization.PublicKey = upgrade.PublicKey; + organization.PrivateKey = upgrade.PrivateKey; await ReplaceAndUpdateCache(organization); if (success) { @@ -564,6 +569,7 @@ namespace Bit.Core.Services UseTotp = plan.HasTotp, Use2fa = plan.Has2fa, UseApi = plan.HasApi, + UseResetPassword = plan.HasResetPassword, SelfHost = plan.HasSelfHost, UsersGetPremium = plan.UsersGetPremium || signup.PremiumAccessAddon, Plan = plan.Name, @@ -572,6 +578,8 @@ namespace Bit.Core.Services Enabled = true, LicenseKey = CoreHelpers.SecureRandomString(20), ApiKey = CoreHelpers.SecureRandomString(30), + PublicKey = signup.PublicKey, + PrivateKey = signup.PrivateKey, CreationDate = DateTime.UtcNow, RevisionDate = DateTime.UtcNow, }; @@ -605,7 +613,8 @@ namespace Bit.Core.Services } public async Task> SignUpAsync( - OrganizationLicense license, User owner, string ownerKey, string collectionName) + OrganizationLicense license, User owner, string ownerKey, string collectionName, string publicKey, + string privateKey) { if (license == null || !_licensingService.VerifyLicense(license)) { @@ -647,6 +656,7 @@ namespace Bit.Core.Services UseTotp = license.UseTotp, Use2fa = license.Use2fa, UseApi = license.UseApi, + UseResetPassword = license.UseResetPassword, Plan = license.Plan, SelfHost = license.SelfHost, UsersGetPremium = license.UsersGetPremium, @@ -658,6 +668,8 @@ namespace Bit.Core.Services ExpirationDate = license.Expires, LicenseKey = license.LicenseKey, ApiKey = CoreHelpers.SecureRandomString(30), + PublicKey = publicKey, + PrivateKey = privateKey, CreationDate = DateTime.UtcNow, RevisionDate = DateTime.UtcNow }; @@ -812,6 +824,9 @@ namespace Bit.Core.Services $"Your new license does not allow for the use of SSO. Disable your SSO configuration."); } } + + // TODO Reset Password - If the license does not allow reset password, but the organization currently does + // TODO Reset Password - Pull Reset Password policy and make sure its disabled. var dir = $"{_globalSettings.LicenseDirectory}/organization"; Directory.CreateDirectory(dir); @@ -832,6 +847,7 @@ namespace Bit.Core.Services organization.UseApi = license.UseApi; organization.UsePolicies = license.UsePolicies; organization.UseSso = license.UseSso; + organization.UseResetPassword = license.UseResetPassword; organization.SelfHost = license.SelfHost; organization.UsersGetPremium = license.UsersGetPremium; organization.Plan = license.Plan; diff --git a/src/Core/Utilities/StaticStore.cs b/src/Core/Utilities/StaticStore.cs index cee3360df3..48cb2c2a2c 100644 --- a/src/Core/Utilities/StaticStore.cs +++ b/src/Core/Utilities/StaticStore.cs @@ -392,6 +392,7 @@ namespace Bit.Core.Utilities Has2fa = true, HasApi = true, HasSso = true, + HasResetPassword = true, UsersGetPremium = true, UpgradeSortOrder = 3, @@ -427,6 +428,7 @@ namespace Bit.Core.Utilities HasApi = true, HasSelfHost = true, HasSso = true, + HasResetPassword = true, UsersGetPremium = true, UpgradeSortOrder = 3, diff --git a/src/Sql/dbo/Stored Procedures/Organization_Create.sql b/src/Sql/dbo/Stored Procedures/Organization_Create.sql index af8bf56206..95cd0438c3 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_Create.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_Create.sql @@ -21,6 +21,7 @@ @UseTotp BIT, @Use2fa BIT, @UseApi BIT, + @UseResetPassword BIT, @SelfHost BIT, @UsersGetPremium BIT, @Storage BIGINT, @@ -32,6 +33,8 @@ @Enabled BIT, @LicenseKey VARCHAR(100), @ApiKey VARCHAR(30), + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX), @TwoFactorProviders NVARCHAR(MAX), @ExpirationDate DATETIME2(7), @CreationDate DATETIME2(7), @@ -64,6 +67,7 @@ BEGIN [UseTotp], [Use2fa], [UseApi], + [UseResetPassword], [SelfHost], [UsersGetPremium], [Storage], @@ -75,6 +79,8 @@ BEGIN [Enabled], [LicenseKey], [ApiKey], + [PublicKey], + [PrivateKey], [TwoFactorProviders], [ExpirationDate], [CreationDate], @@ -104,6 +110,7 @@ BEGIN @UseTotp, @Use2fa, @UseApi, + @UseResetPassword, @SelfHost, @UsersGetPremium, @Storage, @@ -115,6 +122,8 @@ BEGIN @Enabled, @LicenseKey, @ApiKey, + @PublicKey, + @PrivateKey, @TwoFactorProviders, @ExpirationDate, @CreationDate, diff --git a/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql b/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql index a19c639266..eb044ed998 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_ReadAbilities.sql @@ -15,6 +15,7 @@ BEGIN END AS [Using2fa], [UsersGetPremium], [UseSso], + [UseResetPassword], [Enabled] FROM [dbo].[Organization] diff --git a/src/Sql/dbo/Stored Procedures/Organization_Update.sql b/src/Sql/dbo/Stored Procedures/Organization_Update.sql index 668135128b..45ae43f381 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_Update.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_Update.sql @@ -21,6 +21,7 @@ @UseTotp BIT, @Use2fa BIT, @UseApi BIT, + @UseResetPassword BIT, @SelfHost BIT, @UsersGetPremium BIT, @Storage BIGINT, @@ -32,6 +33,8 @@ @Enabled BIT, @LicenseKey VARCHAR(100), @ApiKey VARCHAR(30), + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX), @TwoFactorProviders NVARCHAR(MAX), @ExpirationDate DATETIME2(7), @CreationDate DATETIME2(7), @@ -64,6 +67,7 @@ BEGIN [UseTotp] = @UseTotp, [Use2fa] = @Use2fa, [UseApi] = @UseApi, + [UseResetPassword] = @UseResetPassword, [SelfHost] = @SelfHost, [UsersGetPremium] = @UsersGetPremium, [Storage] = @Storage, @@ -75,6 +79,8 @@ BEGIN [Enabled] = @Enabled, [LicenseKey] = @LicenseKey, [ApiKey] = @ApiKey, + [PublicKey] = @PublicKey, + [PrivateKey] = @PrivateKey, [TwoFactorProviders] = @TwoFactorProviders, [ExpirationDate] = @ExpirationDate, [CreationDate] = @CreationDate, diff --git a/src/Sql/dbo/Tables/Organization.sql b/src/Sql/dbo/Tables/Organization.sql index f4da5aa66a..1b795aebb7 100644 --- a/src/Sql/dbo/Tables/Organization.sql +++ b/src/Sql/dbo/Tables/Organization.sql @@ -21,6 +21,7 @@ [UseTotp] BIT NOT NULL, [Use2fa] BIT NOT NULL, [UseApi] BIT NOT NULL, + [UseResetPassword] BIT NOT NULL, [SelfHost] BIT NOT NULL, [UsersGetPremium] BIT NOT NULL, [Storage] BIGINT NULL, @@ -32,6 +33,8 @@ [Enabled] BIT NOT NULL, [LicenseKey] VARCHAR (100) NULL, [ApiKey] VARCHAR (30) NOT NULL, + [PublicKey] VARCHAR (MAX) NULL, + [PrivateKey] VARCHAR (MAX) NULL, [TwoFactorProviders] NVARCHAR (MAX) NULL, [ExpirationDate] DATETIME2 (7) NULL, [CreationDate] DATETIME2 (7) NOT NULL, diff --git a/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql b/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql index fe7b547f04..e4354fb5d4 100644 --- a/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql +++ b/src/Sql/dbo/Views/OrganizationUserOrganizationDetailsView.sql @@ -13,6 +13,7 @@ SELECT O.[UseTotp], O.[Use2fa], O.[UseApi], + O.[UseResetPassword], O.[SelfHost], O.[UsersGetPremium], O.[Seats], @@ -21,6 +22,8 @@ SELECT O.[Identifier], OU.[Key], OU.[ResetPasswordKey], + O.[PublicKey], + O.[PrivateKey], OU.[Status], OU.[Type], SU.[ExternalId] SsoExternalId, diff --git a/test/Core.Test/AutoFixture/OrganizationFixtures.cs b/test/Core.Test/AutoFixture/OrganizationFixtures.cs index a2c4508134..f3723f895a 100644 --- a/test/Core.Test/AutoFixture/OrganizationFixtures.cs +++ b/test/Core.Test/AutoFixture/OrganizationFixtures.cs @@ -36,9 +36,11 @@ namespace Bit.Core.Test.AutoFixture.OrganizationFixtures .With(o => o.PlanType, PlanType.Free)); var plansToIgnore = new List { PlanType.Free, PlanType.Custom }; - var validPlans = StaticStore.Plans.Where(p => !plansToIgnore.Contains(p.Type) && !p.Disabled).Select(p => p.Type).ToList(); + var selectedPlan = StaticStore.Plans.Last(p => !plansToIgnore.Contains(p.Type) && !p.Disabled); + fixture.Customize(composer => composer - .With(ou => ou.Plan, validPlans.Last())); + .With(ou => ou.Plan, selectedPlan.Type) + .With(ou => ou.PremiumAccessAddon, selectedPlan.HasPremiumAccessOption)); fixture.Customize(composer => composer .Without(o => o.GatewaySubscriptionId)); } diff --git a/util/Migrator/DbScripts/2021-04-28_00_OrgResetPasswordAbilityAndRsaKeys.sql b/util/Migrator/DbScripts/2021-04-28_00_OrgResetPasswordAbilityAndRsaKeys.sql new file mode 100644 index 0000000000..c0a1cecb91 --- /dev/null +++ b/util/Migrator/DbScripts/2021-04-28_00_OrgResetPasswordAbilityAndRsaKeys.sql @@ -0,0 +1,372 @@ +-- Table: Organization (UseResetPassword) +IF COL_LENGTH('[dbo].[Organization]', 'UseResetPassword') IS NULL +BEGIN + ALTER TABLE + [dbo].[Organization] + ADD + [UseResetPassword] BIT NULL +END +GO + +UPDATE + [dbo].[Organization] +SET + [UseResetPassword] = (CASE WHEN [PlanType] = 10 OR [PlanType] = 11 THEN 1 ELSE 0 END) +GO + +ALTER TABLE + [dbo].[Organization] +ALTER COLUMN + [UseResetPassword] BIT NOT NULL +GO + +-- Table: Organization (PublicKey) +IF COL_LENGTH('[dbo].[Organization]', 'PublicKey') IS NULL +BEGIN + ALTER TABLE + [dbo].[Organization] + ADD + [PublicKey] VARCHAR (MAX) NULL +END +GO + +-- Table: Organization (PrivateKey) +IF COL_LENGTH('[dbo].[Organization]', 'PrivateKey') IS NULL +BEGIN + ALTER TABLE + [dbo].[Organization] + ADD + [PrivateKey] VARCHAR (MAX) NULL +END +GO + +-- View: Organization +IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'OrganizationView') +BEGIN + DROP VIEW [dbo].[OrganizationView] +END +GO + +CREATE VIEW [dbo].[OrganizationView] +AS +SELECT + * +FROM + [dbo].[Organization] +GO + +-- View: OrganizationUserOrganizationDetails +IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'OrganizationUserOrganizationDetailsView') +BEGIN + DROP VIEW [dbo].[OrganizationUserOrganizationDetailsView] +END +GO + +CREATE VIEW [dbo].[OrganizationUserOrganizationDetailsView] +AS +SELECT + OU.[UserId], + OU.[OrganizationId], + O.[Name], + O.[Enabled], + O.[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] +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] +GO + +-- Stored Procedure: Organization_Create +IF OBJECT_ID('[dbo].[Organization_Create]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Organization_Create] +END +GO + +CREATE PROCEDURE [dbo].[Organization_Create] + @Id UNIQUEIDENTIFIER, + @Identifier NVARCHAR(50), + @Name NVARCHAR(50), + @BusinessName NVARCHAR(50), + @BusinessAddress1 NVARCHAR(50), + @BusinessAddress2 NVARCHAR(50), + @BusinessAddress3 NVARCHAR(50), + @BusinessCountry VARCHAR(2), + @BusinessTaxNumber NVARCHAR(30), + @BillingEmail NVARCHAR(256), + @Plan NVARCHAR(50), + @PlanType TINYINT, + @Seats SMALLINT, + @MaxCollections SMALLINT, + @UsePolicies BIT, + @UseSso BIT, + @UseGroups BIT, + @UseDirectory BIT, + @UseEvents BIT, + @UseTotp BIT, + @Use2fa BIT, + @UseApi BIT, + @UseResetPassword BIT, + @SelfHost BIT, + @UsersGetPremium BIT, + @Storage BIGINT, + @MaxStorageGb SMALLINT, + @Gateway TINYINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ReferenceData VARCHAR(MAX), + @Enabled BIT, + @LicenseKey VARCHAR(100), + @ApiKey VARCHAR(30), + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX), + @TwoFactorProviders NVARCHAR(MAX), + @ExpirationDate DATETIME2(7), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[Organization] + ( + [Id], + [Identifier], + [Name], + [BusinessName], + [BusinessAddress1], + [BusinessAddress2], + [BusinessAddress3], + [BusinessCountry], + [BusinessTaxNumber], + [BillingEmail], + [Plan], + [PlanType], + [Seats], + [MaxCollections], + [UsePolicies], + [UseSso], + [UseGroups], + [UseDirectory], + [UseEvents], + [UseTotp], + [Use2fa], + [UseApi], + [UseResetPassword], + [SelfHost], + [UsersGetPremium], + [Storage], + [MaxStorageGb], + [Gateway], + [GatewayCustomerId], + [GatewaySubscriptionId], + [ReferenceData], + [Enabled], + [LicenseKey], + [ApiKey], + [PublicKey], + [PrivateKey], + [TwoFactorProviders], + [ExpirationDate], + [CreationDate], + [RevisionDate] + ) + VALUES + ( + @Id, + @Identifier, + @Name, + @BusinessName, + @BusinessAddress1, + @BusinessAddress2, + @BusinessAddress3, + @BusinessCountry, + @BusinessTaxNumber, + @BillingEmail, + @Plan, + @PlanType, + @Seats, + @MaxCollections, + @UsePolicies, + @UseSso, + @UseGroups, + @UseDirectory, + @UseEvents, + @UseTotp, + @Use2fa, + @UseApi, + @UseResetPassword, + @SelfHost, + @UsersGetPremium, + @Storage, + @MaxStorageGb, + @Gateway, + @GatewayCustomerId, + @GatewaySubscriptionId, + @ReferenceData, + @Enabled, + @LicenseKey, + @ApiKey, + @PublicKey, + @PrivateKey, + @TwoFactorProviders, + @ExpirationDate, + @CreationDate, + @RevisionDate + ) +END +GO + +-- Stored Procedure: Organization_Update +IF OBJECT_ID('[dbo].[Organization_Update]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Organization_Update] +END +GO + +CREATE PROCEDURE [dbo].[Organization_Update] + @Id UNIQUEIDENTIFIER, + @Identifier NVARCHAR(50), + @Name NVARCHAR(50), + @BusinessName NVARCHAR(50), + @BusinessAddress1 NVARCHAR(50), + @BusinessAddress2 NVARCHAR(50), + @BusinessAddress3 NVARCHAR(50), + @BusinessCountry VARCHAR(2), + @BusinessTaxNumber NVARCHAR(30), + @BillingEmail NVARCHAR(256), + @Plan NVARCHAR(50), + @PlanType TINYINT, + @Seats SMALLINT, + @MaxCollections SMALLINT, + @UsePolicies BIT, + @UseSso BIT, + @UseGroups BIT, + @UseDirectory BIT, + @UseEvents BIT, + @UseTotp BIT, + @Use2fa BIT, + @UseApi BIT, + @UseResetPassword BIT, + @SelfHost BIT, + @UsersGetPremium BIT, + @Storage BIGINT, + @MaxStorageGb SMALLINT, + @Gateway TINYINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ReferenceData VARCHAR(MAX), + @Enabled BIT, + @LicenseKey VARCHAR(100), + @ApiKey VARCHAR(30), + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX), + @TwoFactorProviders NVARCHAR(MAX), + @ExpirationDate DATETIME2(7), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[Organization] + SET + [Identifier] = @Identifier, + [Name] = @Name, + [BusinessName] = @BusinessName, + [BusinessAddress1] = @BusinessAddress1, + [BusinessAddress2] = @BusinessAddress2, + [BusinessAddress3] = @BusinessAddress3, + [BusinessCountry] = @BusinessCountry, + [BusinessTaxNumber] = @BusinessTaxNumber, + [BillingEmail] = @BillingEmail, + [Plan] = @Plan, + [PlanType] = @PlanType, + [Seats] = @Seats, + [MaxCollections] = @MaxCollections, + [UsePolicies] = @UsePolicies, + [UseSso] = @UseSso, + [UseGroups] = @UseGroups, + [UseDirectory] = @UseDirectory, + [UseEvents] = @UseEvents, + [UseTotp] = @UseTotp, + [Use2fa] = @Use2fa, + [UseApi] = @UseApi, + [UseResetPassword] = @UseResetPassword, + [SelfHost] = @SelfHost, + [UsersGetPremium] = @UsersGetPremium, + [Storage] = @Storage, + [MaxStorageGb] = @MaxStorageGb, + [Gateway] = @Gateway, + [GatewayCustomerId] = @GatewayCustomerId, + [GatewaySubscriptionId] = @GatewaySubscriptionId, + [ReferenceData] = @ReferenceData, + [Enabled] = @Enabled, + [LicenseKey] = @LicenseKey, + [ApiKey] = @ApiKey, + [PublicKey] = @PublicKey, + [PrivateKey] = @PrivateKey, + [TwoFactorProviders] = @TwoFactorProviders, + [ExpirationDate] = @ExpirationDate, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate + WHERE + [Id] = @Id +END +GO + +-- Stored Procedure: Organization_ReadAbilities +IF OBJECT_ID('[dbo].[Organization_ReadAbilities]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Organization_ReadAbilities] +END +GO + +CREATE PROCEDURE [dbo].[Organization_ReadAbilities] +AS +BEGIN + SET NOCOUNT ON + + SELECT + [Id], + [UseEvents], + [Use2fa], + CASE + WHEN [Use2fa] = 1 AND [TwoFactorProviders] IS NOT NULL AND [TwoFactorProviders] != '{}' THEN + 1 + ELSE + 0 + END AS [Using2fa], + [UsersGetPremium], + [UseSso], + [UseResetPassword], + [Enabled] + FROM + [dbo].[Organization] +END +GO \ No newline at end of file