From 7ebf312b844a637a0066bd9f926f2533b6b33d63 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Wed, 30 Apr 2025 07:19:28 -0400 Subject: [PATCH 001/114] Updated seat count logic to ensure that only the correct sponsorships are counted towards the seat count (#5711) --- ...dOccupiedSeatCountByOrganizationIdQuery.cs | 14 ++++++- ..._ReadOccupiedSeatCountByOrganizationId.sql | 14 +++++++ ...erReadOccupiedSeatCountForSponsorships.sql | 41 +++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 util/Migrator/DbScripts/2025-04-24_00_UpdateOrgUserReadOccupiedSeatCountForSponsorships.sql diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery.cs index 0a681e2b5f..6be51f2036 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery.cs @@ -23,7 +23,19 @@ public class OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery : IQuery var sponsorshipsQuery = from os in dbContext.OrganizationSponsorships where os.SponsoringOrganizationId == _organizationId && os.IsAdminInitiated && - !os.ToDelete + ( + // Not marked for deletion - always count + (!os.ToDelete) || + // Marked for deletion but has a valid until date in the future (RevokeWhenExpired status) + (os.ToDelete && os.ValidUntil.HasValue && os.ValidUntil.Value > DateTime.UtcNow) + ) && + ( + // SENT status: When SponsoredOrganizationId is null + os.SponsoredOrganizationId == null || + // ACCEPTED status: When SponsoredOrganizationId is not null and ValidUntil is null or in the future + (os.SponsoredOrganizationId != null && + (!os.ValidUntil.HasValue || os.ValidUntil.Value > DateTime.UtcNow)) + ) select new OrganizationUser { Id = os.Id, diff --git a/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadOccupiedSeatCountByOrganizationId.sql b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadOccupiedSeatCountByOrganizationId.sql index 933441a210..3d861670a6 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadOccupiedSeatCountByOrganizationId.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationUser_ReadOccupiedSeatCountByOrganizationId.sql @@ -19,5 +19,19 @@ BEGIN FROM [dbo].[OrganizationSponsorship] WHERE SponsoringOrganizationId = @OrganizationId AND IsAdminInitiated = 1 + AND ( + -- Not marked for deletion - always count + (ToDelete = 0) + OR + -- Marked for deletion but has a valid until date in the future (RevokeWhenExpired status) + (ToDelete = 1 AND ValidUntil IS NOT NULL AND ValidUntil > GETUTCDATE()) + ) + AND ( + -- SENT status: When SponsoredOrganizationId is null + SponsoredOrganizationId IS NULL + OR + -- ACCEPTED status: When SponsoredOrganizationId is not null and ValidUntil is null or in the future + (SponsoredOrganizationId IS NOT NULL AND (ValidUntil IS NULL OR ValidUntil > GETUTCDATE())) + ) ) END diff --git a/util/Migrator/DbScripts/2025-04-24_00_UpdateOrgUserReadOccupiedSeatCountForSponsorships.sql b/util/Migrator/DbScripts/2025-04-24_00_UpdateOrgUserReadOccupiedSeatCountForSponsorships.sql new file mode 100644 index 0000000000..7260c9c6d4 --- /dev/null +++ b/util/Migrator/DbScripts/2025-04-24_00_UpdateOrgUserReadOccupiedSeatCountForSponsorships.sql @@ -0,0 +1,41 @@ +IF OBJECT_ID('[dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + ( + SELECT COUNT(1) + FROM [dbo].[OrganizationUserView] + WHERE OrganizationId = @OrganizationId + AND Status >= 0 --Invited + ) + + ( + SELECT COUNT(1) + FROM [dbo].[OrganizationSponsorship] + WHERE SponsoringOrganizationId = @OrganizationId + AND IsAdminInitiated = 1 + AND ( + -- Not marked for deletion - always count + (ToDelete = 0) + OR + -- Marked for deletion but has a valid until date in the future (RevokeWhenExpired status) + (ToDelete = 1 AND ValidUntil IS NOT NULL AND ValidUntil > GETUTCDATE()) + ) + AND ( + -- SENT status: When SponsoredOrganizationId is null + SponsoredOrganizationId IS NULL + OR + -- ACCEPTED status: When SponsoredOrganizationId is not null and ValidUntil is null or in the future + (SponsoredOrganizationId IS NOT NULL AND (ValidUntil IS NULL OR ValidUntil > GETUTCDATE())) + ) + ) +END +GO From cf7a59c0772985086eea847cfd092f771a1aba45 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Wed, 30 Apr 2025 11:03:59 -0400 Subject: [PATCH 002/114] [Innovation Sprint] Phishing Detection (#5516) * Initial stubbing out of the phishing service * Add the phishing domain controller * Add changes for the phishing domain get * Add distributed cache to the phishing domain Signed-off-by: Cy Okeke * Rename the variable name Signed-off-by: Cy Okeke * Removed IPhishingDomainService * Feature/phishing detection cronjob (#5512) * Added caching to EF implementation. Added error handling and logging * Refactored update method to use sqlbulkcopy instead of performing a round trip for each new insert * Initial implementation for quartz job to get list of phishing domains * Updated phishing domain settings to be its own interface * Add phishing domain detection with checksum-based updates * Updated auth for phishing domain endpoints to either require api, or licensing claims to support both web and browser clients, and selfhost api clients * [Innovation Sprint] Updated Phishing domains to rely on blob storage (#5517) * Updated phishing detection data layer to rely on azure blob storage instead of sql server * dotnet format * Took rider refactors * Ensuring phishing.testcategory.com exists to test against * Added redis to dev's docker-compose * Removed redis from cloud profile * Remove the Authorize attribute * error whitespace fix whitespace formatting * error WHITESPACE: Fix whitespace formatting * Wrapped phishing detection feature behind feature flag (#5532) * Increased timeout for fetching source list a bunch * Removed PhishingDomains policy --------- Signed-off-by: Cy Okeke Co-authored-by: Cy Okeke --- dev/docker-compose.yml | 12 ++ .../Controllers/PhishingDomainsController.cs | 34 +++++ src/Api/Jobs/JobsHostedService.cs | 9 ++ src/Api/Jobs/UpdatePhishingDomainsJob.cs | 97 ++++++++++++++ src/Api/Startup.cs | 1 + .../Utilities/ServiceCollectionExtensions.cs | 25 ++++ src/Api/appsettings.Development.json | 4 + src/Api/appsettings.json | 3 + src/Core/Constants.cs | 1 + .../AzurePhishingDomainStorageService.cs | 92 +++++++++++++ .../CloudPhishingDomainDirectQuery.cs | 100 ++++++++++++++ .../CloudPhishingDomainRelayQuery.cs | 66 +++++++++ .../Interfaces/ICloudPhishingDomainQuery.cs | 7 + .../Repositories/IPhishingDomainRepository.cs | 8 ++ .../AzurePhishingDomainRepository.cs | 126 ++++++++++++++++++ src/Core/Settings/GlobalSettings.cs | 7 + src/Core/Settings/IGlobalSettings.cs | 1 + src/Core/Settings/IPhishingDomainSettings.cs | 7 + 18 files changed, 600 insertions(+) create mode 100644 src/Api/Controllers/PhishingDomainsController.cs create mode 100644 src/Api/Jobs/UpdatePhishingDomainsJob.cs create mode 100644 src/Core/PhishingDomainFeatures/AzurePhishingDomainStorageService.cs create mode 100644 src/Core/PhishingDomainFeatures/CloudPhishingDomainDirectQuery.cs create mode 100644 src/Core/PhishingDomainFeatures/CloudPhishingDomainRelayQuery.cs create mode 100644 src/Core/PhishingDomainFeatures/Interfaces/ICloudPhishingDomainQuery.cs create mode 100644 src/Core/Repositories/IPhishingDomainRepository.cs create mode 100644 src/Core/Repositories/Implementations/AzurePhishingDomainRepository.cs create mode 100644 src/Core/Settings/IPhishingDomainSettings.cs diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index 1bfbe0a9d7..a21f1ac6b8 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -124,8 +124,20 @@ services: profiles: - servicebus + redis: + image: redis:alpine + container_name: bw-redis + ports: + - "6379:6379" + volumes: + - redis_data:/data + command: redis-server --appendonly yes + profiles: + - redis + volumes: mssql_dev_data: postgres_dev_data: mysql_dev_data: rabbitmq_data: + redis_data: diff --git a/src/Api/Controllers/PhishingDomainsController.cs b/src/Api/Controllers/PhishingDomainsController.cs new file mode 100644 index 0000000000..f0c1a65648 --- /dev/null +++ b/src/Api/Controllers/PhishingDomainsController.cs @@ -0,0 +1,34 @@ +using Bit.Core; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.Controllers; + +[Route("phishing-domains")] +public class PhishingDomainsController(IPhishingDomainRepository phishingDomainRepository, IFeatureService featureService) : Controller +{ + [HttpGet] + public async Task>> GetPhishingDomainsAsync() + { + if (!featureService.IsEnabled(FeatureFlagKeys.PhishingDetection)) + { + return NotFound(); + } + + var domains = await phishingDomainRepository.GetActivePhishingDomainsAsync(); + return Ok(domains); + } + + [HttpGet("checksum")] + public async Task> GetChecksumAsync() + { + if (!featureService.IsEnabled(FeatureFlagKeys.PhishingDetection)) + { + return NotFound(); + } + + var checksum = await phishingDomainRepository.GetCurrentChecksumAsync(); + return Ok(checksum); + } +} diff --git a/src/Api/Jobs/JobsHostedService.cs b/src/Api/Jobs/JobsHostedService.cs index acd95a0213..57b827a8be 100644 --- a/src/Api/Jobs/JobsHostedService.cs +++ b/src/Api/Jobs/JobsHostedService.cs @@ -58,6 +58,13 @@ public class JobsHostedService : BaseJobsHostedService .StartNow() .WithCronSchedule("0 0 * * * ?") .Build(); + var updatePhishingDomainsTrigger = TriggerBuilder.Create() + .WithIdentity("UpdatePhishingDomainsTrigger") + .StartNow() + .WithSimpleSchedule(x => x + .WithIntervalInHours(24) + .RepeatForever()) + .Build(); var jobs = new List> @@ -68,6 +75,7 @@ public class JobsHostedService : BaseJobsHostedService new Tuple(typeof(ValidateUsersJob), everyTopOfTheSixthHourTrigger), new Tuple(typeof(ValidateOrganizationsJob), everyTwelfthHourAndThirtyMinutesTrigger), new Tuple(typeof(ValidateOrganizationDomainJob), validateOrganizationDomainTrigger), + new Tuple(typeof(UpdatePhishingDomainsJob), updatePhishingDomainsTrigger), }; if (_globalSettings.SelfHosted && _globalSettings.EnableCloudCommunication) @@ -96,6 +104,7 @@ public class JobsHostedService : BaseJobsHostedService services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); } public static void AddCommercialSecretsManagerJobServices(IServiceCollection services) diff --git a/src/Api/Jobs/UpdatePhishingDomainsJob.cs b/src/Api/Jobs/UpdatePhishingDomainsJob.cs new file mode 100644 index 0000000000..355f2af69b --- /dev/null +++ b/src/Api/Jobs/UpdatePhishingDomainsJob.cs @@ -0,0 +1,97 @@ +using Bit.Core; +using Bit.Core.Jobs; +using Bit.Core.PhishingDomainFeatures.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Settings; +using Quartz; + +namespace Bit.Api.Jobs; + +public class UpdatePhishingDomainsJob : BaseJob +{ + private readonly GlobalSettings _globalSettings; + private readonly IPhishingDomainRepository _phishingDomainRepository; + private readonly ICloudPhishingDomainQuery _cloudPhishingDomainQuery; + private readonly IFeatureService _featureService; + public UpdatePhishingDomainsJob( + GlobalSettings globalSettings, + IPhishingDomainRepository phishingDomainRepository, + ICloudPhishingDomainQuery cloudPhishingDomainQuery, + IFeatureService featureService, + ILogger logger) + : base(logger) + { + _globalSettings = globalSettings; + _phishingDomainRepository = phishingDomainRepository; + _cloudPhishingDomainQuery = cloudPhishingDomainQuery; + _featureService = featureService; + } + + protected override async Task ExecuteJobAsync(IJobExecutionContext context) + { + if (!_featureService.IsEnabled(FeatureFlagKeys.PhishingDetection)) + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Skipping phishing domain update. Feature flag is disabled."); + return; + } + + if (string.IsNullOrWhiteSpace(_globalSettings.PhishingDomain?.UpdateUrl)) + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Skipping phishing domain update. No URL configured."); + return; + } + + if (_globalSettings.SelfHosted && !_globalSettings.EnableCloudCommunication) + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Skipping phishing domain update. Cloud communication is disabled in global settings."); + return; + } + + var remoteChecksum = await _cloudPhishingDomainQuery.GetRemoteChecksumAsync(); + if (string.IsNullOrWhiteSpace(remoteChecksum)) + { + _logger.LogWarning(Constants.BypassFiltersEventId, "Could not retrieve remote checksum. Skipping update."); + return; + } + + var currentChecksum = await _phishingDomainRepository.GetCurrentChecksumAsync(); + + if (string.Equals(currentChecksum, remoteChecksum, StringComparison.OrdinalIgnoreCase)) + { + _logger.LogInformation(Constants.BypassFiltersEventId, + "Phishing domains list is up to date (checksum: {Checksum}). Skipping update.", + currentChecksum); + return; + } + + _logger.LogInformation(Constants.BypassFiltersEventId, + "Checksums differ (current: {CurrentChecksum}, remote: {RemoteChecksum}). Fetching updated domains from {Source}.", + currentChecksum, remoteChecksum, _globalSettings.SelfHosted ? "Bitwarden cloud API" : "external source"); + + try + { + var domains = await _cloudPhishingDomainQuery.GetPhishingDomainsAsync(); + if (!domains.Contains("phishing.testcategory.com", StringComparer.OrdinalIgnoreCase)) + { + domains.Add("phishing.testcategory.com"); + } + + if (domains.Count > 0) + { + _logger.LogInformation(Constants.BypassFiltersEventId, "Updating {Count} phishing domains with checksum {Checksum}.", + domains.Count, remoteChecksum); + await _phishingDomainRepository.UpdatePhishingDomainsAsync(domains, remoteChecksum); + _logger.LogInformation(Constants.BypassFiltersEventId, "Successfully updated phishing domains."); + } + else + { + _logger.LogWarning(Constants.BypassFiltersEventId, "No valid domains found in the response. Skipping update."); + } + } + catch (Exception ex) + { + _logger.LogError(Constants.BypassFiltersEventId, ex, "Error updating phishing domains."); + } + } +} diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 2872a5b88b..40448f722d 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -182,6 +182,7 @@ public class Startup services.AddBillingOperations(); services.AddReportingServices(); services.AddImportServices(); + services.AddPhishingDomainServices(globalSettings); // Authorization Handlers services.AddAuthorizationHandlers(); diff --git a/src/Api/Utilities/ServiceCollectionExtensions.cs b/src/Api/Utilities/ServiceCollectionExtensions.cs index 4c8589657e..e6a20fe364 100644 --- a/src/Api/Utilities/ServiceCollectionExtensions.cs +++ b/src/Api/Utilities/ServiceCollectionExtensions.cs @@ -3,6 +3,10 @@ using Bit.Api.Tools.Authorization; using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization; using Bit.Core.IdentityServer; +using Bit.Core.PhishingDomainFeatures; +using Bit.Core.PhishingDomainFeatures.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Repositories.Implementations; using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.Core.Vault.Authorization.SecurityTasks; @@ -109,4 +113,25 @@ public static class ServiceCollectionExtensions services.AddScoped(); } + + public static void AddPhishingDomainServices(this IServiceCollection services, GlobalSettings globalSettings) + { + services.AddHttpClient("PhishingDomains", client => + { + client.DefaultRequestHeaders.Add("User-Agent", globalSettings.SelfHosted ? "Bitwarden Self-Hosted" : "Bitwarden"); + client.Timeout = TimeSpan.FromSeconds(1000); // the source list is very slow + }); + + services.AddSingleton(); + services.AddSingleton(); + + if (globalSettings.SelfHosted) + { + services.AddScoped(); + } + else + { + services.AddScoped(); + } + } } diff --git a/src/Api/appsettings.Development.json b/src/Api/appsettings.Development.json index 2f33d87ae8..82fb951261 100644 --- a/src/Api/appsettings.Development.json +++ b/src/Api/appsettings.Development.json @@ -37,6 +37,10 @@ }, "storage": { "connectionString": "UseDevelopmentStorage=true" + }, + "phishingDomain": { + "updateUrl": "https://phish.co.za/latest/phishing-domains-ACTIVE.txt", + "checksumUrl": "https://raw.githubusercontent.com/Phishing-Database/checksums/refs/heads/master/phishing-domains-ACTIVE.txt.sha256" } } } diff --git a/src/Api/appsettings.json b/src/Api/appsettings.json index 98b210cb1e..f8a69dcfac 100644 --- a/src/Api/appsettings.json +++ b/src/Api/appsettings.json @@ -71,6 +71,9 @@ "accessKeySecret": "SECRET", "region": "SECRET" }, + "phishingDomain": { + "updateUrl": "SECRET" + }, "distributedIpRateLimiting": { "enabled": true, "maxRedisTimeoutsThreshold": 10, diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 5f0265df41..a05b89a94f 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -203,6 +203,7 @@ public static class FeatureFlagKeys public const string DesktopCipherForms = "pm-18520-desktop-cipher-forms"; public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk"; public const string EndUserNotifications = "pm-10609-end-user-notifications"; + public const string PhishingDetection = "phishing-detection"; public static List GetAllKeys() { diff --git a/src/Core/PhishingDomainFeatures/AzurePhishingDomainStorageService.cs b/src/Core/PhishingDomainFeatures/AzurePhishingDomainStorageService.cs new file mode 100644 index 0000000000..9af9c94e1d --- /dev/null +++ b/src/Core/PhishingDomainFeatures/AzurePhishingDomainStorageService.cs @@ -0,0 +1,92 @@ +using System.Text; +using Azure.Storage.Blobs; +using Azure.Storage.Blobs.Models; +using Bit.Core.Settings; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.PhishingDomainFeatures; + +public class AzurePhishingDomainStorageService +{ + private const string _containerName = "phishingdomains"; + private const string _domainsFileName = "domains.txt"; + private const string _checksumFileName = "checksum.txt"; + + private readonly BlobServiceClient _blobServiceClient; + private readonly ILogger _logger; + private BlobContainerClient _containerClient; + + public AzurePhishingDomainStorageService( + GlobalSettings globalSettings, + ILogger logger) + { + _blobServiceClient = new BlobServiceClient(globalSettings.Storage.ConnectionString); + _logger = logger; + } + + public async Task> GetDomainsAsync() + { + await InitAsync(); + + var blobClient = _containerClient.GetBlobClient(_domainsFileName); + if (!await blobClient.ExistsAsync()) + { + return []; + } + + var response = await blobClient.DownloadAsync(); + using var streamReader = new StreamReader(response.Value.Content); + var content = await streamReader.ReadToEndAsync(); + + return [.. content + .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Trim()) + .Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith('#'))]; + } + + public async Task GetChecksumAsync() + { + await InitAsync(); + + var blobClient = _containerClient.GetBlobClient(_checksumFileName); + if (!await blobClient.ExistsAsync()) + { + return string.Empty; + } + + var response = await blobClient.DownloadAsync(); + using var streamReader = new StreamReader(response.Value.Content); + return (await streamReader.ReadToEndAsync()).Trim(); + } + + public async Task UpdateDomainsAsync(IEnumerable domains, string checksum) + { + await InitAsync(); + + var domainsContent = string.Join(Environment.NewLine, domains); + var domainsStream = new MemoryStream(Encoding.UTF8.GetBytes(domainsContent)); + var domainsBlobClient = _containerClient.GetBlobClient(_domainsFileName); + + await domainsBlobClient.UploadAsync(domainsStream, new BlobUploadOptions + { + HttpHeaders = new BlobHttpHeaders { ContentType = "text/plain" } + }, CancellationToken.None); + + var checksumStream = new MemoryStream(Encoding.UTF8.GetBytes(checksum)); + var checksumBlobClient = _containerClient.GetBlobClient(_checksumFileName); + + await checksumBlobClient.UploadAsync(checksumStream, new BlobUploadOptions + { + HttpHeaders = new BlobHttpHeaders { ContentType = "text/plain" } + }, CancellationToken.None); + } + + private async Task InitAsync() + { + if (_containerClient is null) + { + _containerClient = _blobServiceClient.GetBlobContainerClient(_containerName); + await _containerClient.CreateIfNotExistsAsync(); + } + } +} diff --git a/src/Core/PhishingDomainFeatures/CloudPhishingDomainDirectQuery.cs b/src/Core/PhishingDomainFeatures/CloudPhishingDomainDirectQuery.cs new file mode 100644 index 0000000000..b059eac0e8 --- /dev/null +++ b/src/Core/PhishingDomainFeatures/CloudPhishingDomainDirectQuery.cs @@ -0,0 +1,100 @@ +using Bit.Core.PhishingDomainFeatures.Interfaces; +using Bit.Core.Settings; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.PhishingDomainFeatures; + +/// +/// Implementation of ICloudPhishingDomainQuery for cloud environments +/// that directly calls the external phishing domain source +/// +public class CloudPhishingDomainDirectQuery : ICloudPhishingDomainQuery +{ + private readonly IGlobalSettings _globalSettings; + private readonly IHttpClientFactory _httpClientFactory; + private readonly ILogger _logger; + + public CloudPhishingDomainDirectQuery( + IGlobalSettings globalSettings, + IHttpClientFactory httpClientFactory, + ILogger logger) + { + _globalSettings = globalSettings; + _httpClientFactory = httpClientFactory; + _logger = logger; + } + + public async Task> GetPhishingDomainsAsync() + { + if (string.IsNullOrWhiteSpace(_globalSettings.PhishingDomain?.UpdateUrl)) + { + throw new InvalidOperationException("Phishing domain update URL is not configured."); + } + + var httpClient = _httpClientFactory.CreateClient("PhishingDomains"); + var response = await httpClient.GetAsync(_globalSettings.PhishingDomain.UpdateUrl); + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync(); + return ParseDomains(content); + } + + /// + /// Gets the SHA256 checksum of the remote phishing domains list + /// + /// The SHA256 checksum as a lowercase hex string + public async Task GetRemoteChecksumAsync() + { + if (string.IsNullOrWhiteSpace(_globalSettings.PhishingDomain?.ChecksumUrl)) + { + _logger.LogWarning("Phishing domain checksum URL is not configured."); + return string.Empty; + } + + try + { + var httpClient = _httpClientFactory.CreateClient("PhishingDomains"); + var response = await httpClient.GetAsync(_globalSettings.PhishingDomain.ChecksumUrl); + response.EnsureSuccessStatusCode(); + + var content = await response.Content.ReadAsStringAsync(); + return ParseChecksumResponse(content); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving phishing domain checksum from {Url}", + _globalSettings.PhishingDomain.ChecksumUrl); + return string.Empty; + } + } + + /// + /// Parses a checksum response in the format "hash *filename" + /// + private static string ParseChecksumResponse(string checksumContent) + { + if (string.IsNullOrWhiteSpace(checksumContent)) + { + return string.Empty; + } + + // Format is typically "hash *filename" + var parts = checksumContent.Split(' ', 2); + + return parts.Length > 0 ? parts[0].Trim() : string.Empty; + } + + private static List ParseDomains(string content) + { + if (string.IsNullOrWhiteSpace(content)) + { + return []; + } + + return content + .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Trim()) + .Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith("#")) + .ToList(); + } +} diff --git a/src/Core/PhishingDomainFeatures/CloudPhishingDomainRelayQuery.cs b/src/Core/PhishingDomainFeatures/CloudPhishingDomainRelayQuery.cs new file mode 100644 index 0000000000..2685d36a7f --- /dev/null +++ b/src/Core/PhishingDomainFeatures/CloudPhishingDomainRelayQuery.cs @@ -0,0 +1,66 @@ +using Bit.Core.PhishingDomainFeatures.Interfaces; +using Bit.Core.Services; +using Bit.Core.Settings; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.PhishingDomainFeatures; + +/// +/// Implementation of ICloudPhishingDomainQuery for self-hosted environments +/// that relays the request to the Bitwarden cloud API +/// +public class CloudPhishingDomainRelayQuery : BaseIdentityClientService, ICloudPhishingDomainQuery +{ + private readonly IGlobalSettings _globalSettings; + + public CloudPhishingDomainRelayQuery( + IHttpClientFactory httpFactory, + IGlobalSettings globalSettings, + ILogger logger) + : base( + httpFactory, + globalSettings.Installation.ApiUri, + globalSettings.Installation.IdentityUri, + "api.licensing", + $"installation.{globalSettings.Installation.Id}", + globalSettings.Installation.Key, + logger) + { + _globalSettings = globalSettings; + } + + public async Task> GetPhishingDomainsAsync() + { + if (!_globalSettings.SelfHosted || !_globalSettings.EnableCloudCommunication) + { + throw new InvalidOperationException("This query is only for self-hosted installations with cloud communication enabled."); + } + + var result = await SendAsync(HttpMethod.Get, "phishing-domains", null, true); + return result?.ToList() ?? new List(); + } + + /// + /// Gets the SHA256 checksum of the remote phishing domains list + /// + /// The SHA256 checksum as a lowercase hex string + public async Task GetRemoteChecksumAsync() + { + if (!_globalSettings.SelfHosted || !_globalSettings.EnableCloudCommunication) + { + throw new InvalidOperationException("This query is only for self-hosted installations with cloud communication enabled."); + } + + try + { + // For self-hosted environments, we get the checksum from the Bitwarden cloud API + var result = await SendAsync(HttpMethod.Get, "phishing-domains/checksum", null, true); + return result ?? string.Empty; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error retrieving phishing domain checksum from Bitwarden cloud API"); + return string.Empty; + } + } +} diff --git a/src/Core/PhishingDomainFeatures/Interfaces/ICloudPhishingDomainQuery.cs b/src/Core/PhishingDomainFeatures/Interfaces/ICloudPhishingDomainQuery.cs new file mode 100644 index 0000000000..dac91747f7 --- /dev/null +++ b/src/Core/PhishingDomainFeatures/Interfaces/ICloudPhishingDomainQuery.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.PhishingDomainFeatures.Interfaces; + +public interface ICloudPhishingDomainQuery +{ + Task> GetPhishingDomainsAsync(); + Task GetRemoteChecksumAsync(); +} diff --git a/src/Core/Repositories/IPhishingDomainRepository.cs b/src/Core/Repositories/IPhishingDomainRepository.cs new file mode 100644 index 0000000000..2d653b0a43 --- /dev/null +++ b/src/Core/Repositories/IPhishingDomainRepository.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Repositories; + +public interface IPhishingDomainRepository +{ + Task> GetActivePhishingDomainsAsync(); + Task UpdatePhishingDomainsAsync(IEnumerable domains, string checksum); + Task GetCurrentChecksumAsync(); +} diff --git a/src/Core/Repositories/Implementations/AzurePhishingDomainRepository.cs b/src/Core/Repositories/Implementations/AzurePhishingDomainRepository.cs new file mode 100644 index 0000000000..2d4ea15b7e --- /dev/null +++ b/src/Core/Repositories/Implementations/AzurePhishingDomainRepository.cs @@ -0,0 +1,126 @@ +using System.Text.Json; +using Bit.Core.PhishingDomainFeatures; +using Microsoft.Extensions.Caching.Distributed; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.Repositories.Implementations; + +public class AzurePhishingDomainRepository : IPhishingDomainRepository +{ + private readonly AzurePhishingDomainStorageService _storageService; + private readonly IDistributedCache _cache; + private readonly ILogger _logger; + private const string _domainsCacheKey = "PhishingDomains_v1"; + private const string _checksumCacheKey = "PhishingDomains_Checksum_v1"; + private static readonly DistributedCacheEntryOptions _cacheOptions = new() + { + AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24), + SlidingExpiration = TimeSpan.FromHours(1) + }; + + public AzurePhishingDomainRepository( + AzurePhishingDomainStorageService storageService, + IDistributedCache cache, + ILogger logger) + { + _storageService = storageService; + _cache = cache; + _logger = logger; + } + + public async Task> GetActivePhishingDomainsAsync() + { + try + { + var cachedDomains = await _cache.GetStringAsync(_domainsCacheKey); + if (!string.IsNullOrEmpty(cachedDomains)) + { + _logger.LogDebug("Retrieved phishing domains from cache"); + return JsonSerializer.Deserialize>(cachedDomains) ?? []; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to retrieve phishing domains from cache"); + } + + var domains = await _storageService.GetDomainsAsync(); + + try + { + await _cache.SetStringAsync( + _domainsCacheKey, + JsonSerializer.Serialize(domains), + _cacheOptions); + _logger.LogDebug("Stored {Count} phishing domains in cache", domains.Count); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to store phishing domains in cache"); + } + + return domains; + } + + public async Task GetCurrentChecksumAsync() + { + try + { + var cachedChecksum = await _cache.GetStringAsync(_checksumCacheKey); + if (!string.IsNullOrEmpty(cachedChecksum)) + { + _logger.LogDebug("Retrieved phishing domain checksum from cache"); + return cachedChecksum; + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to retrieve phishing domain checksum from cache"); + } + + var checksum = await _storageService.GetChecksumAsync(); + + try + { + if (!string.IsNullOrEmpty(checksum)) + { + await _cache.SetStringAsync( + _checksumCacheKey, + checksum, + _cacheOptions); + _logger.LogDebug("Stored phishing domain checksum in cache"); + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to store phishing domain checksum in cache"); + } + + return checksum; + } + + public async Task UpdatePhishingDomainsAsync(IEnumerable domains, string checksum) + { + var domainsList = domains.ToList(); + await _storageService.UpdateDomainsAsync(domainsList, checksum); + + try + { + await _cache.SetStringAsync( + _domainsCacheKey, + JsonSerializer.Serialize(domainsList), + _cacheOptions); + + await _cache.SetStringAsync( + _checksumCacheKey, + checksum, + _cacheOptions); + + _logger.LogDebug("Updated phishing domains cache after update operation"); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to update phishing domains in cache"); + } + } +} diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 2b658c65b3..519889db45 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -85,6 +85,7 @@ public class GlobalSettings : IGlobalSettings public virtual ILaunchDarklySettings LaunchDarkly { get; set; } = new LaunchDarklySettings(); public virtual string DevelopmentDirectory { get; set; } public virtual IWebPushSettings WebPush { get; set; } = new WebPushSettings(); + public virtual IPhishingDomainSettings PhishingDomain { get; set; } = new PhishingDomainSettings(); public virtual bool EnableEmailVerification { get; set; } public virtual string KdfDefaultHashKey { get; set; } @@ -644,6 +645,12 @@ public class GlobalSettings : IGlobalSettings public int MaxNetworkRetries { get; set; } = 2; } + public class PhishingDomainSettings : IPhishingDomainSettings + { + public string UpdateUrl { get; set; } + public string ChecksumUrl { get; set; } + } + public class DistributedIpRateLimitingSettings { public string RedisConnectionString { get; set; } diff --git a/src/Core/Settings/IGlobalSettings.cs b/src/Core/Settings/IGlobalSettings.cs index 411014ea32..d77842373e 100644 --- a/src/Core/Settings/IGlobalSettings.cs +++ b/src/Core/Settings/IGlobalSettings.cs @@ -29,4 +29,5 @@ public interface IGlobalSettings string DevelopmentDirectory { get; set; } IWebPushSettings WebPush { get; set; } GlobalSettings.EventLoggingSettings EventLogging { get; set; } + IPhishingDomainSettings PhishingDomain { get; set; } } diff --git a/src/Core/Settings/IPhishingDomainSettings.cs b/src/Core/Settings/IPhishingDomainSettings.cs new file mode 100644 index 0000000000..2e4a901a5a --- /dev/null +++ b/src/Core/Settings/IPhishingDomainSettings.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Settings; + +public interface IPhishingDomainSettings +{ + string UpdateUrl { get; set; } + string ChecksumUrl { get; set; } +} From 92701d8cd0dd096d7abffbf38d591b930285a14b Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 30 Apr 2025 08:43:39 -0700 Subject: [PATCH 003/114] [PM-20550] Add JSON validation to Cipher Delete/Update attachment sprocs (#5656) * Add JSON validation to Cipher Delete/Update attachment sprocs * Remove [Attachment] assignment from cipher create/update sprocs * Add additional validation and use JSON_PATH_EXISTS for delete sproc check * Update migration script date --- .../Cipher/CipherDetails_Update.sql | 3 +- .../Cipher/Cipher_Create.sql | 4 +- .../Cipher/Cipher_DeleteAttachment.sql | 55 ++- .../Cipher/Cipher_Update.sql | 5 +- .../Cipher/Cipher_UpdateAttachment.sql | 80 +++- ...2025-04-16_00_AttachmentJsonValidation.sql | 350 ++++++++++++++++++ 6 files changed, 468 insertions(+), 29 deletions(-) create mode 100644 util/Migrator/DbScripts/2025-04-16_00_AttachmentJsonValidation.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_Update.sql b/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_Update.sql index 8fc95eb302..214b74d092 100644 --- a/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_Update.sql +++ b/src/Sql/Vault/dbo/Stored Procedures/Cipher/CipherDetails_Update.sql @@ -6,7 +6,7 @@ @Data NVARCHAR(MAX), @Favorites NVARCHAR(MAX), -- not used @Folders NVARCHAR(MAX), -- not used - @Attachments NVARCHAR(MAX), + @Attachments NVARCHAR(MAX), -- not used @CreationDate DATETIME2(7), @RevisionDate DATETIME2(7), @FolderId UNIQUEIDENTIFIER, @@ -50,7 +50,6 @@ BEGIN ELSE JSON_MODIFY([Favorites], @UserIdPath, NULL) END, - [Attachments] = @Attachments, [Reprompt] = @Reprompt, [CreationDate] = @CreationDate, [RevisionDate] = @RevisionDate, diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Create.sql b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Create.sql index c03c27ea78..676c013cc8 100644 --- a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Create.sql +++ b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Create.sql @@ -6,7 +6,7 @@ @Data NVARCHAR(MAX), @Favorites NVARCHAR(MAX), @Folders NVARCHAR(MAX), - @Attachments NVARCHAR(MAX), + @Attachments NVARCHAR(MAX), -- not used @CreationDate DATETIME2(7), @RevisionDate DATETIME2(7), @DeletedDate DATETIME2(7), @@ -25,7 +25,6 @@ BEGIN [Data], [Favorites], [Folders], - [Attachments], [CreationDate], [RevisionDate], [DeletedDate], @@ -41,7 +40,6 @@ BEGIN @Data, @Favorites, @Folders, - @Attachments, @CreationDate, @RevisionDate, @DeletedDate, diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteAttachment.sql b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteAttachment.sql index 5983f557c2..75a0468b42 100644 --- a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteAttachment.sql +++ b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteAttachment.sql @@ -10,20 +10,59 @@ BEGIN DECLARE @UserId UNIQUEIDENTIFIER DECLARE @OrganizationId UNIQUEIDENTIFIER + DECLARE @CurrentAttachments NVARCHAR(MAX) + DECLARE @NewAttachments NVARCHAR(MAX) + -- Get current cipher data SELECT @UserId = [UserId], - @OrganizationId = [OrganizationId] - FROM + @OrganizationId = [OrganizationId], + @CurrentAttachments = [Attachments] + FROM [dbo].[Cipher] WHERE [Id] = @Id - UPDATE - [dbo].[Cipher] - SET - [Attachments] = JSON_MODIFY([Attachments], @AttachmentIdPath, NULL) - WHERE - [Id] = @Id + -- If there are no attachments, nothing to do + IF @CurrentAttachments IS NULL + BEGIN + RETURN; + END + + -- Validate the initial JSON + IF ISJSON(@CurrentAttachments) = 0 + BEGIN + THROW 50000, 'Current initial attachments data is not valid JSON', 1; + RETURN; + END + + -- Check if the attachment exists before trying to remove it + IF JSON_PATH_EXISTS(@CurrentAttachments, @AttachmentIdPath) = 0 + BEGIN + -- Attachment doesn't exist, nothing to do + RETURN; + END + + -- Create the new attachments JSON with the specified attachment removed + SET @NewAttachments = JSON_MODIFY(@CurrentAttachments, @AttachmentIdPath, NULL) + + -- Validate the resulting JSON + IF ISJSON(@NewAttachments) = 0 + BEGIN + THROW 50000, 'Failed to create valid JSON when removing attachment', 1; + RETURN; + END + + -- Check if we've removed all attachments and have an empty object + IF @NewAttachments = '{}' + BEGIN + -- If we have an empty JSON object, set to NULL instead + SET @NewAttachments = NULL; + END + + -- Update with validated JSON + UPDATE [dbo].[Cipher] + SET [Attachments] = @NewAttachments + WHERE [Id] = @Id IF @OrganizationId IS NOT NULL BEGIN diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Update.sql b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Update.sql index 7815aa3053..0aa73ae9b6 100644 --- a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Update.sql +++ b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_Update.sql @@ -6,7 +6,7 @@ @Data NVARCHAR(MAX), @Favorites NVARCHAR(MAX), @Folders NVARCHAR(MAX), - @Attachments NVARCHAR(MAX), + @Attachments NVARCHAR(MAX), -- not used @CreationDate DATETIME2(7), @RevisionDate DATETIME2(7), @DeletedDate DATETIME2(7), @@ -25,7 +25,6 @@ BEGIN [Data] = @Data, [Favorites] = @Favorites, [Folders] = @Folders, - [Attachments] = @Attachments, [CreationDate] = @CreationDate, [RevisionDate] = @RevisionDate, [DeletedDate] = @DeletedDate, @@ -42,4 +41,4 @@ BEGIN BEGIN EXEC [dbo].[User_BumpAccountRevisionDate] @UserId END -END \ No newline at end of file +END diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_UpdateAttachment.sql b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_UpdateAttachment.sql index 520df72505..4401a4a0c6 100644 --- a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_UpdateAttachment.sql +++ b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_UpdateAttachment.sql @@ -8,21 +8,75 @@ AS BEGIN SET NOCOUNT ON + -- Validate that AttachmentData is valid JSON + IF ISJSON(@AttachmentData) = 0 + BEGIN + THROW 50000, 'Invalid JSON format in AttachmentData parameter', 1; + RETURN; + END + + -- Validate that AttachmentData has the expected structure + -- Check for required fields + IF JSON_VALUE(@AttachmentData, '$.FileName') IS NULL OR + JSON_VALUE(@AttachmentData, '$.Size') IS NULL + BEGIN + THROW 50000, 'AttachmentData is missing required fields (FileName, Size)', 1; + RETURN; + END + + -- Validate data types for critical fields + DECLARE @Size BIGINT = TRY_CAST(JSON_VALUE(@AttachmentData, '$.Size') AS BIGINT) + IF @Size IS NULL OR @Size <= 0 + BEGIN + THROW 50000, 'AttachmentData has invalid Size value', 1; + RETURN; + END + DECLARE @AttachmentIdKey VARCHAR(50) = CONCAT('"', @AttachmentId, '"') DECLARE @AttachmentIdPath VARCHAR(50) = CONCAT('$.', @AttachmentIdKey) + DECLARE @NewAttachments NVARCHAR(MAX) - UPDATE - [dbo].[Cipher] - SET - [Attachments] = - CASE - WHEN [Attachments] IS NULL THEN - CONCAT('{', @AttachmentIdKey, ':', @AttachmentData, '}') - ELSE - JSON_MODIFY([Attachments], @AttachmentIdPath, JSON_QUERY(@AttachmentData, '$')) - END - WHERE - [Id] = @Id + -- Get current attachments + DECLARE @CurrentAttachments NVARCHAR(MAX) + SELECT @CurrentAttachments = [Attachments] FROM [dbo].[Cipher] WHERE [Id] = @Id + + -- Prepare the new attachments value based on current state + IF @CurrentAttachments IS NULL + BEGIN + -- Create new JSON object with the attachment + SET @NewAttachments = CONCAT('{', @AttachmentIdKey, ':', @AttachmentData, '}') + + -- Validate the constructed JSON + IF ISJSON(@NewAttachments) = 0 + BEGIN + THROW 50000, 'Failed to create valid JSON when adding new attachment', 1; + RETURN; + END + END + ELSE + BEGIN + -- Validate existing attachments + IF ISJSON(@CurrentAttachments) = 0 + BEGIN + THROW 50000, 'Current attachments data is not valid JSON', 1; + RETURN; + END + + -- Modify existing JSON + SET @NewAttachments = JSON_MODIFY(@CurrentAttachments, @AttachmentIdPath, JSON_QUERY(@AttachmentData, '$')) + + -- Validate the modified JSON + IF ISJSON(@NewAttachments) = 0 + BEGIN + THROW 50000, 'Failed to create valid JSON when updating existing attachments', 1; + RETURN; + END + END + + -- Update with validated JSON + UPDATE [dbo].[Cipher] + SET [Attachments] = @NewAttachments + WHERE [Id] = @Id IF @OrganizationId IS NOT NULL BEGIN @@ -34,4 +88,4 @@ BEGIN EXEC [dbo].[User_UpdateStorage] @UserId EXEC [dbo].[User_BumpAccountRevisionDate] @UserId END -END \ No newline at end of file +END diff --git a/util/Migrator/DbScripts/2025-04-16_00_AttachmentJsonValidation.sql b/util/Migrator/DbScripts/2025-04-16_00_AttachmentJsonValidation.sql new file mode 100644 index 0000000000..a501b28574 --- /dev/null +++ b/util/Migrator/DbScripts/2025-04-16_00_AttachmentJsonValidation.sql @@ -0,0 +1,350 @@ +CREATE OR ALTER PROCEDURE [dbo].[Cipher_UpdateAttachment] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @AttachmentId VARCHAR(50), + @AttachmentData NVARCHAR(MAX) +AS +BEGIN + SET NOCOUNT ON + + -- Validate that AttachmentData is valid JSON + IF ISJSON(@AttachmentData) = 0 + BEGIN + THROW 50000, 'Invalid JSON format in AttachmentData parameter', 1; + RETURN; + END + + -- Validate that AttachmentData has the expected structure + -- Check for required fields + IF JSON_VALUE(@AttachmentData, '$.FileName') IS NULL OR + JSON_VALUE(@AttachmentData, '$.Size') IS NULL + BEGIN + THROW 50000, 'AttachmentData is missing required fields (FileName, Size)', 1; + RETURN; + END + + -- Validate data types for critical fields + DECLARE @Size BIGINT = TRY_CAST(JSON_VALUE(@AttachmentData, '$.Size') AS BIGINT) + IF @Size IS NULL OR @Size <= 0 + BEGIN + THROW 50000, 'AttachmentData has invalid Size value', 1; + RETURN; + END + + DECLARE @AttachmentIdKey VARCHAR(50) = CONCAT('"', @AttachmentId, '"') + DECLARE @AttachmentIdPath VARCHAR(50) = CONCAT('$.', @AttachmentIdKey) + DECLARE @NewAttachments NVARCHAR(MAX) + + -- Get current attachments + DECLARE @CurrentAttachments NVARCHAR(MAX) + SELECT @CurrentAttachments = [Attachments] FROM [dbo].[Cipher] WHERE [Id] = @Id + + -- Prepare the new attachments value based on current state + IF @CurrentAttachments IS NULL + BEGIN + -- Create new JSON object with the attachment + SET @NewAttachments = CONCAT('{', @AttachmentIdKey, ':', @AttachmentData, '}') + + -- Validate the constructed JSON + IF ISJSON(@NewAttachments) = 0 + BEGIN + THROW 50000, 'Failed to create valid JSON when adding new attachment', 1; + RETURN; + END + END + ELSE + BEGIN + -- Validate existing attachments + IF ISJSON(@CurrentAttachments) = 0 + BEGIN + THROW 50000, 'Current attachments data is not valid JSON', 1; + RETURN; + END + + -- Modify existing JSON + SET @NewAttachments = JSON_MODIFY(@CurrentAttachments, @AttachmentIdPath, JSON_QUERY(@AttachmentData, '$')) + + -- Validate the modified JSON + IF ISJSON(@NewAttachments) = 0 + BEGIN + THROW 50000, 'Failed to create valid JSON when updating existing attachments', 1; + RETURN; + END + END + + -- Update with validated JSON + UPDATE [dbo].[Cipher] + SET [Attachments] = @NewAttachments + WHERE [Id] = @Id + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[Organization_UpdateStorage] @OrganizationId + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_UpdateStorage] @UserId + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Cipher_DeleteAttachment] + @Id UNIQUEIDENTIFIER, + @AttachmentId VARCHAR(50) +AS +BEGIN + SET NOCOUNT ON + + DECLARE @AttachmentIdKey VARCHAR(50) = CONCAT('"', @AttachmentId, '"') + DECLARE @AttachmentIdPath VARCHAR(50) = CONCAT('$.', @AttachmentIdKey) + + DECLARE @UserId UNIQUEIDENTIFIER + DECLARE @OrganizationId UNIQUEIDENTIFIER + DECLARE @CurrentAttachments NVARCHAR(MAX) + DECLARE @NewAttachments NVARCHAR(MAX) + + -- Get current cipher data + SELECT + @UserId = [UserId], + @OrganizationId = [OrganizationId], + @CurrentAttachments = [Attachments] + FROM + [dbo].[Cipher] + WHERE [Id] = @Id + + -- If there are no attachments, nothing to do + IF @CurrentAttachments IS NULL + BEGIN + RETURN; + END + + -- Validate the initial JSON + IF ISJSON(@CurrentAttachments) = 0 + BEGIN + THROW 50000, 'Current initial attachments data is not valid JSON', 1; + RETURN; + END + + -- Check if the attachment exists before trying to remove it + IF JSON_PATH_EXISTS(@CurrentAttachments, @AttachmentIdPath) = 0 + BEGIN + -- Attachment doesn't exist, nothing to do + RETURN; + END + + -- Create the new attachments JSON with the specified attachment removed + SET @NewAttachments = JSON_MODIFY(@CurrentAttachments, @AttachmentIdPath, NULL) + + -- Validate the resulting JSON + IF ISJSON(@NewAttachments) = 0 + BEGIN + THROW 50000, 'Failed to create valid JSON when removing attachment', 1; + RETURN; + END + + -- Check if we've removed all attachments and have an empty object + IF @NewAttachments = '{}' + BEGIN + -- If we have an empty JSON object, set to NULL instead + SET @NewAttachments = NULL; + END + + -- Update with validated JSON + UPDATE [dbo].[Cipher] + SET [Attachments] = @NewAttachments + WHERE [Id] = @Id + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[Organization_UpdateStorage] @OrganizationId + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_UpdateStorage] @UserId + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END +GO + +-- Remove [Attachments] assignment from Cipher_Create, Cipher_Update, and CipherDetails_Update procedures + +CREATE OR ALTER PROCEDURE [dbo].[Cipher_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @Favorites NVARCHAR(MAX), + @Folders NVARCHAR(MAX), + @Attachments NVARCHAR(MAX), -- not used + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @DeletedDate DATETIME2(7), + @Reprompt TINYINT, + @Key VARCHAR(MAX) = NULL +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[Cipher] + ( + [Id], + [UserId], + [OrganizationId], + [Type], + [Data], + [Favorites], + [Folders], + [CreationDate], + [RevisionDate], + [DeletedDate], + [Reprompt], + [Key] + ) + VALUES + ( + @Id, + CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END, + @OrganizationId, + @Type, + @Data, + @Favorites, + @Folders, + @CreationDate, + @RevisionDate, + @DeletedDate, + @Reprompt, + @Key + ) + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[CipherDetails_Update] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @Favorites NVARCHAR(MAX), -- not used + @Folders NVARCHAR(MAX), -- not used + @Attachments NVARCHAR(MAX), -- not used + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @FolderId UNIQUEIDENTIFIER, + @Favorite BIT, + @Edit BIT, -- not used + @ViewPassword BIT, -- not used + @Manage BIT, -- not used + @OrganizationUseTotp BIT, -- not used + @DeletedDate DATETIME2(2), + @Reprompt TINYINT, + @Key VARCHAR(MAX) = NULL +AS +BEGIN + SET NOCOUNT ON + + DECLARE @UserIdKey VARCHAR(50) = CONCAT('"', @UserId, '"') + DECLARE @UserIdPath VARCHAR(50) = CONCAT('$.', @UserIdKey) + + UPDATE + [dbo].[Cipher] + SET + [UserId] = CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END, + [OrganizationId] = @OrganizationId, + [Type] = @Type, + [Data] = @Data, + [Folders] = + CASE + WHEN @FolderId IS NOT NULL AND [Folders] IS NULL THEN + CONCAT('{', @UserIdKey, ':"', @FolderId, '"', '}') + WHEN @FolderId IS NOT NULL THEN + JSON_MODIFY([Folders], @UserIdPath, CAST(@FolderId AS VARCHAR(50))) + ELSE + JSON_MODIFY([Folders], @UserIdPath, NULL) + END, + [Favorites] = + CASE + WHEN @Favorite = 1 AND [Favorites] IS NULL THEN + CONCAT('{', @UserIdKey, ':true}') + WHEN @Favorite = 1 THEN + JSON_MODIFY([Favorites], @UserIdPath, CAST(1 AS BIT)) + ELSE + JSON_MODIFY([Favorites], @UserIdPath, NULL) + END, + [Reprompt] = @Reprompt, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [DeletedDate] = @DeletedDate, + [Key] = @Key + WHERE + [Id] = @Id + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Cipher_Update] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data NVARCHAR(MAX), + @Favorites NVARCHAR(MAX), + @Folders NVARCHAR(MAX), + @Attachments NVARCHAR(MAX), -- not used + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @DeletedDate DATETIME2(7), + @Reprompt TINYINT, + @Key VARCHAR(MAX) = NULL +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[Cipher] + SET + [UserId] = CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END, + [OrganizationId] = @OrganizationId, + [Type] = @Type, + [Data] = @Data, + [Favorites] = @Favorites, + [Folders] = @Folders, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [DeletedDate] = @DeletedDate, + [Reprompt] = @Reprompt, + [Key] = @Key + WHERE + [Id] = @Id + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END +GO From 8ecd9c5fb329a8949043173766fad3398cb0bd44 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Thu, 1 May 2025 10:07:19 -0400 Subject: [PATCH 004/114] [PM-19332] Create InitPendingOrganizationCommand (#5584) * wip * implement CommandResult * remove auth handler * fix import * remove method from OrganizationService * cleanup, add tests * clean up * fix auth in tests * clean up comments * clean up comments * clean up test --- .../OrganizationUsersController.cs | 7 +- .../InitPendingOrganizationCommand.cs | 128 +++++++++++++ .../IInitPendingOrganizationCommand.cs | 13 ++ .../Services/IOrganizationService.cs | 7 - .../Implementations/OrganizationService.cs | 78 +------- ...OrganizationServiceCollectionExtensions.cs | 1 + .../InitPendingOrganizationCommandTests.cs | 169 ++++++++++++++++++ 7 files changed, 317 insertions(+), 86 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommand.cs create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IInitPendingOrganizationCommand.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommandTests.cs diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 5a714943f0..e21dd3de49 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -63,6 +63,7 @@ public class OrganizationUsersController : Controller private readonly IPricingClient _pricingClient; private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand; private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand; + private readonly IInitPendingOrganizationCommand _initPendingOrganizationCommand; public OrganizationUsersController( IOrganizationRepository organizationRepository, @@ -89,7 +90,8 @@ public class OrganizationUsersController : Controller IFeatureService featureService, IPricingClient pricingClient, IConfirmOrganizationUserCommand confirmOrganizationUserCommand, - IRestoreOrganizationUserCommand restoreOrganizationUserCommand) + IRestoreOrganizationUserCommand restoreOrganizationUserCommand, + IInitPendingOrganizationCommand initPendingOrganizationCommand) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -116,6 +118,7 @@ public class OrganizationUsersController : Controller _pricingClient = pricingClient; _confirmOrganizationUserCommand = confirmOrganizationUserCommand; _restoreOrganizationUserCommand = restoreOrganizationUserCommand; + _initPendingOrganizationCommand = initPendingOrganizationCommand; } [HttpGet("{id}")] @@ -313,7 +316,7 @@ public class OrganizationUsersController : Controller throw new UnauthorizedAccessException(); } - await _organizationService.InitPendingOrganization(user, orgId, organizationUserId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName, model.Token); + await _initPendingOrganizationCommand.InitPendingOrganizationAsync(user, orgId, organizationUserId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName, model.Token); await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService); await _confirmOrganizationUserCommand.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommand.cs new file mode 100644 index 0000000000..3e060c66a5 --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommand.cs @@ -0,0 +1,128 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Services; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Core.Tokens; +using Microsoft.AspNetCore.DataProtection; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; + +public class InitPendingOrganizationCommand : IInitPendingOrganizationCommand +{ + + private readonly IOrganizationService _organizationService; + private readonly ICollectionRepository _collectionRepository; + private readonly IOrganizationRepository _organizationRepository; + private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; + private readonly IDataProtector _dataProtector; + private readonly IGlobalSettings _globalSettings; + private readonly IPolicyService _policyService; + private readonly IOrganizationUserRepository _organizationUserRepository; + + public InitPendingOrganizationCommand( + IOrganizationService organizationService, + ICollectionRepository collectionRepository, + IOrganizationRepository organizationRepository, + IDataProtectorTokenFactory orgUserInviteTokenDataFactory, + IDataProtectionProvider dataProtectionProvider, + IGlobalSettings globalSettings, + IPolicyService policyService, + IOrganizationUserRepository organizationUserRepository + ) + { + _organizationService = organizationService; + _collectionRepository = collectionRepository; + _organizationRepository = organizationRepository; + _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; + _dataProtector = dataProtectionProvider.CreateProtector(OrgUserInviteTokenable.DataProtectorPurpose); + _globalSettings = globalSettings; + _policyService = policyService; + _organizationUserRepository = organizationUserRepository; + } + + public async Task InitPendingOrganizationAsync(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken) + { + await ValidateSignUpPoliciesAsync(user.Id); + + var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); + if (orgUser == null) + { + throw new BadRequestException("User invalid."); + } + + var tokenValid = ValidateInviteToken(orgUser, user, emailToken); + + if (!tokenValid) + { + throw new BadRequestException("Invalid token"); + } + + var org = await _organizationRepository.GetByIdAsync(organizationId); + + if (org.Enabled) + { + throw new BadRequestException("Organization is already enabled."); + } + + if (org.Status != OrganizationStatusType.Pending) + { + throw new BadRequestException("Organization is not on a Pending status."); + } + + if (!string.IsNullOrEmpty(org.PublicKey)) + { + throw new BadRequestException("Organization already has a Public Key."); + } + + if (!string.IsNullOrEmpty(org.PrivateKey)) + { + throw new BadRequestException("Organization already has a Private Key."); + } + + org.Enabled = true; + org.Status = OrganizationStatusType.Created; + org.PublicKey = publicKey; + org.PrivateKey = privateKey; + + await _organizationService.UpdateAsync(org); + + if (!string.IsNullOrWhiteSpace(collectionName)) + { + // give the owner Can Manage access over the default collection + List defaultOwnerAccess = + [new CollectionAccessSelection { Id = orgUser.Id, HidePasswords = false, ReadOnly = false, Manage = true }]; + + var defaultCollection = new Collection + { + Name = collectionName, + OrganizationId = org.Id + }; + await _collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess); + } + } + + private async Task ValidateSignUpPoliciesAsync(Guid ownerId) + { + var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg); + if (anySingleOrgPolicies) + { + throw new BadRequestException("You may not create an organization. You belong to an organization " + + "which has a policy that prohibits you from being a member of any other organization."); + } + } + + private bool ValidateInviteToken(OrganizationUser orgUser, User user, string emailToken) + { + var tokenValid = OrgUserInviteTokenable.ValidateOrgUserInviteStringToken( + _orgUserInviteTokenDataFactory, emailToken, orgUser); + + return tokenValid; + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IInitPendingOrganizationCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IInitPendingOrganizationCommand.cs new file mode 100644 index 0000000000..273182664e --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/Interfaces/IInitPendingOrganizationCommand.cs @@ -0,0 +1,13 @@ +using Bit.Core.Entities; +namespace Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces; + +public interface IInitPendingOrganizationCommand +{ + /// + /// Update an Organization entry by setting the public/private keys, set it as 'Enabled' and move the Status from 'Pending' to 'Created'. + /// + /// + /// This method must target a disabled Organization that has null keys and status as 'Pending'. + /// + Task InitPendingOrganizationAsync(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken); +} diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index 228c2b522c..9c9e311a02 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -48,13 +48,6 @@ public interface IOrganizationService Task>> RevokeUsersAsync(Guid organizationId, IEnumerable organizationUserIds, Guid? revokingUserId); Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted); - /// - /// Update an Organization entry by setting the public/private keys, set it as 'Enabled' and move the Status from 'Pending' to 'Created'. - /// - /// - /// This method must target a disabled Organization that has null keys and status as 'Pending'. - /// - Task InitPendingOrganization(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken); Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null); void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade); diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index b31b43406e..532aebf5e0 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -13,7 +13,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Constants; @@ -31,12 +30,10 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Platform.Push; using Bit.Core.Repositories; using Bit.Core.Settings; -using Bit.Core.Tokens; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Services; using Bit.Core.Utilities; -using Microsoft.AspNetCore.DataProtection; using Microsoft.Extensions.Logging; using Stripe; using OrganizationUserInvite = Bit.Core.Models.Business.OrganizationUserInvite; @@ -77,8 +74,6 @@ public class OrganizationService : IOrganizationService private readonly IPricingClient _pricingClient; private readonly IPolicyRequirementQuery _policyRequirementQuery; private readonly ISendOrganizationInvitesCommand _sendOrganizationInvitesCommand; - private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; - private readonly IDataProtector _dataProtector; public OrganizationService( IOrganizationRepository organizationRepository, @@ -112,9 +107,7 @@ public class OrganizationService : IOrganizationService IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, IPricingClient pricingClient, IPolicyRequirementQuery policyRequirementQuery, - ISendOrganizationInvitesCommand sendOrganizationInvitesCommand, - IDataProtectorTokenFactory orgUserInviteTokenDataFactory, - IDataProtectionProvider dataProtectionProvider + ISendOrganizationInvitesCommand sendOrganizationInvitesCommand ) { _organizationRepository = organizationRepository; @@ -149,8 +142,6 @@ public class OrganizationService : IOrganizationService _pricingClient = pricingClient; _policyRequirementQuery = policyRequirementQuery; _sendOrganizationInvitesCommand = sendOrganizationInvitesCommand; - _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; - _dataProtector = dataProtectionProvider.CreateProtector(OrgUserInviteTokenable.DataProtectorPurpose); } public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, @@ -1921,71 +1912,4 @@ public class OrganizationService : IOrganizationService SalesAssistedTrialStarted = salesAssistedTrialStarted, }); } - - public async Task InitPendingOrganization(User user, Guid organizationId, Guid organizationUserId, string publicKey, string privateKey, string collectionName, string emailToken) - { - await ValidateSignUpPoliciesAsync(user.Id); - - var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId); - if (orgUser == null) - { - throw new BadRequestException("User invalid."); - } - - // TODO: PM-4142 - remove old token validation logic once 3 releases of backwards compatibility are complete - var newTokenValid = OrgUserInviteTokenable.ValidateOrgUserInviteStringToken( - _orgUserInviteTokenDataFactory, emailToken, orgUser); - - var tokenValid = newTokenValid || - CoreHelpers.UserInviteTokenIsValid(_dataProtector, emailToken, user.Email, orgUser.Id, - _globalSettings); - - if (!tokenValid) - { - throw new BadRequestException("Invalid token."); - } - - var org = await GetOrgById(organizationId); - - if (org.Enabled) - { - throw new BadRequestException("Organization is already enabled."); - } - - if (org.Status != OrganizationStatusType.Pending) - { - throw new BadRequestException("Organization is not on a Pending status."); - } - - if (!string.IsNullOrEmpty(org.PublicKey)) - { - throw new BadRequestException("Organization already has a Public Key."); - } - - if (!string.IsNullOrEmpty(org.PrivateKey)) - { - throw new BadRequestException("Organization already has a Private Key."); - } - - org.Enabled = true; - org.Status = OrganizationStatusType.Created; - org.PublicKey = publicKey; - org.PrivateKey = privateKey; - - await UpdateAsync(org); - - if (!string.IsNullOrWhiteSpace(collectionName)) - { - // give the owner Can Manage access over the default collection - List defaultOwnerAccess = - [new CollectionAccessSelection { Id = organizationUserId, HidePasswords = false, ReadOnly = false, Manage = true }]; - - var defaultCollection = new Collection - { - Name = collectionName, - OrganizationId = org.Id - }; - await _collectionRepository.CreateAsync(defaultCollection, null, defaultOwnerAccess); - } - } } diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index 164710d522..b016e329bf 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -193,6 +193,7 @@ public static class OrganizationServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } // TODO: move to OrganizationSubscriptionServiceCollectionExtensions when OrganizationUser methods are moved out of diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommandTests.cs new file mode 100644 index 0000000000..83ea4798db --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/InitPendingOrganizationCommandTests.cs @@ -0,0 +1,169 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Tokens; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Fakes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations; + +[SutProviderCustomize] +public class InitPendingOrganizationCommandTests +{ + + private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory = Substitute.For(); + private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory(); + + [Theory, BitAutoData] + public async Task Init_Organization_Success(User user, Guid orgId, Guid orgUserId, string publicKey, + string privateKey, SutProvider sutProvider, Organization org, OrganizationUser orgUser) + { + var token = CreateToken(orgUser, orgUserId, sutProvider); + + org.PrivateKey = null; + org.PublicKey = null; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(org); + + var organizationServcie = sutProvider.GetDependency(); + var collectionRepository = sutProvider.GetDependency(); + + await sutProvider.Sut.InitPendingOrganizationAsync(user, orgId, orgUserId, publicKey, privateKey, "", token); + + await organizationRepository.Received().GetByIdAsync(orgId); + await organizationServcie.Received().UpdateAsync(org); + await collectionRepository.DidNotReceiveWithAnyArgs().CreateAsync(default); + + } + + [Theory, BitAutoData] + public async Task Init_Organization_With_CollectionName_Success(User user, Guid orgId, Guid orgUserId, string publicKey, + string privateKey, SutProvider sutProvider, Organization org, string collectionName, OrganizationUser orgUser) + { + var token = CreateToken(orgUser, orgUserId, sutProvider); + + org.PrivateKey = null; + org.PublicKey = null; + org.Id = orgId; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(org); + + var organizationServcie = sutProvider.GetDependency(); + var collectionRepository = sutProvider.GetDependency(); + + await sutProvider.Sut.InitPendingOrganizationAsync(user, orgId, orgUserId, publicKey, privateKey, collectionName, token); + + await organizationRepository.Received().GetByIdAsync(orgId); + await organizationServcie.Received().UpdateAsync(org); + + await collectionRepository.Received().CreateAsync( + Arg.Any(), + Arg.Is>(l => l == null), + Arg.Is>(l => l.Any(i => i.Manage == true))); + + } + + [Theory, BitAutoData] + public async Task Init_Organization_When_Organization_Is_Enabled(User user, Guid orgId, Guid orgUserId, string publicKey, + string privateKey, SutProvider sutProvider, Organization org, OrganizationUser orgUser) + + { + var token = CreateToken(orgUser, orgUserId, sutProvider); + + org.Enabled = true; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(org); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.InitPendingOrganizationAsync(user, orgId, orgUserId, publicKey, privateKey, "", token)); + + Assert.Equal("Organization is already enabled.", exception.Message); + + } + + [Theory, BitAutoData] + public async Task Init_Organization_When_Organization_Is_Not_Pending(User user, Guid orgId, Guid orgUserId, string publicKey, + string privateKey, SutProvider sutProvider, Organization org, OrganizationUser orgUser) + + { + + + var token = CreateToken(orgUser, orgUserId, sutProvider); + + org.Status = Enums.OrganizationStatusType.Created; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(org); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.InitPendingOrganizationAsync(user, orgId, orgUserId, publicKey, privateKey, "", token)); + + Assert.Equal("Organization is not on a Pending status.", exception.Message); + + } + + [Theory, BitAutoData] + public async Task Init_Organization_When_Organization_Has_Public_Key(User user, Guid orgId, Guid orgUserId, string publicKey, + string privateKey, SutProvider sutProvider, Organization org, OrganizationUser orgUser) + + { + var token = CreateToken(orgUser, orgUserId, sutProvider); + + org.PublicKey = publicKey; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(org); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.InitPendingOrganizationAsync(user, orgId, orgUserId, publicKey, privateKey, "", token)); + + Assert.Equal("Organization already has a Public Key.", exception.Message); + + } + + [Theory, BitAutoData] + public async Task Init_Organization_When_Organization_Has_Private_Key(User user, Guid orgId, Guid orgUserId, string publicKey, + string privateKey, SutProvider sutProvider, Organization org, OrganizationUser orgUser) + + { + + var token = CreateToken(orgUser, orgUserId, sutProvider); + + org.PublicKey = null; + org.PrivateKey = privateKey; + org.Enabled = false; + + var organizationRepository = sutProvider.GetDependency(); + organizationRepository.GetByIdAsync(orgId).Returns(org); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.InitPendingOrganizationAsync(user, orgId, orgUserId, publicKey, privateKey, "", token)); + + Assert.Equal("Organization already has a Private Key.", exception.Message); + + } + + public string CreateToken(OrganizationUser orgUser, Guid orgUserId, SutProvider sutProvider) + { + sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory"); + sutProvider.Create(); + + _orgUserInviteTokenableFactory.CreateToken(orgUser).Returns(new OrgUserInviteTokenable(orgUser) + { + ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5)) + }); + + var orgUserInviteTokenable = _orgUserInviteTokenableFactory.CreateToken(orgUser); + var protectedToken = _orgUserInviteTokenDataFactory.Protect(orgUserInviteTokenable); + sutProvider.GetDependency().GetByIdAsync(orgUserId).Returns(orgUser); + + return protectedToken; + } +} From dc5db5673f4ca5d70d3bac0ca0b5900f699a808f Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Thu, 1 May 2025 16:35:51 +0100 Subject: [PATCH 005/114] [PM-17775] (#5699) * Changes to allow admin to send F4E sponsorship * Fix the failing unit tests * Fix the failing test Signed-off-by: Cy Okeke * Merge Changes with pm-17777 Signed-off-by: Cy Okeke * Add changes for autoscale Signed-off-by: Cy Okeke * Return the right error response Signed-off-by: Cy Okeke * Resolve the failing unit test Signed-off-by: Cy Okeke --------- Signed-off-by: Cy Okeke --- .../OrganizationSponsorshipsController.cs | 29 ++++++- ...nizationSponsorshipInvitesResponseModel.cs | 37 +++++++++ .../CreateSponsorshipCommand.cs | 25 ++++-- ...OrganizationSponsorshipsControllerTests.cs | 78 +++++++++++++++++++ 4 files changed, 161 insertions(+), 8 deletions(-) create mode 100644 src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipInvitesResponseModel.cs diff --git a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs index 67cd691a34..9a328081fe 100644 --- a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs @@ -1,4 +1,5 @@ using Bit.Api.Models.Request.Organizations; +using Bit.Api.Models.Response; using Bit.Api.Models.Response.Organizations; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationConnections.Interfaces; @@ -8,6 +9,7 @@ using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Models.Api.Request.OrganizationSponsorships; using Bit.Core.Models.Api.Response.OrganizationSponsorships; +using Bit.Core.Models.Data.Organizations.OrganizationSponsorships; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -105,7 +107,10 @@ public class OrganizationSponsorshipsController : Controller model.FriendlyName, model.IsAdminInitiated.GetValueOrDefault(), model.Notes); - await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name); + if (sponsorship.OfferedToEmail != null) + { + await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name); + } } [Authorize("Application")] @@ -246,5 +251,27 @@ public class OrganizationSponsorshipsController : Controller return new OrganizationSponsorshipSyncStatusResponseModel(lastSyncDate); } + [Authorize("Application")] + [HttpGet("{sponsoringOrgId}/sponsored")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task> GetSponsoredOrganizations(Guid sponsoringOrgId) + { + var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgId); + if (sponsoringOrg == null) + { + throw new NotFoundException(); + } + var organization = _currentContext.Organizations.First(x => x.Id == sponsoringOrg.Id); + if (!await _currentContext.OrganizationOwner(sponsoringOrg.Id) && !await _currentContext.OrganizationAdmin(sponsoringOrg.Id) && !organization.Permissions.ManageUsers) + { + throw new UnauthorizedAccessException(); + } + + var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId); + return new ListResponseModel(sponsorships.Select(s => + new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s)))); + + } + private Task CurrentUser => _userService.GetUserByIdAsync(_currentContext.UserId.Value); } diff --git a/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipInvitesResponseModel.cs b/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipInvitesResponseModel.cs new file mode 100644 index 0000000000..b75144c81b --- /dev/null +++ b/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipInvitesResponseModel.cs @@ -0,0 +1,37 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Data.Organizations.OrganizationSponsorships; + +namespace Bit.Core.Models.Api.Response.OrganizationSponsorships; + +public class OrganizationSponsorshipInvitesResponseModel : ResponseModel +{ + public OrganizationSponsorshipInvitesResponseModel(OrganizationSponsorshipData sponsorshipData, string obj = "organizationSponsorship") : base(obj) + { + if (sponsorshipData == null) + { + throw new ArgumentNullException(nameof(sponsorshipData)); + } + + SponsoringOrganizationUserId = sponsorshipData.SponsoringOrganizationUserId; + FriendlyName = sponsorshipData.FriendlyName; + OfferedToEmail = sponsorshipData.OfferedToEmail; + PlanSponsorshipType = sponsorshipData.PlanSponsorshipType; + LastSyncDate = sponsorshipData.LastSyncDate; + ValidUntil = sponsorshipData.ValidUntil; + ToDelete = sponsorshipData.ToDelete; + IsAdminInitiated = sponsorshipData.IsAdminInitiated; + Notes = sponsorshipData.Notes; + CloudSponsorshipRemoved = sponsorshipData.CloudSponsorshipRemoved; + } + + public Guid SponsoringOrganizationUserId { get; set; } + public string FriendlyName { get; set; } + public string OfferedToEmail { get; set; } + public PlanSponsorshipType PlanSponsorshipType { get; set; } + public DateTime? LastSyncDate { get; set; } + public DateTime? ValidUntil { get; set; } + public bool ToDelete { get; set; } + public bool IsAdminInitiated { get; set; } + public string Notes { get; set; } + public bool CloudSponsorshipRemoved { get; set; } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs index f81a1d9e84..3b74baf6f9 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs @@ -47,11 +47,12 @@ public class CreateSponsorshipCommand( throw new BadRequestException("Only confirmed users can sponsor other organizations."); } - var existingOrgSponsorship = await organizationSponsorshipRepository - .GetBySponsoringOrganizationUserIdAsync(sponsoringMember.Id); - if (existingOrgSponsorship?.SponsoredOrganizationId != null) + var sponsorships = + await organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrganization.Id); + var existingSponsorship = sponsorships.FirstOrDefault(s => s.FriendlyName == friendlyName); + if (existingSponsorship != null) { - throw new BadRequestException("Can only sponsor one organization per Organization User."); + return existingSponsorship; } if (isAdminInitiated) @@ -70,10 +71,20 @@ public class CreateSponsorshipCommand( Notes = notes }; - if (existingOrgSponsorship != null) + if (!isAdminInitiated) { - // Replace existing invalid offer with our new sponsorship offer - sponsorship.Id = existingOrgSponsorship.Id; + var existingOrgSponsorship = await organizationSponsorshipRepository + .GetBySponsoringOrganizationUserIdAsync(sponsoringMember.Id); + if (existingOrgSponsorship?.SponsoredOrganizationId != null) + { + throw new BadRequestException("Can only sponsor one organization per Organization User."); + } + + if (existingOrgSponsorship != null) + { + // Replace existing invalid offer with our new sponsorship offer + sponsorship.Id = existingOrgSponsorship.Id; + } } if (isAdminInitiated && sponsoringOrganization.Seats.HasValue) diff --git a/test/Api.Test/Billing/Controllers/OrganizationSponsorshipsControllerTests.cs b/test/Api.Test/Billing/Controllers/OrganizationSponsorshipsControllerTests.cs index 377bc9c2c8..f6158b9e3f 100644 --- a/test/Api.Test/Billing/Controllers/OrganizationSponsorshipsControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/OrganizationSponsorshipsControllerTests.cs @@ -6,6 +6,7 @@ using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.Models.Data; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -13,6 +14,7 @@ using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; +using NSubstitute.ReturnsExtensions; using Xunit; namespace Bit.Api.Test.Billing.Controllers; @@ -146,4 +148,80 @@ public class OrganizationSponsorshipsControllerTests .DidNotReceiveWithAnyArgs() .RemoveSponsorshipAsync(default); } + + [Theory] + [BitAutoData] + public async Task GetSponsoredOrganizations_OrganizationNotFound_ThrowsNotFound( + Guid sponsoringOrgId, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetByIdAsync(sponsoringOrgId).ReturnsNull(); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetSponsoredOrganizations(sponsoringOrgId)); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .GetManyBySponsoringOrganizationAsync(default); + } + + [Theory] + [BitAutoData] + public async Task GetSponsoredOrganizations_NotOrganizationOwner_ThrowsNotFound( + Organization sponsoringOrg, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg); + sutProvider.GetDependency().OrganizationOwner(sponsoringOrg.Id).Returns(false); + sutProvider.GetDependency().OrganizationAdmin(sponsoringOrg.Id).Returns(false); + + // Create a CurrentContextOrganization with ManageUsers set to false + var currentContextOrg = new CurrentContextOrganization + { + Id = sponsoringOrg.Id, + Permissions = new Permissions { ManageUsers = false } + }; + sutProvider.GetDependency().Organizations.Returns(new List { currentContextOrg }); + + // Act & Assert + await Assert.ThrowsAsync(() => + sutProvider.Sut.GetSponsoredOrganizations(sponsoringOrg.Id)); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .GetManyBySponsoringOrganizationAsync(default); + } + + [Theory] + [BitAutoData] + public async Task GetSponsoredOrganizations_Success_ReturnsSponsorships( + Organization sponsoringOrg, + List sponsorships, + SutProvider sutProvider) + { + // Arrange + sutProvider.GetDependency().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg); + sutProvider.GetDependency().OrganizationOwner(sponsoringOrg.Id).Returns(true); + sutProvider.GetDependency().OrganizationAdmin(sponsoringOrg.Id).Returns(false); + + // Create a CurrentContextOrganization from the sponsoringOrg + var currentContextOrg = new CurrentContextOrganization + { + Id = sponsoringOrg.Id, + Permissions = new Permissions { ManageUsers = true } + }; + sutProvider.GetDependency().Organizations.Returns(new List { currentContextOrg }); + + sutProvider.GetDependency() + .GetManyBySponsoringOrganizationAsync(sponsoringOrg.Id).Returns(sponsorships); + + // Act + var result = await sutProvider.Sut.GetSponsoredOrganizations(sponsoringOrg.Id); + + // Assert + Assert.Equal(sponsorships.Count, result.Data.Count()); + await sutProvider.GetDependency().Received(1) + .GetManyBySponsoringOrganizationAsync(sponsoringOrg.Id); + } } From e77acbc5ad419b3641eab6ba381f4b3d55105e10 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 1 May 2025 12:12:45 -0400 Subject: [PATCH 006/114] [PM-19956] [PM-18795] Require provider payment method during setup behind FF (#5752) * Require provider payment method during setup behind FF * Fix failing test * Run dotnet format * Rui's feedback --- .../AdminConsole/Services/ProviderService.cs | 18 +- .../Billing/ProviderBillingService.cs | 131 +++- .../Services/ProviderServiceTests.cs | 85 ++- .../Billing/ProviderBillingServiceTests.cs | 605 +++++++++++++++++- .../Controllers/ProvidersController.cs | 26 +- .../Providers/ProviderSetupRequestModel.cs | 3 + .../AdminConsole/Services/IProviderService.cs | 4 +- .../NoopProviderService.cs | 3 +- .../Services/IProviderBillingService.cs | 4 +- src/Core/Constants.cs | 1 + 10 files changed, 848 insertions(+), 32 deletions(-) diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs index fff6b5271d..2fc44937a7 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs @@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Models; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Context; @@ -82,7 +83,7 @@ public class ProviderService : IProviderService _pricingClient = pricingClient; } - public async Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo = null) + public async Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource = null) { var owner = await _userService.GetUserByIdAsync(ownerUserId); if (owner == null) @@ -111,7 +112,20 @@ public class ProviderService : IProviderService { throw new BadRequestException("Both address and postal code are required to set up your provider."); } - var customer = await _providerBillingService.SetupCustomer(provider, taxInfo); + + var requireProviderPaymentMethodDuringSetup = + _featureService.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup); + + if (requireProviderPaymentMethodDuringSetup && tokenizedPaymentSource is not + { + Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal, + Token: not null and not "" + }) + { + throw new BadRequestException("A payment method is required to set up your provider."); + } + + var customer = await _providerBillingService.SetupCustomer(provider, taxInfo, tokenizedPaymentSource); provider.GatewayCustomerId = customer.Id; var subscription = await _providerBillingService.SetupSubscription(provider); provider.GatewaySubscriptionId = subscription.Id; diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs index bdfff079cf..f049d6c8df 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs @@ -6,9 +6,11 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing; +using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Repositories; @@ -21,14 +23,20 @@ using Bit.Core.Models.Business; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; +using Braintree; using CsvHelper; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Stripe; +using static Bit.Core.Billing.Utilities; +using Customer = Stripe.Customer; +using Subscription = Stripe.Subscription; + namespace Bit.Commercial.Core.Billing; public class ProviderBillingService( + IBraintreeGateway braintreeGateway, IEventService eventService, IFeatureService featureService, IGlobalSettings globalSettings, @@ -39,6 +47,7 @@ public class ProviderBillingService( IProviderOrganizationRepository providerOrganizationRepository, IProviderPlanRepository providerPlanRepository, IProviderUserRepository providerUserRepository, + ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, ISubscriberService subscriberService, ITaxService taxService, @@ -463,7 +472,8 @@ public class ProviderBillingService( public async Task SetupCustomer( Provider provider, - TaxInfo taxInfo) + TaxInfo taxInfo, + TokenizedPaymentSource tokenizedPaymentSource = null) { if (taxInfo is not { @@ -532,13 +542,97 @@ public class ProviderBillingService( options.Coupon = provider.DiscountId; } + var requireProviderPaymentMethodDuringSetup = + featureService.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup); + + var braintreeCustomerId = ""; + + if (requireProviderPaymentMethodDuringSetup) + { + if (tokenizedPaymentSource is not + { + Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal, + Token: not null and not "" + }) + { + logger.LogError("Cannot create customer for provider ({ProviderID}) without a payment method", provider.Id); + throw new BillingException(); + } + + var (type, token) = tokenizedPaymentSource; + + // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault + switch (type) + { + case PaymentMethodType.BankAccount: + { + var setupIntent = + (await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = token })) + .FirstOrDefault(); + + if (setupIntent == null) + { + logger.LogError("Cannot create customer for provider ({ProviderID}) without a setup intent for their bank account", provider.Id); + throw new BillingException(); + } + + await setupIntentCache.Set(provider.Id, setupIntent.Id); + break; + } + case PaymentMethodType.Card: + { + options.PaymentMethod = token; + options.InvoiceSettings.DefaultPaymentMethod = token; + break; + } + case PaymentMethodType.PayPal: + { + braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(provider, token); + options.Metadata[BraintreeCustomerIdKey] = braintreeCustomerId; + break; + } + } + } + try { return await stripeAdapter.CustomerCreateAsync(options); } - catch (StripeException stripeException) when (stripeException.StripeError?.Code == StripeConstants.ErrorCodes.TaxIdInvalid) + catch (StripeException stripeException) when (stripeException.StripeError?.Code == + StripeConstants.ErrorCodes.TaxIdInvalid) { - throw new BadRequestException("Your tax ID wasn't recognized for your selected country. Please ensure your country and tax ID are valid."); + await Revert(); + throw new BadRequestException( + "Your tax ID wasn't recognized for your selected country. Please ensure your country and tax ID are valid."); + } + catch + { + await Revert(); + throw; + } + + async Task Revert() + { + if (requireProviderPaymentMethodDuringSetup && tokenizedPaymentSource != null) + { + // ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault + switch (tokenizedPaymentSource.Type) + { + case PaymentMethodType.BankAccount: + { + var setupIntentId = await setupIntentCache.Get(provider.Id); + await stripeAdapter.SetupIntentCancel(setupIntentId, + new SetupIntentCancelOptions { CancellationReason = "abandoned" }); + await setupIntentCache.Remove(provider.Id); + break; + } + case PaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId): + { + await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId); + break; + } + } + } } } @@ -580,18 +674,38 @@ public class ProviderBillingService( }); } + var requireProviderPaymentMethodDuringSetup = + featureService.IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup); + + var setupIntentId = await setupIntentCache.Get(provider.Id); + + var setupIntent = !string.IsNullOrEmpty(setupIntentId) + ? await stripeAdapter.SetupIntentGet(setupIntentId, new SetupIntentGetOptions + { + Expand = ["payment_method"] + }) + : null; + + var usePaymentMethod = + requireProviderPaymentMethodDuringSetup && + (!string.IsNullOrEmpty(customer.InvoiceSettings.DefaultPaymentMethodId) || + customer.Metadata.ContainsKey(BraintreeCustomerIdKey) || + setupIntent.IsUnverifiedBankAccount()); + var subscriptionCreateOptions = new SubscriptionCreateOptions { - CollectionMethod = StripeConstants.CollectionMethod.SendInvoice, + CollectionMethod = usePaymentMethod ? + StripeConstants.CollectionMethod.ChargeAutomatically : StripeConstants.CollectionMethod.SendInvoice, Customer = customer.Id, - DaysUntilDue = 30, + DaysUntilDue = usePaymentMethod ? null : 30, Items = subscriptionItemOptionsList, Metadata = new Dictionary { { "providerId", provider.Id.ToString() } }, OffSession = true, - ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations + ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations, + TrialPeriodDays = usePaymentMethod ? 14 : null }; if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements)) @@ -607,7 +721,10 @@ public class ProviderBillingService( { var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); - if (subscription.Status == StripeConstants.SubscriptionStatus.Active) + if (subscription is + { + Status: StripeConstants.SubscriptionStatus.Active or StripeConstants.SubscriptionStatus.Trialing + }) { return subscription; } diff --git a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs index d2d82f47de..c66acfa8ce 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs @@ -1,5 +1,6 @@ using Bit.Commercial.Core.AdminConsole.Services; using Bit.Commercial.Core.Test.AdminConsole.AutoFixture; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; @@ -7,6 +8,7 @@ using Bit.Core.AdminConsole.Models.Business.Provider; using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Models; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Context; @@ -38,7 +40,7 @@ public class ProviderServiceTests public async Task CompleteSetupAsync_UserIdIsInvalid_Throws(SutProvider sutProvider) { var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.CompleteSetupAsync(default, default, default, default)); + () => sutProvider.Sut.CompleteSetupAsync(default, default, default, default, null)); Assert.Contains("Invalid owner.", exception.Message); } @@ -50,12 +52,85 @@ public class ProviderServiceTests userService.GetUserByIdAsync(user.Id).Returns(user); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.CompleteSetupAsync(provider, user.Id, default, default)); + () => sutProvider.Sut.CompleteSetupAsync(provider, user.Id, default, default, null)); Assert.Contains("Invalid token.", exception.Message); } [Theory, BitAutoData] - public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, TaxInfo taxInfo, + public async Task CompleteSetupAsync_InvalidTaxInfo_ThrowsBadRequestException( + User user, + Provider provider, + string key, + TaxInfo taxInfo, + TokenizedPaymentSource tokenizedPaymentSource, + [ProviderUser] ProviderUser providerUser, + SutProvider sutProvider) + { + providerUser.ProviderId = provider.Id; + providerUser.UserId = user.Id; + var userService = sutProvider.GetDependency(); + userService.GetUserByIdAsync(user.Id).Returns(user); + + var providerUserRepository = sutProvider.GetDependency(); + providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser); + + var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName"); + var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector"); + sutProvider.GetDependency().CreateProtector("ProviderServiceDataProtector") + .Returns(protector); + + sutProvider.Create(); + + var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}"); + + taxInfo.BillingAddressCountry = null; + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource)); + + Assert.Equal("Both address and postal code are required to set up your provider.", exception.Message); + } + + [Theory, BitAutoData] + public async Task CompleteSetupAsync_InvalidTokenizedPaymentSource_ThrowsBadRequestException( + User user, + Provider provider, + string key, + TaxInfo taxInfo, + TokenizedPaymentSource tokenizedPaymentSource, + [ProviderUser] ProviderUser providerUser, + SutProvider sutProvider) + { + providerUser.ProviderId = provider.Id; + providerUser.UserId = user.Id; + var userService = sutProvider.GetDependency(); + userService.GetUserByIdAsync(user.Id).Returns(user); + + var providerUserRepository = sutProvider.GetDependency(); + providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser); + + var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName"); + var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector"); + sutProvider.GetDependency().CreateProtector("ProviderServiceDataProtector") + .Returns(protector); + + sutProvider.Create(); + + var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}"); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + tokenizedPaymentSource = tokenizedPaymentSource with { Type = PaymentMethodType.BitPay }; + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource)); + + Assert.Equal("A payment method is required to set up your provider.", exception.Message); + } + + [Theory, BitAutoData] + public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource, [ProviderUser] ProviderUser providerUser, SutProvider sutProvider) { @@ -75,7 +150,7 @@ public class ProviderServiceTests var providerBillingService = sutProvider.GetDependency(); var customer = new Customer { Id = "customer_id" }; - providerBillingService.SetupCustomer(provider, taxInfo).Returns(customer); + providerBillingService.SetupCustomer(provider, taxInfo, tokenizedPaymentSource).Returns(customer); var subscription = new Subscription { Id = "subscription_id" }; providerBillingService.SetupSubscription(provider).Returns(subscription); @@ -84,7 +159,7 @@ public class ProviderServiceTests var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}"); - await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo); + await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource); await sutProvider.GetDependency().Received().UpsertAsync(Arg.Is( p => diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs index 2661a0eff6..1862692087 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs @@ -2,14 +2,17 @@ using System.Net; using Bit.Commercial.Core.Billing; using Bit.Commercial.Core.Billing.Models; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Models; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; @@ -24,11 +27,17 @@ using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; +using Braintree; using CsvHelper; using NSubstitute; +using NSubstitute.ExceptionExtensions; using Stripe; using Xunit; using static Bit.Core.Test.Billing.Utilities; +using Address = Stripe.Address; +using Customer = Stripe.Customer; +using PaymentMethod = Stripe.PaymentMethod; +using Subscription = Stripe.Subscription; namespace Bit.Commercial.Core.Test.Billing; @@ -833,7 +842,7 @@ public class ProviderBillingServiceTests } [Theory, BitAutoData] - public async Task SetupCustomer_Success( + public async Task SetupCustomer_NoPaymentMethod_Success( SutProvider sutProvider, Provider provider, TaxInfo taxInfo) @@ -877,6 +886,301 @@ public class ProviderBillingServiceTests Assert.Equivalent(expected, actual); } + [Theory, BitAutoData] + public async Task SetupCustomer_InvalidRequiredPaymentMethod_ThrowsBillingException( + SutProvider sutProvider, + Provider provider, + TaxInfo taxInfo, + TokenizedPaymentSource tokenizedPaymentSource) + { + provider.Name = "MSP"; + + sutProvider.GetDependency() + .GetStripeTaxCode(Arg.Is( + p => p == taxInfo.BillingAddressCountry), + Arg.Is(p => p == taxInfo.TaxIdNumber)) + .Returns(taxInfo.TaxIdType); + + taxInfo.BillingAddressCountry = "AD"; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + tokenizedPaymentSource = tokenizedPaymentSource with { Type = PaymentMethodType.BitPay }; + + await ThrowsBillingExceptionAsync(() => + sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource)); + } + + [Theory, BitAutoData] + public async Task SetupCustomer_WithBankAccount_Error_Reverts( + SutProvider sutProvider, + Provider provider, + TaxInfo taxInfo) + { + provider.Name = "MSP"; + + sutProvider.GetDependency() + .GetStripeTaxCode(Arg.Is( + p => p == taxInfo.BillingAddressCountry), + Arg.Is(p => p == taxInfo.TaxIdNumber)) + .Returns(taxInfo.TaxIdType); + + taxInfo.BillingAddressCountry = "AD"; + + var stripeAdapter = sutProvider.GetDependency(); + + var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.BankAccount, "token"); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + stripeAdapter.SetupIntentList(Arg.Is(options => + options.PaymentMethod == tokenizedPaymentSource.Token)).Returns([ + new SetupIntent { Id = "setup_intent_id" } + ]); + + stripeAdapter.CustomerCreateAsync(Arg.Is(o => + o.Address.Country == taxInfo.BillingAddressCountry && + o.Address.PostalCode == taxInfo.BillingAddressPostalCode && + o.Address.Line1 == taxInfo.BillingAddressLine1 && + o.Address.Line2 == taxInfo.BillingAddressLine2 && + o.Address.City == taxInfo.BillingAddressCity && + o.Address.State == taxInfo.BillingAddressState && + o.Description == WebUtility.HtmlDecode(provider.BusinessName) && + o.Email == provider.BillingEmail && + o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" && + o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" && + o.Metadata["region"] == "" && + o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType && + o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber)) + .Throws(); + + sutProvider.GetDependency().Get(provider.Id).Returns("setup_intent_id"); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource)); + + await sutProvider.GetDependency().Received(1).Set(provider.Id, "setup_intent_id"); + + await stripeAdapter.Received(1).SetupIntentCancel("setup_intent_id", Arg.Is(options => + options.CancellationReason == "abandoned")); + + await sutProvider.GetDependency().Received(1).Remove(provider.Id); + } + + [Theory, BitAutoData] + public async Task SetupCustomer_WithPayPal_Error_Reverts( + SutProvider sutProvider, + Provider provider, + TaxInfo taxInfo) + { + provider.Name = "MSP"; + + sutProvider.GetDependency() + .GetStripeTaxCode(Arg.Is( + p => p == taxInfo.BillingAddressCountry), + Arg.Is(p => p == taxInfo.TaxIdNumber)) + .Returns(taxInfo.TaxIdType); + + taxInfo.BillingAddressCountry = "AD"; + + var stripeAdapter = sutProvider.GetDependency(); + + var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.PayPal, "token"); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + sutProvider.GetDependency().CreateBraintreeCustomer(provider, tokenizedPaymentSource.Token) + .Returns("braintree_customer_id"); + + stripeAdapter.CustomerCreateAsync(Arg.Is(o => + o.Address.Country == taxInfo.BillingAddressCountry && + o.Address.PostalCode == taxInfo.BillingAddressPostalCode && + o.Address.Line1 == taxInfo.BillingAddressLine1 && + o.Address.Line2 == taxInfo.BillingAddressLine2 && + o.Address.City == taxInfo.BillingAddressCity && + o.Address.State == taxInfo.BillingAddressState && + o.Description == WebUtility.HtmlDecode(provider.BusinessName) && + o.Email == provider.BillingEmail && + o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" && + o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" && + o.Metadata["region"] == "" && + o.Metadata["btCustomerId"] == "braintree_customer_id" && + o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType && + o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber)) + .Throws(); + + await Assert.ThrowsAsync(() => + sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource)); + + await sutProvider.GetDependency().Customer.Received(1).DeleteAsync("braintree_customer_id"); + } + + [Theory, BitAutoData] + public async Task SetupCustomer_WithBankAccount_Success( + SutProvider sutProvider, + Provider provider, + TaxInfo taxInfo) + { + provider.Name = "MSP"; + + sutProvider.GetDependency() + .GetStripeTaxCode(Arg.Is( + p => p == taxInfo.BillingAddressCountry), + Arg.Is(p => p == taxInfo.TaxIdNumber)) + .Returns(taxInfo.TaxIdType); + + taxInfo.BillingAddressCountry = "AD"; + + var stripeAdapter = sutProvider.GetDependency(); + + var expected = new Customer + { + Id = "customer_id", + Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } + }; + + var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.BankAccount, "token"); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + stripeAdapter.SetupIntentList(Arg.Is(options => + options.PaymentMethod == tokenizedPaymentSource.Token)).Returns([ + new SetupIntent { Id = "setup_intent_id" } + ]); + + stripeAdapter.CustomerCreateAsync(Arg.Is(o => + o.Address.Country == taxInfo.BillingAddressCountry && + o.Address.PostalCode == taxInfo.BillingAddressPostalCode && + o.Address.Line1 == taxInfo.BillingAddressLine1 && + o.Address.Line2 == taxInfo.BillingAddressLine2 && + o.Address.City == taxInfo.BillingAddressCity && + o.Address.State == taxInfo.BillingAddressState && + o.Description == WebUtility.HtmlDecode(provider.BusinessName) && + o.Email == provider.BillingEmail && + o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" && + o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" && + o.Metadata["region"] == "" && + o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType && + o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber)) + .Returns(expected); + + var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource); + + Assert.Equivalent(expected, actual); + + await sutProvider.GetDependency().Received(1).Set(provider.Id, "setup_intent_id"); + } + + [Theory, BitAutoData] + public async Task SetupCustomer_WithPayPal_Success( + SutProvider sutProvider, + Provider provider, + TaxInfo taxInfo) + { + provider.Name = "MSP"; + + sutProvider.GetDependency() + .GetStripeTaxCode(Arg.Is( + p => p == taxInfo.BillingAddressCountry), + Arg.Is(p => p == taxInfo.TaxIdNumber)) + .Returns(taxInfo.TaxIdType); + + taxInfo.BillingAddressCountry = "AD"; + + var stripeAdapter = sutProvider.GetDependency(); + + var expected = new Customer + { + Id = "customer_id", + Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } + }; + + var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.PayPal, "token"); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + sutProvider.GetDependency().CreateBraintreeCustomer(provider, tokenizedPaymentSource.Token) + .Returns("braintree_customer_id"); + + stripeAdapter.CustomerCreateAsync(Arg.Is(o => + o.Address.Country == taxInfo.BillingAddressCountry && + o.Address.PostalCode == taxInfo.BillingAddressPostalCode && + o.Address.Line1 == taxInfo.BillingAddressLine1 && + o.Address.Line2 == taxInfo.BillingAddressLine2 && + o.Address.City == taxInfo.BillingAddressCity && + o.Address.State == taxInfo.BillingAddressState && + o.Description == WebUtility.HtmlDecode(provider.BusinessName) && + o.Email == provider.BillingEmail && + o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" && + o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" && + o.Metadata["region"] == "" && + o.Metadata["btCustomerId"] == "braintree_customer_id" && + o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType && + o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber)) + .Returns(expected); + + var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource); + + Assert.Equivalent(expected, actual); + } + + [Theory, BitAutoData] + public async Task SetupCustomer_WithCard_Success( + SutProvider sutProvider, + Provider provider, + TaxInfo taxInfo) + { + provider.Name = "MSP"; + + sutProvider.GetDependency() + .GetStripeTaxCode(Arg.Is( + p => p == taxInfo.BillingAddressCountry), + Arg.Is(p => p == taxInfo.TaxIdNumber)) + .Returns(taxInfo.TaxIdType); + + taxInfo.BillingAddressCountry = "AD"; + + var stripeAdapter = sutProvider.GetDependency(); + + var expected = new Customer + { + Id = "customer_id", + Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } + }; + + var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.Card, "token"); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + stripeAdapter.CustomerCreateAsync(Arg.Is(o => + o.Address.Country == taxInfo.BillingAddressCountry && + o.Address.PostalCode == taxInfo.BillingAddressPostalCode && + o.Address.Line1 == taxInfo.BillingAddressLine1 && + o.Address.Line2 == taxInfo.BillingAddressLine2 && + o.Address.City == taxInfo.BillingAddressCity && + o.Address.State == taxInfo.BillingAddressState && + o.Description == WebUtility.HtmlDecode(provider.BusinessName) && + o.Email == provider.BillingEmail && + o.PaymentMethod == tokenizedPaymentSource.Token && + o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentSource.Token && + o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" && + o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" && + o.Metadata["region"] == "" && + o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType && + o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber)) + .Returns(expected); + + var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource); + + Assert.Equivalent(expected, actual); + } + [Theory, BitAutoData] public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid( SutProvider sutProvider, @@ -1044,7 +1348,7 @@ public class ProviderBillingServiceTests } [Theory, BitAutoData] - public async Task SetupSubscription_Succeeds( + public async Task SetupSubscription_SendInvoice_Succeeds( SutProvider sutProvider, Provider provider) { @@ -1127,6 +1431,303 @@ public class ProviderBillingServiceTests Assert.Equivalent(expected, actual); } + [Theory, BitAutoData] + public async Task SetupSubscription_ChargeAutomatically_HasCard_Succeeds( + SutProvider sutProvider, + Provider provider) + { + provider.Type = ProviderType.Msp; + provider.GatewaySubscriptionId = null; + + var customer = new Customer + { + Id = "customer_id", + InvoiceSettings = new CustomerInvoiceSettings + { + DefaultPaymentMethodId = "pm_123" + }, + Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } + }; + + sutProvider.GetDependency() + .GetCustomerOrThrow( + provider, + Arg.Is(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids"))).Returns(customer); + + var providerPlans = new List + { + new() + { + Id = Guid.NewGuid(), + ProviderId = provider.Id, + PlanType = PlanType.TeamsMonthly, + SeatMinimum = 100, + PurchasedSeats = 0, + AllocatedSeats = 0 + }, + new() + { + Id = Guid.NewGuid(), + ProviderId = provider.Id, + PlanType = PlanType.EnterpriseMonthly, + SeatMinimum = 100, + PurchasedSeats = 0, + AllocatedSeats = 0 + } + }; + + foreach (var plan in providerPlans) + { + sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) + .Returns(StaticStore.GetPlan(plan.PlanType)); + } + + sutProvider.GetDependency().GetByProviderId(provider.Id) + .Returns(providerPlans); + + var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active }; + + sutProvider.GetDependency() + .When(x => x.SetCreateOptions( + Arg.Is(options => + options.Customer == "customer_id") + , Arg.Is(p => p == customer))) + .Do(x => + { + x.Arg().AutomaticTax = new SubscriptionAutomaticTaxOptions + { + Enabled = true + }; + }); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + sutProvider.GetDependency().SubscriptionCreateAsync(Arg.Is( + sub => + sub.AutomaticTax.Enabled == true && + sub.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically && + sub.Customer == "customer_id" && + sub.DaysUntilDue == null && + sub.Items.Count == 2 && + sub.Items.ElementAt(0).Price == ProviderPriceAdapter.MSP.Active.Teams && + sub.Items.ElementAt(0).Quantity == 100 && + sub.Items.ElementAt(1).Price == ProviderPriceAdapter.MSP.Active.Enterprise && + sub.Items.ElementAt(1).Quantity == 100 && + sub.Metadata["providerId"] == provider.Id.ToString() && + sub.OffSession == true && + sub.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations && + sub.TrialPeriodDays == 14)).Returns(expected); + + var actual = await sutProvider.Sut.SetupSubscription(provider); + + Assert.Equivalent(expected, actual); + } + + [Theory, BitAutoData] + public async Task SetupSubscription_ChargeAutomatically_HasBankAccount_Succeeds( + SutProvider sutProvider, + Provider provider) + { + provider.Type = ProviderType.Msp; + provider.GatewaySubscriptionId = null; + + var customer = new Customer + { + Id = "customer_id", + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary(), + Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } + }; + + sutProvider.GetDependency() + .GetCustomerOrThrow( + provider, + Arg.Is(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids"))).Returns(customer); + + var providerPlans = new List + { + new() + { + Id = Guid.NewGuid(), + ProviderId = provider.Id, + PlanType = PlanType.TeamsMonthly, + SeatMinimum = 100, + PurchasedSeats = 0, + AllocatedSeats = 0 + }, + new() + { + Id = Guid.NewGuid(), + ProviderId = provider.Id, + PlanType = PlanType.EnterpriseMonthly, + SeatMinimum = 100, + PurchasedSeats = 0, + AllocatedSeats = 0 + } + }; + + foreach (var plan in providerPlans) + { + sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) + .Returns(StaticStore.GetPlan(plan.PlanType)); + } + + sutProvider.GetDependency().GetByProviderId(provider.Id) + .Returns(providerPlans); + + var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active }; + + sutProvider.GetDependency() + .When(x => x.SetCreateOptions( + Arg.Is(options => + options.Customer == "customer_id") + , Arg.Is(p => p == customer))) + .Do(x => + { + x.Arg().AutomaticTax = new SubscriptionAutomaticTaxOptions + { + Enabled = true + }; + }); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + const string setupIntentId = "seti_123"; + + sutProvider.GetDependency().Get(provider.Id).Returns(setupIntentId); + + sutProvider.GetDependency().SetupIntentGet(setupIntentId, Arg.Is(options => + options.Expand.Contains("payment_method"))).Returns(new SetupIntent + { + Id = setupIntentId, + Status = "requires_action", + NextAction = new SetupIntentNextAction + { + VerifyWithMicrodeposits = new SetupIntentNextActionVerifyWithMicrodeposits() + }, + PaymentMethod = new PaymentMethod + { + UsBankAccount = new PaymentMethodUsBankAccount() + } + }); + + sutProvider.GetDependency().SubscriptionCreateAsync(Arg.Is( + sub => + sub.AutomaticTax.Enabled == true && + sub.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically && + sub.Customer == "customer_id" && + sub.DaysUntilDue == null && + sub.Items.Count == 2 && + sub.Items.ElementAt(0).Price == ProviderPriceAdapter.MSP.Active.Teams && + sub.Items.ElementAt(0).Quantity == 100 && + sub.Items.ElementAt(1).Price == ProviderPriceAdapter.MSP.Active.Enterprise && + sub.Items.ElementAt(1).Quantity == 100 && + sub.Metadata["providerId"] == provider.Id.ToString() && + sub.OffSession == true && + sub.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations && + sub.TrialPeriodDays == 14)).Returns(expected); + + var actual = await sutProvider.Sut.SetupSubscription(provider); + + Assert.Equivalent(expected, actual); + } + + [Theory, BitAutoData] + public async Task SetupSubscription_ChargeAutomatically_HasPayPal_Succeeds( + SutProvider sutProvider, + Provider provider) + { + provider.Type = ProviderType.Msp; + provider.GatewaySubscriptionId = null; + + var customer = new Customer + { + Id = "customer_id", + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary + { + ["btCustomerId"] = "braintree_customer_id" + }, + Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } + }; + + sutProvider.GetDependency() + .GetCustomerOrThrow( + provider, + Arg.Is(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids"))).Returns(customer); + + var providerPlans = new List + { + new() + { + Id = Guid.NewGuid(), + ProviderId = provider.Id, + PlanType = PlanType.TeamsMonthly, + SeatMinimum = 100, + PurchasedSeats = 0, + AllocatedSeats = 0 + }, + new() + { + Id = Guid.NewGuid(), + ProviderId = provider.Id, + PlanType = PlanType.EnterpriseMonthly, + SeatMinimum = 100, + PurchasedSeats = 0, + AllocatedSeats = 0 + } + }; + + foreach (var plan in providerPlans) + { + sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) + .Returns(StaticStore.GetPlan(plan.PlanType)); + } + + sutProvider.GetDependency().GetByProviderId(provider.Id) + .Returns(providerPlans); + + var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active }; + + sutProvider.GetDependency() + .When(x => x.SetCreateOptions( + Arg.Is(options => + options.Customer == "customer_id") + , Arg.Is(p => p == customer))) + .Do(x => + { + x.Arg().AutomaticTax = new SubscriptionAutomaticTaxOptions + { + Enabled = true + }; + }); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + sutProvider.GetDependency().SubscriptionCreateAsync(Arg.Is( + sub => + sub.AutomaticTax.Enabled == true && + sub.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically && + sub.Customer == "customer_id" && + sub.DaysUntilDue == null && + sub.Items.Count == 2 && + sub.Items.ElementAt(0).Price == ProviderPriceAdapter.MSP.Active.Teams && + sub.Items.ElementAt(0).Quantity == 100 && + sub.Items.ElementAt(1).Price == ProviderPriceAdapter.MSP.Active.Enterprise && + sub.Items.ElementAt(1).Quantity == 100 && + sub.Metadata["providerId"] == provider.Id.ToString() && + sub.OffSession == true && + sub.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations && + sub.TrialPeriodDays == 14)).Returns(expected); + + var actual = await sutProvider.Sut.SetupSubscription(provider); + + Assert.Equivalent(expected, actual); + } + #endregion #region UpdateSeatMinimums diff --git a/src/Api/AdminConsole/Controllers/ProvidersController.cs b/src/Api/AdminConsole/Controllers/ProvidersController.cs index be119744b3..b6933da0c9 100644 --- a/src/Api/AdminConsole/Controllers/ProvidersController.cs +++ b/src/Api/AdminConsole/Controllers/ProvidersController.cs @@ -84,22 +84,22 @@ public class ProvidersController : Controller var userId = _userService.GetProperUserId(User).Value; - var taxInfo = model.TaxInfo != null - ? new TaxInfo - { - BillingAddressCountry = model.TaxInfo.Country, - BillingAddressPostalCode = model.TaxInfo.PostalCode, - TaxIdNumber = model.TaxInfo.TaxId, - BillingAddressLine1 = model.TaxInfo.Line1, - BillingAddressLine2 = model.TaxInfo.Line2, - BillingAddressCity = model.TaxInfo.City, - BillingAddressState = model.TaxInfo.State - } - : null; + var taxInfo = new TaxInfo + { + BillingAddressCountry = model.TaxInfo.Country, + BillingAddressPostalCode = model.TaxInfo.PostalCode, + TaxIdNumber = model.TaxInfo.TaxId, + BillingAddressLine1 = model.TaxInfo.Line1, + BillingAddressLine2 = model.TaxInfo.Line2, + BillingAddressCity = model.TaxInfo.City, + BillingAddressState = model.TaxInfo.State + }; + + var tokenizedPaymentSource = model.PaymentSource?.ToDomain(); var response = await _providerService.CompleteSetupAsync(model.ToProvider(provider), userId, model.Token, model.Key, - taxInfo); + taxInfo, tokenizedPaymentSource); return new ProviderResponseModel(response); } diff --git a/src/Api/AdminConsole/Models/Request/Providers/ProviderSetupRequestModel.cs b/src/Api/AdminConsole/Models/Request/Providers/ProviderSetupRequestModel.cs index 5e10807c69..697077c9b6 100644 --- a/src/Api/AdminConsole/Models/Request/Providers/ProviderSetupRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Providers/ProviderSetupRequestModel.cs @@ -1,5 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; +using Bit.Api.Billing.Models.Requests; using Bit.Api.Models.Request; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Utilities; @@ -23,7 +24,9 @@ public class ProviderSetupRequestModel public string Token { get; set; } [Required] public string Key { get; set; } + [Required] public ExpandedTaxInfoUpdateRequestModel TaxInfo { get; set; } + public TokenizedPaymentSourceRequestBody PaymentSource { get; set; } public virtual Provider ToProvider(Provider provider) { diff --git a/src/Core/AdminConsole/Services/IProviderService.cs b/src/Core/AdminConsole/Services/IProviderService.cs index 8999b3cb81..e4b6f3aabd 100644 --- a/src/Core/AdminConsole/Services/IProviderService.cs +++ b/src/Core/AdminConsole/Services/IProviderService.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Models.Business.Provider; +using Bit.Core.Billing.Models; using Bit.Core.Entities; using Bit.Core.Models.Business; @@ -7,7 +8,8 @@ namespace Bit.Core.AdminConsole.Services; public interface IProviderService { - Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo = null); + Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo, + TokenizedPaymentSource tokenizedPaymentSource = null); Task UpdateAsync(Provider provider, bool updateBilling = false); Task> InviteUserAsync(ProviderUserInvite invite); diff --git a/src/Core/AdminConsole/Services/NoopImplementations/NoopProviderService.cs b/src/Core/AdminConsole/Services/NoopImplementations/NoopProviderService.cs index bd3a757663..94c1096b58 100644 --- a/src/Core/AdminConsole/Services/NoopImplementations/NoopProviderService.cs +++ b/src/Core/AdminConsole/Services/NoopImplementations/NoopProviderService.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Models.Business.Provider; +using Bit.Core.Billing.Models; using Bit.Core.Entities; using Bit.Core.Models.Business; @@ -7,7 +8,7 @@ namespace Bit.Core.AdminConsole.Services.NoopImplementations; public class NoopProviderService : IProviderService { - public Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo = null) => throw new NotImplementedException(); + public Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource = null) => throw new NotImplementedException(); public Task UpdateAsync(Provider provider, bool updateBilling = false) => throw new NotImplementedException(); diff --git a/src/Core/Billing/Services/IProviderBillingService.cs b/src/Core/Billing/Services/IProviderBillingService.cs index 64585f3361..6ed8910dd8 100644 --- a/src/Core/Billing/Services/IProviderBillingService.cs +++ b/src/Core/Billing/Services/IProviderBillingService.cs @@ -79,10 +79,12 @@ public interface IProviderBillingService /// /// The to create a Stripe customer for. /// The to use for calculating the customer's automatic tax. + /// The (ex. Credit Card) to attach to the customer. /// The newly created for the . Task SetupCustomer( Provider provider, - TaxInfo taxInfo); + TaxInfo taxInfo, + TokenizedPaymentSource tokenizedPaymentSource = null); /// /// For use during the provider setup process, this method starts a Stripe for the given . diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index a05b89a94f..0d4c105b2d 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -149,6 +149,7 @@ public static class FeatureFlagKeys public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates"; public const string PM18770_EnableOrganizationBusinessUnitConversion = "pm-18770-enable-organization-business-unit-conversion"; public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically"; + public const string PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup"; /* Data Insights and Reporting Team */ public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application"; From 706d7a5768be5ba40dec5c9bc64f2030a4f0a20e Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Thu, 1 May 2025 10:08:39 -0700 Subject: [PATCH 007/114] Migrate to new LD Action for code references (#5759) --- .github/workflows/code-references.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/code-references.yml b/.github/workflows/code-references.yml index ce8cb8e467..676a747017 100644 --- a/.github/workflows/code-references.yml +++ b/.github/workflows/code-references.yml @@ -37,12 +37,10 @@ jobs: - name: Collect id: collect - uses: launchdarkly/find-code-references-in-pull-request@30f4c4ab2949bbf258b797ced2fbf6dea34df9ce # v2.1.0 + uses: launchdarkly/find-code-references@e3e9da201b87ada54eb4c550c14fb783385c5c8a # v2.13.0 with: - project-key: default - environment-key: dev - access-token: ${{ secrets.LD_ACCESS_TOKEN }} - repo-token: ${{ secrets.GITHUB_TOKEN }} + accessToken: ${{ secrets.LD_ACCESS_TOKEN }} + projKey: default - name: Add label if: steps.collect.outputs.any-changed == 'true' From 0fa6962d1784a39734bc41264761eaf75708ae94 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 1 May 2025 13:39:04 -0400 Subject: [PATCH 008/114] Register EF OrganizationInstallationRepository (#5751) --- .../EntityFrameworkServiceCollectionExtensions.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs index ad6c7cf369..c9f0406a58 100644 --- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs @@ -103,6 +103,7 @@ public static class EntityFrameworkServiceCollectionExtensions services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); if (selfHosted) { From 011298c9ff0a873ba696e73ad5bfe0cbc6d32c65 Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Thu, 1 May 2025 19:53:03 +0200 Subject: [PATCH 009/114] PM-16517: Create personal use plan for additional storage (#5205) * PM-16517: Create personal use plan for additional storage * f * f * f * fix * f --------- Co-authored-by: Jonas Hendrickx Co-authored-by: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> --- src/Core/Billing/Constants/StripeConstants.cs | 4 ++++ .../Billing/Models/StaticStore/Plans/Families2019Plan.cs | 2 +- src/Core/Billing/Models/StaticStore/Plans/FamiliesPlan.cs | 2 +- .../Services/Implementations/PremiumUserBillingService.cs | 2 +- src/Core/Services/Implementations/UserService.cs | 6 +++--- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Core/Billing/Constants/StripeConstants.cs b/src/Core/Billing/Constants/StripeConstants.cs index 8a4303e378..b5c2794d22 100644 --- a/src/Core/Billing/Constants/StripeConstants.cs +++ b/src/Core/Billing/Constants/StripeConstants.cs @@ -2,6 +2,10 @@ public static class StripeConstants { + public static class Prices + { + public const string StoragePlanPersonal = "personal-storage-gb-annually"; + } public static class AutomaticTaxStatus { public const string Failed = "failed"; diff --git a/src/Core/Billing/Models/StaticStore/Plans/Families2019Plan.cs b/src/Core/Billing/Models/StaticStore/Plans/Families2019Plan.cs index b0ca8feeb0..93ab2c39a1 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/Families2019Plan.cs +++ b/src/Core/Billing/Models/StaticStore/Plans/Families2019Plan.cs @@ -38,7 +38,7 @@ public record Families2019Plan : Plan HasPremiumAccessOption = true; StripePlanId = "personal-org-annually"; - StripeStoragePlanId = "storage-gb-annually"; + StripeStoragePlanId = "personal-storage-gb-annually"; StripePremiumAccessPlanId = "personal-org-premium-access-annually"; BasePrice = 12; AdditionalStoragePricePerGb = 4; diff --git a/src/Core/Billing/Models/StaticStore/Plans/FamiliesPlan.cs b/src/Core/Billing/Models/StaticStore/Plans/FamiliesPlan.cs index e2f51ec913..8c71e50fa4 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/FamiliesPlan.cs +++ b/src/Core/Billing/Models/StaticStore/Plans/FamiliesPlan.cs @@ -37,7 +37,7 @@ public record FamiliesPlan : Plan HasAdditionalStorageOption = true; StripePlanId = "2020-families-org-annually"; - StripeStoragePlanId = "storage-gb-annually"; + StripeStoragePlanId = "personal-storage-gb-annually"; BasePrice = 40; AdditionalStoragePricePerGb = 4; diff --git a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs index 6746a8cc98..cbd4dbbdff 100644 --- a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs +++ b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs @@ -313,7 +313,7 @@ public class PremiumUserBillingService( { subscriptionItemOptionsList.Add(new SubscriptionItemOptions { - Price = "storage-gb-annually", + Price = StripeConstants.Prices.StoragePlanPersonal, Quantity = storage }); } diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index de0fa427ba..95ee4544fa 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -11,6 +11,7 @@ using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Billing.Constants; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Services; @@ -45,7 +46,6 @@ namespace Bit.Core.Services; public class UserService : UserManager, IUserService, IDisposable { private const string PremiumPlanId = "premium-annually"; - private const string StoragePlanId = "storage-gb-annually"; private readonly IUserRepository _userRepository; private readonly ICipherRepository _cipherRepository; @@ -1106,12 +1106,12 @@ public class UserService : UserManager, IUserService, IDisposable } var secret = await BillingHelpers.AdjustStorageAsync(_paymentService, user, storageAdjustmentGb, - StoragePlanId); + StripeConstants.Prices.StoragePlanPersonal); await _referenceEventService.RaiseEventAsync( new ReferenceEvent(ReferenceEventType.AdjustStorage, user, _currentContext) { Storage = storageAdjustmentGb, - PlanName = StoragePlanId, + PlanName = StripeConstants.Prices.StoragePlanPersonal, }); await SaveUserAsync(user); return secret; From 9da98d8e974b9e57468e1514b5c7820b22c755ed Mon Sep 17 00:00:00 2001 From: Matt Bishop Date: Thu, 1 May 2025 12:25:52 -0700 Subject: [PATCH 010/114] Run LD reference check on all pushes (#5760) * Run LD reference check on all pushes * Fix syntax of code-references.yml --------- Co-authored-by: Matt Andreko --- .github/workflows/code-references.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/code-references.yml b/.github/workflows/code-references.yml index 676a747017..30fbff32ed 100644 --- a/.github/workflows/code-references.yml +++ b/.github/workflows/code-references.yml @@ -1,7 +1,10 @@ name: Collect code references -on: - pull_request: +on: + push: +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true jobs: check-ld-secret: From 41001fefaeacaaf9a63740711f36fee27445fa0d Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Fri, 2 May 2025 07:00:48 +1000 Subject: [PATCH 011/114] Support use of organizationId parameter in authorization (#5758) --- .../Authorization/HttpContextExtensions.cs | 20 ++++++--- .../HttpContextExtensionsTests.cs | 42 ++++++++++++++++++- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/src/Api/AdminConsole/Authorization/HttpContextExtensions.cs b/src/Api/AdminConsole/Authorization/HttpContextExtensions.cs index ba00ea6c18..accb9539fa 100644 --- a/src/Api/AdminConsole/Authorization/HttpContextExtensions.cs +++ b/src/Api/AdminConsole/Authorization/HttpContextExtensions.cs @@ -9,7 +9,7 @@ namespace Bit.Api.AdminConsole.Authorization; public static class HttpContextExtensions { public const string NoOrgIdError = - "A route decorated with with '[Authorize]' must include a route value named 'orgId' either through the [Controller] attribute or through a '[Http*]' attribute."; + "A route decorated with with '[Authorize]' must include a route value named 'orgId' or 'organizationId' either through the [Controller] attribute or through a '[Http*]' attribute."; /// /// Returns the result of the callback, caching it in HttpContext.Features for the lifetime of the request. @@ -61,19 +61,27 @@ public static class HttpContextExtensions /// - /// Parses the {orgId} route parameter into a Guid, or throws if the {orgId} is not present or not a valid guid. + /// Parses the {orgId} or {organizationId} route parameter into a Guid, or throws if neither are present or are not valid guids. /// /// /// /// public static Guid GetOrganizationId(this HttpContext httpContext) { - httpContext.GetRouteData().Values.TryGetValue("orgId", out var orgIdParam); - if (orgIdParam == null || !Guid.TryParse(orgIdParam.ToString(), out var orgId)) + var routeValues = httpContext.GetRouteData().Values; + + routeValues.TryGetValue("orgId", out var orgIdParam); + if (orgIdParam != null && Guid.TryParse(orgIdParam.ToString(), out var orgId)) { - throw new InvalidOperationException(NoOrgIdError); + return orgId; } - return orgId; + routeValues.TryGetValue("organizationId", out var organizationIdParam); + if (organizationIdParam != null && Guid.TryParse(organizationIdParam.ToString(), out var organizationId)) + { + return organizationId; + } + + throw new InvalidOperationException(NoOrgIdError); } } diff --git a/test/Api.Test/AdminConsole/Authorization/HttpContextExtensionsTests.cs b/test/Api.Test/AdminConsole/Authorization/HttpContextExtensionsTests.cs index 1901742777..428726aaac 100644 --- a/test/Api.Test/AdminConsole/Authorization/HttpContextExtensionsTests.cs +++ b/test/Api.Test/AdminConsole/Authorization/HttpContextExtensionsTests.cs @@ -1,5 +1,7 @@ -using Bit.Api.AdminConsole.Authorization; +using AutoFixture.Xunit2; +using Bit.Api.AdminConsole.Authorization; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; using NSubstitute; using Xunit; @@ -25,4 +27,42 @@ public class HttpContextExtensionsTests await callback.ReceivedWithAnyArgs(1).Invoke(); } + [Theory] + [InlineAutoData("orgId")] + [InlineAutoData("organizationId")] + public void GetOrganizationId_GivenValidParameter_ReturnsOrganizationId(string paramName, Guid orgId) + { + var httpContext = new DefaultHttpContext + { + Request = { RouteValues = new RouteValueDictionary + { + { "userId", "someGuid" }, + { paramName, orgId.ToString() } + } + } + }; + + var result = httpContext.GetOrganizationId(); + Assert.Equal(orgId, result); + } + + [Theory] + [InlineAutoData("orgId")] + [InlineAutoData("organizationId")] + [InlineAutoData("missingParameter")] + public void GetOrganizationId_GivenMissingOrInvalidGuid_Throws(string paramName) + { + var httpContext = new DefaultHttpContext + { + Request = { RouteValues = new RouteValueDictionary + { + { "userId", "someGuid" }, + { paramName, "invalidGuid" } + } + } + }; + + var exception = Assert.Throws(() => httpContext.GetOrganizationId()); + Assert.Equal(HttpContextExtensions.NoOrgIdError, exception.Message); + } } From 2d4ec530c5c3638cbc3c7ddb286ba3442cd03014 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 1 May 2025 17:13:10 -0400 Subject: [PATCH 012/114] [PM-18955] Implement `OrganizationWarningsQuery` (#5713) * Add GetWarnings endpoint to OrganizationBillingController * Add OrganizationWarningsQueryTests --- .../OrganizationBillingController.cs | 26 ++ .../OrganizationWarningsResponse.cs | 43 +++ .../OrganizationWarningsQuery.cs | 214 ++++++++++++ src/Api/Billing/Registrations.cs | 11 + src/Api/Startup.cs | 3 + src/Core/Billing/Constants/StripeConstants.cs | 2 + src/Core/Constants.cs | 1 + .../OrganizationWarningsQueryTests.cs | 315 ++++++++++++++++++ 8 files changed, 615 insertions(+) create mode 100644 src/Api/Billing/Models/Responses/Organizations/OrganizationWarningsResponse.cs create mode 100644 src/Api/Billing/Queries/Organizations/OrganizationWarningsQuery.cs create mode 100644 src/Api/Billing/Registrations.cs create mode 100644 test/Api.Test/Billing/Queries/Organizations/OrganizationWarningsQueryTests.cs diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index 1d4ebc1511..2f0a4ef48b 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -2,6 +2,7 @@ using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; +using Bit.Api.Billing.Queries.Organizations; using Bit.Core; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; @@ -24,6 +25,7 @@ public class OrganizationBillingController( IFeatureService featureService, IOrganizationBillingService organizationBillingService, IOrganizationRepository organizationRepository, + IOrganizationWarningsQuery organizationWarningsQuery, IPaymentService paymentService, IPricingClient pricingClient, ISubscriberService subscriberService, @@ -335,4 +337,28 @@ public class OrganizationBillingController( return TypedResults.Ok(providerId); } + + [HttpGet("warnings")] + public async Task GetWarningsAsync([FromRoute] Guid organizationId) + { + /* + * We'll keep these available at the User level, because we're hiding any pertinent information and + * we want to throw as few errors as possible since these are not core features. + */ + if (!await currentContext.OrganizationUser(organizationId)) + { + return Error.Unauthorized(); + } + + var organization = await organizationRepository.GetByIdAsync(organizationId); + + if (organization == null) + { + return Error.NotFound(); + } + + var response = await organizationWarningsQuery.Run(organization); + + return TypedResults.Ok(response); + } } diff --git a/src/Api/Billing/Models/Responses/Organizations/OrganizationWarningsResponse.cs b/src/Api/Billing/Models/Responses/Organizations/OrganizationWarningsResponse.cs new file mode 100644 index 0000000000..e124bdc318 --- /dev/null +++ b/src/Api/Billing/Models/Responses/Organizations/OrganizationWarningsResponse.cs @@ -0,0 +1,43 @@ +#nullable enable +namespace Bit.Api.Billing.Models.Responses.Organizations; + +public record OrganizationWarningsResponse +{ + public FreeTrialWarning? FreeTrial { get; set; } + public InactiveSubscriptionWarning? InactiveSubscription { get; set; } + public ResellerRenewalWarning? ResellerRenewal { get; set; } + + public record FreeTrialWarning + { + public int RemainingTrialDays { get; set; } + } + + public record InactiveSubscriptionWarning + { + public required string Resolution { get; set; } + } + + public record ResellerRenewalWarning + { + public required string Type { get; set; } + public UpcomingRenewal? Upcoming { get; set; } + public IssuedRenewal? Issued { get; set; } + public PastDueRenewal? PastDue { get; set; } + + public record UpcomingRenewal + { + public required DateTime RenewalDate { get; set; } + } + + public record IssuedRenewal + { + public required DateTime IssuedDate { get; set; } + public required DateTime DueDate { get; set; } + } + + public record PastDueRenewal + { + public required DateTime SuspensionDate { get; set; } + } + } +} diff --git a/src/Api/Billing/Queries/Organizations/OrganizationWarningsQuery.cs b/src/Api/Billing/Queries/Organizations/OrganizationWarningsQuery.cs new file mode 100644 index 0000000000..f6a0e5b1e6 --- /dev/null +++ b/src/Api/Billing/Queries/Organizations/OrganizationWarningsQuery.cs @@ -0,0 +1,214 @@ +// ReSharper disable InconsistentNaming + +#nullable enable + +using Bit.Api.Billing.Models.Responses.Organizations; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Services; +using Bit.Core.Context; +using Bit.Core.Services; +using Stripe; +using FreeTrialWarning = Bit.Api.Billing.Models.Responses.Organizations.OrganizationWarningsResponse.FreeTrialWarning; +using InactiveSubscriptionWarning = + Bit.Api.Billing.Models.Responses.Organizations.OrganizationWarningsResponse.InactiveSubscriptionWarning; +using ResellerRenewalWarning = + Bit.Api.Billing.Models.Responses.Organizations.OrganizationWarningsResponse.ResellerRenewalWarning; + +namespace Bit.Api.Billing.Queries.Organizations; + +public interface IOrganizationWarningsQuery +{ + Task Run( + Organization organization); +} + +public class OrganizationWarningsQuery( + ICurrentContext currentContext, + IProviderRepository providerRepository, + IStripeAdapter stripeAdapter, + ISubscriberService subscriberService) : IOrganizationWarningsQuery +{ + public async Task Run( + Organization organization) + { + var response = new OrganizationWarningsResponse(); + + var subscription = + await subscriberService.GetSubscription(organization, + new SubscriptionGetOptions { Expand = ["customer", "latest_invoice", "test_clock"] }); + + if (subscription == null) + { + return response; + } + + response.FreeTrial = await GetFreeTrialWarning(organization, subscription); + + var provider = await providerRepository.GetByOrganizationIdAsync(organization.Id); + + response.InactiveSubscription = await GetInactiveSubscriptionWarning(organization, provider, subscription); + + response.ResellerRenewal = await GetResellerRenewalWarning(provider, subscription); + + return response; + } + + private async Task GetFreeTrialWarning( + Organization organization, + Subscription subscription) + { + if (!await currentContext.EditSubscription(organization.Id)) + { + return null; + } + + if (subscription is not + { + Status: StripeConstants.SubscriptionStatus.Trialing, + TrialEnd: not null, + Customer: not null + }) + { + return null; + } + + var customer = subscription.Customer; + + var hasPaymentMethod = + !string.IsNullOrEmpty(customer.InvoiceSettings.DefaultPaymentMethodId) || + !string.IsNullOrEmpty(customer.DefaultSourceId) || + customer.Metadata.ContainsKey(StripeConstants.MetadataKeys.BraintreeCustomerId); + + if (hasPaymentMethod) + { + return null; + } + + var now = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow; + + var remainingTrialDays = (int)Math.Ceiling((subscription.TrialEnd.Value - now).TotalDays); + + return new FreeTrialWarning { RemainingTrialDays = remainingTrialDays }; + } + + private async Task GetInactiveSubscriptionWarning( + Organization organization, + Provider? provider, + Subscription subscription) + { + if (organization.Enabled || + subscription.Status is not StripeConstants.SubscriptionStatus.Unpaid + and not StripeConstants.SubscriptionStatus.Canceled) + { + return null; + } + + if (provider != null) + { + return new InactiveSubscriptionWarning { Resolution = "contact_provider" }; + } + + if (await currentContext.OrganizationOwner(organization.Id)) + { + return subscription.Status switch + { + StripeConstants.SubscriptionStatus.Unpaid => new InactiveSubscriptionWarning + { + Resolution = "add_payment_method" + }, + StripeConstants.SubscriptionStatus.Canceled => new InactiveSubscriptionWarning + { + Resolution = "resubscribe" + }, + _ => null + }; + } + + return new InactiveSubscriptionWarning { Resolution = "contact_owner" }; + } + + private async Task GetResellerRenewalWarning( + Provider? provider, + Subscription subscription) + { + if (provider is not + { + Type: ProviderType.Reseller + }) + { + return null; + } + + if (subscription.CollectionMethod != StripeConstants.CollectionMethod.SendInvoice) + { + return null; + } + + var now = subscription.TestClock?.FrozenTime ?? DateTime.UtcNow; + + // ReSharper disable once ConvertIfStatementToSwitchStatement + if (subscription is + { + Status: StripeConstants.SubscriptionStatus.Trialing or StripeConstants.SubscriptionStatus.Active, + LatestInvoice: null or { Status: StripeConstants.InvoiceStatus.Paid } + } && (subscription.CurrentPeriodEnd - now).TotalDays <= 14) + { + return new ResellerRenewalWarning + { + Type = "upcoming", + Upcoming = new ResellerRenewalWarning.UpcomingRenewal + { + RenewalDate = subscription.CurrentPeriodEnd + } + }; + } + + if (subscription is + { + Status: StripeConstants.SubscriptionStatus.Active, + LatestInvoice: { Status: StripeConstants.InvoiceStatus.Open, DueDate: not null } + } && subscription.LatestInvoice.DueDate > now) + { + return new ResellerRenewalWarning + { + Type = "issued", + Issued = new ResellerRenewalWarning.IssuedRenewal + { + IssuedDate = subscription.LatestInvoice.Created, + DueDate = subscription.LatestInvoice.DueDate.Value + } + }; + } + + // ReSharper disable once InvertIf + if (subscription.Status == StripeConstants.SubscriptionStatus.PastDue) + { + var openInvoices = await stripeAdapter.InvoiceSearchAsync(new InvoiceSearchOptions + { + Query = $"subscription:'{subscription.Id}' status:'open'" + }); + + var earliestOverdueInvoice = openInvoices + .Where(invoice => invoice.DueDate != null && invoice.DueDate < now) + .MinBy(invoice => invoice.Created); + + if (earliestOverdueInvoice != null) + { + return new ResellerRenewalWarning + { + Type = "past_due", + PastDue = new ResellerRenewalWarning.PastDueRenewal + { + SuspensionDate = earliestOverdueInvoice.DueDate!.Value.AddDays(30) + } + }; + } + } + + return null; + } +} diff --git a/src/Api/Billing/Registrations.cs b/src/Api/Billing/Registrations.cs new file mode 100644 index 0000000000..cb92098333 --- /dev/null +++ b/src/Api/Billing/Registrations.cs @@ -0,0 +1,11 @@ +using Bit.Api.Billing.Queries.Organizations; + +namespace Bit.Api.Billing; + +public static class Registrations +{ + public static void AddBillingQueries(this IServiceCollection services) + { + services.AddTransient(); + } +} diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 40448f722d..1cc371ae1b 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -27,6 +27,7 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions; using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; using Bit.Api.Auth.Models.Request.WebAuthn; +using Bit.Api.Billing; using Bit.Core.AdminConsole.Services.NoopImplementations; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Identity.TokenProviders; @@ -184,6 +185,8 @@ public class Startup services.AddImportServices(); services.AddPhishingDomainServices(globalSettings); + services.AddBillingQueries(); + // Authorization Handlers services.AddAuthorizationHandlers(); diff --git a/src/Core/Billing/Constants/StripeConstants.cs b/src/Core/Billing/Constants/StripeConstants.cs index b5c2794d22..c3e3ec6c30 100644 --- a/src/Core/Billing/Constants/StripeConstants.cs +++ b/src/Core/Billing/Constants/StripeConstants.cs @@ -46,10 +46,12 @@ public static class StripeConstants { public const string Draft = "draft"; public const string Open = "open"; + public const string Paid = "paid"; } public static class MetadataKeys { + public const string BraintreeCustomerId = "btCustomerId"; public const string InvoiceApproved = "invoice_approved"; public const string OrganizationId = "organizationId"; public const string ProviderId = "providerId"; diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 0d4c105b2d..13d0bad495 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -150,6 +150,7 @@ public static class FeatureFlagKeys public const string PM18770_EnableOrganizationBusinessUnitConversion = "pm-18770-enable-organization-business-unit-conversion"; public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically"; public const string PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup"; + public const string UseOrganizationWarningsService = "use-organization-warnings-service"; /* Data Insights and Reporting Team */ public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application"; diff --git a/test/Api.Test/Billing/Queries/Organizations/OrganizationWarningsQueryTests.cs b/test/Api.Test/Billing/Queries/Organizations/OrganizationWarningsQueryTests.cs new file mode 100644 index 0000000000..67979f506e --- /dev/null +++ b/test/Api.Test/Billing/Queries/Organizations/OrganizationWarningsQueryTests.cs @@ -0,0 +1,315 @@ +using Bit.Api.Billing.Queries.Organizations; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Enums.Provider; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Services; +using Bit.Core.Context; +using Bit.Core.Services; +using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ReturnsExtensions; +using Stripe; +using Stripe.TestHelpers; +using Xunit; + +namespace Bit.Api.Test.Billing.Queries.Organizations; + +[SutProviderCustomize] +public class OrganizationWarningsQueryTests +{ + private static readonly string[] _requiredExpansions = ["customer", "latest_invoice", "test_clock"]; + + [Theory, BitAutoData] + public async Task Run_NoSubscription_NoWarnings( + Organization organization, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .ReturnsNull(); + + var response = await sutProvider.Sut.Run(organization); + + Assert.True(response is + { + FreeTrial: null, + InactiveSubscription: null, + ResellerRenewal: null + }); + } + + [Theory, BitAutoData] + public async Task Run_Has_FreeTrialWarning( + Organization organization, + SutProvider sutProvider) + { + var now = DateTime.UtcNow; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = StripeConstants.SubscriptionStatus.Trialing, + TrialEnd = now.AddDays(7), + Customer = new Customer + { + InvoiceSettings = new CustomerInvoiceSettings(), + Metadata = new Dictionary() + }, + TestClock = new TestClock + { + FrozenTime = now + } + }); + + sutProvider.GetDependency().EditSubscription(organization.Id).Returns(true); + + var response = await sutProvider.Sut.Run(organization); + + Assert.True(response is + { + FreeTrial.RemainingTrialDays: 7 + }); + } + + [Theory, BitAutoData] + public async Task Run_Has_InactiveSubscriptionWarning_ContactProvider( + Organization organization, + SutProvider sutProvider) + { + organization.Enabled = false; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = StripeConstants.SubscriptionStatus.Unpaid + }); + + sutProvider.GetDependency().GetByOrganizationIdAsync(organization.Id) + .Returns(new Provider()); + + var response = await sutProvider.Sut.Run(organization); + + Assert.True(response is + { + InactiveSubscription.Resolution: "contact_provider" + }); + } + + [Theory, BitAutoData] + public async Task Run_Has_InactiveSubscriptionWarning_AddPaymentMethod( + Organization organization, + SutProvider sutProvider) + { + organization.Enabled = false; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = StripeConstants.SubscriptionStatus.Unpaid + }); + + sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(true); + + var response = await sutProvider.Sut.Run(organization); + + Assert.True(response is + { + InactiveSubscription.Resolution: "add_payment_method" + }); + } + + [Theory, BitAutoData] + public async Task Run_Has_InactiveSubscriptionWarning_Resubscribe( + Organization organization, + SutProvider sutProvider) + { + organization.Enabled = false; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = StripeConstants.SubscriptionStatus.Canceled + }); + + sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(true); + + var response = await sutProvider.Sut.Run(organization); + + Assert.True(response is + { + InactiveSubscription.Resolution: "resubscribe" + }); + } + + [Theory, BitAutoData] + public async Task Run_Has_InactiveSubscriptionWarning_ContactOwner( + Organization organization, + SutProvider sutProvider) + { + organization.Enabled = false; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Status = StripeConstants.SubscriptionStatus.Unpaid + }); + + sutProvider.GetDependency().OrganizationOwner(organization.Id).Returns(false); + + var response = await sutProvider.Sut.Run(organization); + + Assert.True(response is + { + InactiveSubscription.Resolution: "contact_owner" + }); + } + + [Theory, BitAutoData] + public async Task Run_Has_ResellerRenewalWarning_Upcoming( + Organization organization, + SutProvider sutProvider) + { + var now = DateTime.UtcNow; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + CollectionMethod = StripeConstants.CollectionMethod.SendInvoice, + Status = StripeConstants.SubscriptionStatus.Active, + CurrentPeriodEnd = now.AddDays(10), + TestClock = new TestClock + { + FrozenTime = now + } + }); + + sutProvider.GetDependency().GetByOrganizationIdAsync(organization.Id) + .Returns(new Provider + { + Type = ProviderType.Reseller + }); + + var response = await sutProvider.Sut.Run(organization); + + Assert.True(response is + { + ResellerRenewal.Type: "upcoming" + }); + + Assert.Equal(now.AddDays(10), response.ResellerRenewal.Upcoming!.RenewalDate); + } + + [Theory, BitAutoData] + public async Task Run_Has_ResellerRenewalWarning_Issued( + Organization organization, + SutProvider sutProvider) + { + var now = DateTime.UtcNow; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + CollectionMethod = StripeConstants.CollectionMethod.SendInvoice, + Status = StripeConstants.SubscriptionStatus.Active, + LatestInvoice = new Invoice + { + Status = StripeConstants.InvoiceStatus.Open, + DueDate = now.AddDays(30), + Created = now + }, + TestClock = new TestClock + { + FrozenTime = now + } + }); + + sutProvider.GetDependency().GetByOrganizationIdAsync(organization.Id) + .Returns(new Provider + { + Type = ProviderType.Reseller + }); + + var response = await sutProvider.Sut.Run(organization); + + Assert.True(response is + { + ResellerRenewal.Type: "issued" + }); + + Assert.Equal(now, response.ResellerRenewal.Issued!.IssuedDate); + Assert.Equal(now.AddDays(30), response.ResellerRenewal.Issued!.DueDate); + } + + [Theory, BitAutoData] + public async Task Run_Has_ResellerRenewalWarning_PastDue( + Organization organization, + SutProvider sutProvider) + { + var now = DateTime.UtcNow; + + const string subscriptionId = "subscription_id"; + + sutProvider.GetDependency() + .GetSubscription(organization, Arg.Is(options => + options.Expand.SequenceEqual(_requiredExpansions) + )) + .Returns(new Subscription + { + Id = subscriptionId, + CollectionMethod = StripeConstants.CollectionMethod.SendInvoice, + Status = StripeConstants.SubscriptionStatus.PastDue, + TestClock = new TestClock + { + FrozenTime = now + } + }); + + sutProvider.GetDependency().GetByOrganizationIdAsync(organization.Id) + .Returns(new Provider + { + Type = ProviderType.Reseller + }); + + var dueDate = now.AddDays(-10); + + sutProvider.GetDependency().InvoiceSearchAsync(Arg.Is(options => + options.Query == $"subscription:'{subscriptionId}' status:'open'")).Returns([ + new Invoice { DueDate = dueDate, Created = dueDate.AddDays(-30) } + ]); + + var response = await sutProvider.Sut.Run(organization); + + Assert.True(response is + { + ResellerRenewal.Type: "past_due" + }); + + Assert.Equal(dueDate.AddDays(30), response.ResellerRenewal.PastDue!.SuspensionDate); + } +} From cd3f16948b31367cfbc02f2a9e457cb45581d6cc Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Fri, 2 May 2025 08:25:52 -0400 Subject: [PATCH 013/114] Resolved the ambiguous build error (#5762) --- .../PhishingDomainFeatures/AzurePhishingDomainStorageService.cs | 2 +- .../PhishingDomainFeatures/CloudPhishingDomainDirectQuery.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Core/PhishingDomainFeatures/AzurePhishingDomainStorageService.cs b/src/Core/PhishingDomainFeatures/AzurePhishingDomainStorageService.cs index 9af9c94e1d..0d287a2229 100644 --- a/src/Core/PhishingDomainFeatures/AzurePhishingDomainStorageService.cs +++ b/src/Core/PhishingDomainFeatures/AzurePhishingDomainStorageService.cs @@ -39,7 +39,7 @@ public class AzurePhishingDomainStorageService var content = await streamReader.ReadToEndAsync(); return [.. content - .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries) + .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) .Select(line => line.Trim()) .Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith('#'))]; } diff --git a/src/Core/PhishingDomainFeatures/CloudPhishingDomainDirectQuery.cs b/src/Core/PhishingDomainFeatures/CloudPhishingDomainDirectQuery.cs index b059eac0e8..420948e310 100644 --- a/src/Core/PhishingDomainFeatures/CloudPhishingDomainDirectQuery.cs +++ b/src/Core/PhishingDomainFeatures/CloudPhishingDomainDirectQuery.cs @@ -92,7 +92,7 @@ public class CloudPhishingDomainDirectQuery : ICloudPhishingDomainQuery } return content - .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries) + .Split(new[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries) .Select(line => line.Trim()) .Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith("#")) .ToList(); From 077d0fa6d7268a402dcbe88e574d2c4242cd6c78 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Fri, 2 May 2025 12:53:06 -0400 Subject: [PATCH 014/114] Resolved an issue where autoscaling always happened (#5765) --- .../Services/IOrganizationService.cs | 1 + .../Implementations/OrganizationService.cs | 2 +- .../CreateSponsorshipCommand.cs | 19 ++- .../CreateSponsorshipCommandTests.cs | 127 ++++++++++++++++++ 4 files changed, 145 insertions(+), 4 deletions(-) diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index 9c9e311a02..1e53be734e 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -49,6 +49,7 @@ public interface IOrganizationService IEnumerable organizationUserIds, Guid? revokingUserId); Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted); Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null); + Task<(bool canScale, string failureReason)> CanScaleAsync(Organization organization, int seatsToAdd); void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade); void ValidateSecretsManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade); diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 532aebf5e0..5c7e5e29ed 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -1058,7 +1058,7 @@ public class OrganizationService : IOrganizationService organization: organization, initOrganization: initOrganization)); - internal async Task<(bool canScale, string failureReason)> CanScaleAsync( + public async Task<(bool canScale, string failureReason)> CanScaleAsync( Organization organization, int seatsToAdd) { diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs index 3b74baf6f9..b15cbea240 100644 --- a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs @@ -15,7 +15,8 @@ public class CreateSponsorshipCommand( ICurrentContext currentContext, IOrganizationSponsorshipRepository organizationSponsorshipRepository, IUserService userService, - IOrganizationService organizationService) : ICreateSponsorshipCommand + IOrganizationService organizationService, + IOrganizationUserRepository organizationUserRepository) : ICreateSponsorshipCommand { public async Task CreateSponsorshipAsync( Organization sponsoringOrganization, @@ -82,14 +83,26 @@ public class CreateSponsorshipCommand( if (existingOrgSponsorship != null) { - // Replace existing invalid offer with our new sponsorship offer sponsorship.Id = existingOrgSponsorship.Id; } } if (isAdminInitiated && sponsoringOrganization.Seats.HasValue) { - await organizationService.AutoAddSeatsAsync(sponsoringOrganization, 1); + var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrganization.Id); + var availableSeats = sponsoringOrganization.Seats.Value - occupiedSeats; + + if (availableSeats <= 0) + { + var newSeatsRequired = 1; + var (canScale, failureReason) = await organizationService.CanScaleAsync(sponsoringOrganization, newSeatsRequired); + if (!canScale) + { + throw new BadRequestException(failureReason); + } + + await organizationService.AutoAddSeatsAsync(sponsoringOrganization, newSeatsRequired); + } } try diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommandTests.cs index f6b6721bd2..7dc6b7360d 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommandTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommandTests.cs @@ -168,6 +168,11 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase }); sutProvider.GetDependency().UserId.Returns(sponsoringOrgUser.UserId.Value); + // Setup for checking available seats + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id) + .Returns(0); + await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, false, null); @@ -293,6 +298,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase { sponsoringOrg.PlanType = PlanType.EnterpriseAnnually; sponsoringOrg.UseAdminSponsoredFamilies = true; + sponsoringOrg.Seats = 10; sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed; sutProvider.GetDependency().GetUserByIdAsync(sponsoringOrgUser.UserId!.Value).Returns(user); @@ -311,6 +317,11 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase } ]); + // Setup for checking available seats - organization has plenty of seats + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id) + .Returns(5); + var actual = await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, notes); @@ -331,5 +342,121 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase await sutProvider.GetDependency().Received(1) .CreateAsync(Arg.Is(s => SponsorshipValidator(s, expectedSponsorship))); + + // Verify we didn't need to add seats + await sutProvider.GetDependency().DidNotReceive() + .AutoAddSeatsAsync(Arg.Any(), Arg.Any()); + } + + [Theory] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task CreateSponsorship_CreatesAdminInitiatedSponsorship_AutoscalesWhenNeeded( + OrganizationUserType organizationUserType, + Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, User user, string sponsoredEmail, + string friendlyName, Guid sponsorshipId, Guid currentUserId, string notes, SutProvider sutProvider) + { + sponsoringOrg.PlanType = PlanType.EnterpriseAnnually; + sponsoringOrg.UseAdminSponsoredFamilies = true; + sponsoringOrg.Seats = 10; + sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed; + + sutProvider.GetDependency().GetUserByIdAsync(sponsoringOrgUser.UserId!.Value).Returns(user); + sutProvider.GetDependency().WhenForAnyArgs(x => x.UpsertAsync(null!)).Do(callInfo => + { + var sponsorship = callInfo.Arg(); + sponsorship.Id = sponsorshipId; + }); + sutProvider.GetDependency().UserId.Returns(currentUserId); + sutProvider.GetDependency().Organizations.Returns([ + new() + { + Id = sponsoringOrg.Id, + Permissions = new Permissions { ManageUsers = true }, + Type = organizationUserType + } + ]); + + // Setup for checking available seats - organization has no available seats + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id) + .Returns(10); + + // Setup for checking if can scale + sutProvider.GetDependency() + .CanScaleAsync(sponsoringOrg, 1) + .Returns((true, "")); + + var actual = await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, + PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, notes); + + + var expectedSponsorship = new OrganizationSponsorship + { + Id = sponsorshipId, + SponsoringOrganizationId = sponsoringOrg.Id, + SponsoringOrganizationUserId = sponsoringOrgUser.Id, + FriendlyName = friendlyName, + OfferedToEmail = sponsoredEmail, + PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise, + IsAdminInitiated = true, + Notes = notes + }; + + Assert.True(SponsorshipValidator(expectedSponsorship, actual)); + + await sutProvider.GetDependency().Received(1) + .CreateAsync(Arg.Is(s => SponsorshipValidator(s, expectedSponsorship))); + + // Verify we needed to add seats + await sutProvider.GetDependency().Received(1) + .AutoAddSeatsAsync(sponsoringOrg, 1); + } + + [Theory] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Owner)] + public async Task CreateSponsorship_CreatesAdminInitiatedSponsorship_ThrowsWhenCannotAutoscale( + OrganizationUserType organizationUserType, + Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, User user, string sponsoredEmail, + string friendlyName, Guid sponsorshipId, Guid currentUserId, string notes, SutProvider sutProvider) + { + sponsoringOrg.PlanType = PlanType.EnterpriseAnnually; + sponsoringOrg.UseAdminSponsoredFamilies = true; + sponsoringOrg.Seats = 10; + sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed; + + sutProvider.GetDependency().GetUserByIdAsync(sponsoringOrgUser.UserId!.Value).Returns(user); + sutProvider.GetDependency().WhenForAnyArgs(x => x.UpsertAsync(null!)).Do(callInfo => + { + var sponsorship = callInfo.Arg(); + sponsorship.Id = sponsorshipId; + }); + sutProvider.GetDependency().UserId.Returns(currentUserId); + sutProvider.GetDependency().Organizations.Returns([ + new() + { + Id = sponsoringOrg.Id, + Permissions = new Permissions { ManageUsers = true }, + Type = organizationUserType + } + ]); + + // Setup for checking available seats - organization has no available seats + sutProvider.GetDependency() + .GetOccupiedSeatCountByOrganizationIdAsync(sponsoringOrg.Id) + .Returns(10); + + // Setup for checking if can scale - cannot scale + var failureReason = "Seat limit has been reached."; + sutProvider.GetDependency() + .CanScaleAsync(sponsoringOrg, 1) + .Returns((false, failureReason)); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, + PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, notes)); + + Assert.Equal(failureReason, exception.Message); } } From 9511c26683534e63d7705d79e91be739c05b4d83 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 5 May 2025 10:29:50 +0000 Subject: [PATCH 015/114] Bumped version to 2025.5.0 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 16d8d83ae0..60d61e5e26 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.4.3 + 2025.5.0 Bit.$(MSBuildProjectName) enable From 75a2da3c4bd4debb45be28b7d50743a3726c25c6 Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Mon, 5 May 2025 08:04:59 -0400 Subject: [PATCH 016/114] [PM-17562] Add support for extended properties on event integrations (#5755) * [PM-17562] Add support for extended properties on event integrations * Clean up IntegrationEventHandlerBase * Respond to PR feedback --- .../IntegrationTemplateContext.cs | 37 +++ .../IntegrationEventHandlerBase.cs | 66 ++++++ .../Implementations/SlackEventHandler.cs | 47 ++-- .../Implementations/WebhookEventHandler.cs | 47 ++-- .../Utilities/IntegrationTemplateProcessor.cs | 35 ++- .../IntegrationEventHandlerBaseTests.cs | 219 ++++++++++++++++++ .../IntegrationTemplateProcessorTests.cs | 57 +++++ 7 files changed, 445 insertions(+), 63 deletions(-) create mode 100644 src/Core/AdminConsole/Models/Data/Integrations/IntegrationTemplateContext.cs create mode 100644 src/Core/AdminConsole/Services/Implementations/IntegrationEventHandlerBase.cs create mode 100644 test/Core.Test/AdminConsole/Services/IntegrationEventHandlerBaseTests.cs diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationTemplateContext.cs b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationTemplateContext.cs new file mode 100644 index 0000000000..18aa3b7681 --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationTemplateContext.cs @@ -0,0 +1,37 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Entities; +using Bit.Core.Enums; + +#nullable enable + +namespace Bit.Core.Models.Data.Integrations; + +public class IntegrationTemplateContext(EventMessage eventMessage) +{ + public EventMessage Event { get; } = eventMessage; + + public string DomainName => Event.DomainName; + public string IpAddress => Event.IpAddress; + public DeviceType? DeviceType => Event.DeviceType; + public Guid? ActingUserId => Event.ActingUserId; + public Guid? OrganizationUserId => Event.OrganizationUserId; + public DateTime Date => Event.Date; + public EventType Type => Event.Type; + public Guid? UserId => Event.UserId; + public Guid? OrganizationId => Event.OrganizationId; + public Guid? CipherId => Event.CipherId; + public Guid? CollectionId => Event.CollectionId; + public Guid? GroupId => Event.GroupId; + public Guid? PolicyId => Event.PolicyId; + + public User? User { get; set; } + public string? UserName => User?.Name; + public string? UserEmail => User?.Email; + + public User? ActingUser { get; set; } + public string? ActingUserName => ActingUser?.Name; + public string? ActingUserEmail => ActingUser?.Email; + + public Organization? Organization { get; set; } + public string? OrganizationName => Organization?.DisplayName(); +} diff --git a/src/Core/AdminConsole/Services/Implementations/IntegrationEventHandlerBase.cs b/src/Core/AdminConsole/Services/Implementations/IntegrationEventHandlerBase.cs new file mode 100644 index 0000000000..d8e521de97 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/IntegrationEventHandlerBase.cs @@ -0,0 +1,66 @@ +using System.Text.Json.Nodes; +using Bit.Core.AdminConsole.Utilities; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Integrations; +using Bit.Core.Repositories; + +namespace Bit.Core.Services; + +public abstract class IntegrationEventHandlerBase( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationIntegrationConfigurationRepository configurationRepository) + : IEventMessageHandler +{ + public async Task HandleEventAsync(EventMessage eventMessage) + { + var organizationId = eventMessage.OrganizationId ?? Guid.Empty; + var configurations = await configurationRepository.GetConfigurationDetailsAsync( + organizationId, + GetIntegrationType(), + eventMessage.Type); + + foreach (var configuration in configurations) + { + var context = await BuildContextAsync(eventMessage, configuration.Template); + var renderedTemplate = IntegrationTemplateProcessor.ReplaceTokens(configuration.Template, context); + + await ProcessEventIntegrationAsync(configuration.MergedConfiguration, renderedTemplate); + } + } + + public async Task HandleManyEventsAsync(IEnumerable eventMessages) + { + foreach (var eventMessage in eventMessages) + { + await HandleEventAsync(eventMessage); + } + } + + private async Task BuildContextAsync(EventMessage eventMessage, string template) + { + var context = new IntegrationTemplateContext(eventMessage); + + if (IntegrationTemplateProcessor.TemplateRequiresUser(template) && eventMessage.UserId.HasValue) + { + context.User = await userRepository.GetByIdAsync(eventMessage.UserId.Value); + } + + if (IntegrationTemplateProcessor.TemplateRequiresActingUser(template) && eventMessage.ActingUserId.HasValue) + { + context.ActingUser = await userRepository.GetByIdAsync(eventMessage.ActingUserId.Value); + } + + if (IntegrationTemplateProcessor.TemplateRequiresOrganization(template) && eventMessage.OrganizationId.HasValue) + { + context.Organization = await organizationRepository.GetByIdAsync(eventMessage.OrganizationId.Value); + } + + return context; + } + + protected abstract IntegrationType GetIntegrationType(); + + protected abstract Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration, string renderedTemplate); +} diff --git a/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs b/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs index c81914b708..3ddecc67f4 100644 --- a/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs @@ -1,46 +1,35 @@ using System.Text.Json; -using Bit.Core.AdminConsole.Utilities; +using System.Text.Json.Nodes; using Bit.Core.Enums; -using Bit.Core.Models.Data; using Bit.Core.Models.Data.Integrations; using Bit.Core.Repositories; +#nullable enable + namespace Bit.Core.Services; public class SlackEventHandler( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, IOrganizationIntegrationConfigurationRepository configurationRepository, ISlackService slackService) - : IEventMessageHandler + : IntegrationEventHandlerBase(userRepository, organizationRepository, configurationRepository) { - public async Task HandleEventAsync(EventMessage eventMessage) + protected override IntegrationType GetIntegrationType() => IntegrationType.Slack; + + protected override async Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration, + string renderedTemplate) { - var organizationId = eventMessage.OrganizationId ?? Guid.Empty; - var configurations = await configurationRepository.GetConfigurationDetailsAsync( - organizationId, - IntegrationType.Slack, - eventMessage.Type); - - foreach (var configuration in configurations) + var config = mergedConfiguration.Deserialize(); + if (config is null) { - var config = configuration.MergedConfiguration.Deserialize(); - if (config is null) - { - continue; - } - - await slackService.SendSlackMessageByChannelIdAsync( - config.token, - IntegrationTemplateProcessor.ReplaceTokens(configuration.Template, eventMessage), - config.channelId - ); + return; } - } - public async Task HandleManyEventsAsync(IEnumerable eventMessages) - { - foreach (var eventMessage in eventMessages) - { - await HandleEventAsync(eventMessage); - } + await slackService.SendSlackMessageByChannelIdAsync( + config.token, + renderedTemplate, + config.channelId + ); } } diff --git a/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs b/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs index 1c3b279ee5..ec6924bb3e 100644 --- a/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs @@ -1,8 +1,7 @@ using System.Text; using System.Text.Json; -using Bit.Core.AdminConsole.Utilities; +using System.Text.Json.Nodes; using Bit.Core.Enums; -using Bit.Core.Models.Data; using Bit.Core.Models.Data.Integrations; using Bit.Core.Repositories; @@ -12,46 +11,28 @@ namespace Bit.Core.Services; public class WebhookEventHandler( IHttpClientFactory httpClientFactory, + IUserRepository userRepository, + IOrganizationRepository organizationRepository, IOrganizationIntegrationConfigurationRepository configurationRepository) - : IEventMessageHandler + : IntegrationEventHandlerBase(userRepository, organizationRepository, configurationRepository) { private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName); public const string HttpClientName = "WebhookEventHandlerHttpClient"; - public async Task HandleEventAsync(EventMessage eventMessage) + protected override IntegrationType GetIntegrationType() => IntegrationType.Webhook; + + protected override async Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration, + string renderedTemplate) { - var organizationId = eventMessage.OrganizationId ?? Guid.Empty; - var configurations = await configurationRepository.GetConfigurationDetailsAsync( - organizationId, - IntegrationType.Webhook, - eventMessage.Type); - - foreach (var configuration in configurations) + var config = mergedConfiguration.Deserialize(); + if (config is null || string.IsNullOrEmpty(config.url)) { - var config = configuration.MergedConfiguration.Deserialize(); - if (config is null || string.IsNullOrEmpty(config.url)) - { - continue; - } - - var content = new StringContent( - IntegrationTemplateProcessor.ReplaceTokens(configuration.Template, eventMessage), - Encoding.UTF8, - "application/json" - ); - var response = await _httpClient.PostAsync( - config.url, - content); - response.EnsureSuccessStatusCode(); + return; } - } - public async Task HandleManyEventsAsync(IEnumerable eventMessages) - { - foreach (var eventMessage in eventMessages) - { - await HandleEventAsync(eventMessage); - } + var content = new StringContent(renderedTemplate, Encoding.UTF8, "application/json"); + var response = await _httpClient.PostAsync(config.url, content); + response.EnsureSuccessStatusCode(); } } diff --git a/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs b/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs index 178c0348d9..4fb5c15e63 100644 --- a/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs +++ b/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs @@ -10,8 +10,9 @@ public static partial class IntegrationTemplateProcessor public static string ReplaceTokens(string template, object values) { if (string.IsNullOrEmpty(template) || values == null) + { return template; - + } var type = values.GetType(); return TokenRegex().Replace(template, match => { @@ -20,4 +21,36 @@ public static partial class IntegrationTemplateProcessor return property?.GetValue(values)?.ToString() ?? match.Value; }); } + + public static bool TemplateRequiresUser(string template) + { + if (string.IsNullOrEmpty(template)) + { + return false; + } + + return template.Contains("#UserName#", StringComparison.Ordinal) + || template.Contains("#UserEmail#", StringComparison.Ordinal); + } + + public static bool TemplateRequiresActingUser(string template) + { + if (string.IsNullOrEmpty(template)) + { + return false; + } + + return template.Contains("#ActingUserName#", StringComparison.Ordinal) + || template.Contains("#ActingUserEmail#", StringComparison.Ordinal); + } + + public static bool TemplateRequiresOrganization(string template) + { + if (string.IsNullOrEmpty(template)) + { + return false; + } + + return template.Contains("#OrganizationName#", StringComparison.Ordinal); + } } diff --git a/test/Core.Test/AdminConsole/Services/IntegrationEventHandlerBaseTests.cs b/test/Core.Test/AdminConsole/Services/IntegrationEventHandlerBaseTests.cs new file mode 100644 index 0000000000..e1a2fbff68 --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/IntegrationEventHandlerBaseTests.cs @@ -0,0 +1,219 @@ +using System.Text.Json; +using System.Text.Json.Nodes; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class IntegrationEventHandlerBaseEventHandlerTests +{ + private const string _templateBase = "Date: #Date#, Type: #Type#, UserId: #UserId#"; + private const string _templateWithOrganization = "Org: #OrganizationName#"; + private const string _templateWithUser = "#UserName#, #UserEmail#"; + private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#"; + private const string _url = "https://localhost"; + + private SutProvider GetSutProvider( + List configurations) + { + var configurationRepository = Substitute.For(); + configurationRepository.GetConfigurationDetailsAsync(Arg.Any(), + IntegrationType.Webhook, Arg.Any()).Returns(configurations); + + return new SutProvider() + .SetDependency(configurationRepository) + .Create(); + } + + private static List NoConfigurations() + { + return []; + } + + private static List OneConfiguration(string template) + { + var config = Substitute.For(); + config.Configuration = null; + config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url }); + config.Template = template; + + return [config]; + } + + private static List TwoConfigurations(string template) + { + var config = Substitute.For(); + config.Configuration = null; + config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url }); + config.Template = template; + var config2 = Substitute.For(); + config2.Configuration = null; + config2.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url }); + config2.Template = template; + + return [config, config2]; + } + + [Theory, BitAutoData] + public async Task HandleEventAsync_BaseTemplateNoConfigurations_DoesNothing(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(NoConfigurations()); + + await sutProvider.Sut.HandleEventAsync(eventMessage); + Assert.Empty(sutProvider.Sut.CapturedCalls); + } + + [Theory, BitAutoData] + public async Task HandleEventAsync_BaseTemplateOneConfiguration_CallsProcessEventIntegrationAsync(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateBase)); + + await sutProvider.Sut.HandleEventAsync(eventMessage); + + Assert.Single(sutProvider.Sut.CapturedCalls); + + var expectedTemplate = $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"; + + Assert.Equal(expectedTemplate, sutProvider.Sut.CapturedCalls.Single().RenderedTemplate); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task HandleEventAsync_ActingUserTemplate_LoadsUserFromRepository(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser)); + var user = Substitute.For(); + user.Email = "test@example.com"; + user.Name = "Test"; + + sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(user); + await sutProvider.Sut.HandleEventAsync(eventMessage); + + Assert.Single(sutProvider.Sut.CapturedCalls); + + var expectedTemplate = $"{user.Name}, {user.Email}"; + Assert.Equal(expectedTemplate, sutProvider.Sut.CapturedCalls.Single().RenderedTemplate); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.ActingUserId ?? Guid.Empty); + } + + [Theory, BitAutoData] + public async Task HandleEventAsync_OrganizationTemplate_LoadsOrganizationFromRepository(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization)); + var organization = Substitute.For(); + organization.Name = "Test"; + + sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(organization); + await sutProvider.Sut.HandleEventAsync(eventMessage); + + Assert.Single(sutProvider.Sut.CapturedCalls); + + var expectedTemplate = $"Org: {organization.Name}"; + Assert.Equal(expectedTemplate, sutProvider.Sut.CapturedCalls.Single().RenderedTemplate); + await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.OrganizationId ?? Guid.Empty); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task HandleEventAsync_UserTemplate_LoadsUserFromRepository(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser)); + var user = Substitute.For(); + user.Email = "test@example.com"; + user.Name = "Test"; + + sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(user); + await sutProvider.Sut.HandleEventAsync(eventMessage); + + Assert.Single(sutProvider.Sut.CapturedCalls); + + var expectedTemplate = $"{user.Name}, {user.Email}"; + Assert.Equal(expectedTemplate, sutProvider.Sut.CapturedCalls.Single().RenderedTemplate); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.UserId ?? Guid.Empty); + } + + [Theory, BitAutoData] + public async Task HandleManyEventsAsync_BaseTemplateNoConfigurations_DoesNothing(List eventMessages) + { + var sutProvider = GetSutProvider(NoConfigurations()); + + await sutProvider.Sut.HandleManyEventsAsync(eventMessages); + Assert.Empty(sutProvider.Sut.CapturedCalls); + } + + [Theory, BitAutoData] + public async Task HandleManyEventsAsync_BaseTemplateOneConfiguration_CallsProcessEventIntegrationAsync(List eventMessages) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateBase)); + + await sutProvider.Sut.HandleManyEventsAsync(eventMessages); + + Assert.Equal(eventMessages.Count, sutProvider.Sut.CapturedCalls.Count); + var index = 0; + foreach (var call in sutProvider.Sut.CapturedCalls) + { + var expected = eventMessages[index]; + var expectedTemplate = $"Date: {expected.Date}, Type: {expected.Type}, UserId: {expected.UserId}"; + + Assert.Equal(expectedTemplate, call.RenderedTemplate); + index++; + } + } + + [Theory, BitAutoData] + public async Task HandleManyEventsAsync_BaseTemplateTwoConfigurations_CallsProcessEventIntegrationAsyncMultipleTimes( + List eventMessages) + { + var sutProvider = GetSutProvider(TwoConfigurations(_templateBase)); + + await sutProvider.Sut.HandleManyEventsAsync(eventMessages); + + Assert.Equal(eventMessages.Count * 2, sutProvider.Sut.CapturedCalls.Count); + + var capturedCalls = sutProvider.Sut.CapturedCalls.GetEnumerator(); + foreach (var eventMessage in eventMessages) + { + var expectedTemplate = $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"; + + Assert.True(capturedCalls.MoveNext()); + var call = capturedCalls.Current; + Assert.Equal(expectedTemplate, call.RenderedTemplate); + + Assert.True(capturedCalls.MoveNext()); + call = capturedCalls.Current; + Assert.Equal(expectedTemplate, call.RenderedTemplate); + } + } + + private class TestIntegrationEventHandlerBase : IntegrationEventHandlerBase + { + public TestIntegrationEventHandlerBase(IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationIntegrationConfigurationRepository configurationRepository) + : base(userRepository, organizationRepository, configurationRepository) + { } + + public List<(JsonObject MergedConfiguration, string RenderedTemplate)> CapturedCalls { get; } = new(); + + protected override IntegrationType GetIntegrationType() => IntegrationType.Webhook; + + protected override Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration, string renderedTemplate) + { + CapturedCalls.Add((mergedConfiguration, renderedTemplate)); + return Task.CompletedTask; + } + } +} diff --git a/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs b/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs index 9ab3b592cb..d117b5e999 100644 --- a/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs +++ b/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs @@ -89,4 +89,61 @@ public class IntegrationTemplateProcessorTests Assert.Equal(expected, result); } + + [Theory] + [InlineData("User name is #UserName#")] + [InlineData("Email: #UserEmail#")] + public void TemplateRequiresUser_ContainingKeys_ReturnsTrue(string template) + { + var result = IntegrationTemplateProcessor.TemplateRequiresUser(template); + Assert.True(result); + } + + [Theory] + [InlineData("#UserId#")] // This is on the base class, not fetched, so should be false + [InlineData("No User Tokens")] + [InlineData("")] + public void TemplateRequiresUser_EmptyInputOrNoMatchingKeys_ReturnsFalse(string template) + { + var result = IntegrationTemplateProcessor.TemplateRequiresUser(template); + Assert.False(result); + } + + [Theory] + [InlineData("Acting user is #ActingUserName#")] + [InlineData("Acting user's email is #ActingUserEmail#")] + public void TemplateRequiresActingUser_ContainingKeys_ReturnsTrue(string template) + { + var result = IntegrationTemplateProcessor.TemplateRequiresActingUser(template); + Assert.True(result); + } + + [Theory] + [InlineData("No ActiveUser tokens")] + [InlineData("#ActiveUserId#")] // This is on the base class, not fetched, so should be false + [InlineData("")] + public void TemplateRequiresActingUser_EmptyInputOrNoMatchingKeys_ReturnsFalse(string template) + { + var result = IntegrationTemplateProcessor.TemplateRequiresActingUser(template); + Assert.False(result); + } + + [Theory] + [InlineData("Organization: #OrganizationName#")] + [InlineData("Welcome to #OrganizationName#")] + public void TemplateRequiresOrganization_ContainingKeys_ReturnsTrue(string template) + { + var result = IntegrationTemplateProcessor.TemplateRequiresOrganization(template); + Assert.True(result); + } + + [Theory] + [InlineData("No organization tokens")] + [InlineData("#OrganizationId#")] // This is on the base class, not fetched, so should be false + [InlineData("")] + public void TemplateRequiresOrganization_EmptyInputOrNoMatchingKeys_ReturnsFalse(string template) + { + var result = IntegrationTemplateProcessor.TemplateRequiresOrganization(template); + Assert.False(result); + } } From 4b49b04409bd255b832c30971f368d25408d23d6 Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Mon, 5 May 2025 08:05:38 -0400 Subject: [PATCH 017/114] [PM-17562] Revert event route optimization (#5766) --- .../Implementations/EventRouteService.cs | 34 ++++++++++ src/Events/Startup.cs | 9 +-- .../Utilities/ServiceCollectionExtensions.cs | 9 +-- .../Services/EventRouteServiceTests.cs | 65 +++++++++++++++++++ 4 files changed, 101 insertions(+), 16 deletions(-) create mode 100644 src/Core/AdminConsole/Services/Implementations/EventRouteService.cs create mode 100644 test/Core.Test/AdminConsole/Services/EventRouteServiceTests.cs diff --git a/src/Core/AdminConsole/Services/Implementations/EventRouteService.cs b/src/Core/AdminConsole/Services/Implementations/EventRouteService.cs new file mode 100644 index 0000000000..a542e75a7b --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/EventRouteService.cs @@ -0,0 +1,34 @@ +using Bit.Core.Models.Data; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.Services; + +public class EventRouteService( + [FromKeyedServices("broadcast")] IEventWriteService broadcastEventWriteService, + [FromKeyedServices("storage")] IEventWriteService storageEventWriteService, + IFeatureService _featureService) : IEventWriteService +{ + public async Task CreateAsync(IEvent e) + { + if (_featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations)) + { + await broadcastEventWriteService.CreateAsync(e); + } + else + { + await storageEventWriteService.CreateAsync(e); + } + } + + public async Task CreateManyAsync(IEnumerable e) + { + if (_featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations)) + { + await broadcastEventWriteService.CreateManyAsync(e); + } + else + { + await storageEventWriteService.CreateManyAsync(e); + } + } +} diff --git a/src/Events/Startup.cs b/src/Events/Startup.cs index bb37e240c8..366b562485 100644 --- a/src/Events/Startup.cs +++ b/src/Events/Startup.cs @@ -1,5 +1,4 @@ using System.Globalization; -using Bit.Core; using Bit.Core.AdminConsole.Services.Implementations; using Bit.Core.AdminConsole.Services.NoopImplementations; using Bit.Core.Context; @@ -94,13 +93,7 @@ public class Startup services.AddKeyedSingleton("broadcast"); } } - services.AddScoped(sp => - { - var featureService = sp.GetRequiredService(); - var key = featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations) - ? "broadcast" : "storage"; - return sp.GetRequiredKeyedService(key); - }); + services.AddScoped(); services.AddScoped(); services.AddOptionality(); diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 9883e6db47..26e5c7abaf 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -4,7 +4,6 @@ using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using AspNetCoreRateLimit; using Azure.Storage.Queues; -using Bit.Core; using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.Services; @@ -366,13 +365,7 @@ public static class ServiceCollectionExtensions services.AddKeyedSingleton("storage"); services.AddKeyedSingleton("broadcast"); } - services.AddScoped(sp => - { - var featureService = sp.GetRequiredService(); - var key = featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations) - ? "broadcast" : "storage"; - return sp.GetRequiredKeyedService(key); - }); + services.AddScoped(); if (CoreHelpers.SettingHasValue(globalSettings.Attachment.ConnectionString)) { diff --git a/test/Core.Test/AdminConsole/Services/EventRouteServiceTests.cs b/test/Core.Test/AdminConsole/Services/EventRouteServiceTests.cs new file mode 100644 index 0000000000..f593a4628b --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/EventRouteServiceTests.cs @@ -0,0 +1,65 @@ +using Bit.Core.Models.Data; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class EventRouteServiceTests +{ + private readonly IEventWriteService _broadcastEventWriteService = Substitute.For(); + private readonly IEventWriteService _storageEventWriteService = Substitute.For(); + private readonly IFeatureService _featureService = Substitute.For(); + private readonly EventRouteService Subject; + + public EventRouteServiceTests() + { + Subject = new EventRouteService(_broadcastEventWriteService, _storageEventWriteService, _featureService); + } + + [Theory, BitAutoData] + public async Task CreateAsync_FlagDisabled_EventSentToStorageService(EventMessage eventMessage) + { + _featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations).Returns(false); + + await Subject.CreateAsync(eventMessage); + + _broadcastEventWriteService.DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any()); + _storageEventWriteService.Received(1).CreateAsync(eventMessage); + } + + [Theory, BitAutoData] + public async Task CreateAsync_FlagEnabled_EventSentToBroadcastService(EventMessage eventMessage) + { + _featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations).Returns(true); + + await Subject.CreateAsync(eventMessage); + + _broadcastEventWriteService.Received(1).CreateAsync(eventMessage); + _storageEventWriteService.DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task CreateManyAsync_FlagDisabled_EventsSentToStorageService(IEnumerable eventMessages) + { + _featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations).Returns(false); + + await Subject.CreateManyAsync(eventMessages); + + _broadcastEventWriteService.DidNotReceiveWithAnyArgs().CreateManyAsync(Arg.Any>()); + _storageEventWriteService.Received(1).CreateManyAsync(eventMessages); + } + + [Theory, BitAutoData] + public async Task CreateManyAsync_FlagEnabled_EventsSentToBroadcastService(IEnumerable eventMessages) + { + _featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations).Returns(true); + + await Subject.CreateManyAsync(eventMessages); + + _broadcastEventWriteService.Received(1).CreateManyAsync(eventMessages); + _storageEventWriteService.DidNotReceiveWithAnyArgs().CreateManyAsync(Arg.Any>()); + } +} From 7fe022e26fce3c3f032757e832df50e9478e6658 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Mon, 5 May 2025 09:48:43 -0400 Subject: [PATCH 018/114] Add SmMaxProjects to OrganizationLicense (#5678) * Add SmMaxProjects to OrganizationLicense * Run dotnet format --- .../Queries/Projects/MaxProjectsQuery.cs | 64 ++++++-- .../Queries/Projects/MaxProjectsQueryTests.cs | 150 +++++++++++++++++- src/Core/Billing/Licenses/LicenseConstants.cs | 1 + .../Billing/Licenses/Models/LicenseContext.cs | 1 + .../OrganizationLicenseClaimsFactory.cs | 5 + .../Cloud/CloudGetOrganizationLicenseQuery.cs | 12 +- src/Core/Services/ILicensingService.cs | 3 +- .../Implementations/LicensingService.cs | 3 +- .../NoopLicensingService.cs | 2 +- .../CloudGetOrganizationLicenseQueryTests.cs | 6 +- 10 files changed, 226 insertions(+), 21 deletions(-) diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/Projects/MaxProjectsQuery.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/Projects/MaxProjectsQuery.cs index d9a7d4a2ce..394e8aa9bc 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/Projects/MaxProjectsQuery.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/Projects/MaxProjectsQuery.cs @@ -1,9 +1,14 @@ -using Bit.Core.Billing.Enums; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Licenses; +using Bit.Core.Billing.Licenses.Extensions; +using Bit.Core.Billing.Pricing; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.SecretsManager.Queries.Projects.Interfaces; using Bit.Core.SecretsManager.Repositories; -using Bit.Core.Utilities; +using Bit.Core.Services; +using Bit.Core.Settings; namespace Bit.Commercial.Core.SecretsManager.Queries.Projects; @@ -11,13 +16,22 @@ public class MaxProjectsQuery : IMaxProjectsQuery { private readonly IOrganizationRepository _organizationRepository; private readonly IProjectRepository _projectRepository; + private readonly IGlobalSettings _globalSettings; + private readonly ILicensingService _licensingService; + private readonly IPricingClient _pricingClient; public MaxProjectsQuery( IOrganizationRepository organizationRepository, - IProjectRepository projectRepository) + IProjectRepository projectRepository, + IGlobalSettings globalSettings, + ILicensingService licensingService, + IPricingClient pricingClient) { _organizationRepository = organizationRepository; _projectRepository = projectRepository; + _globalSettings = globalSettings; + _licensingService = licensingService; + _pricingClient = pricingClient; } public async Task<(short? max, bool? overMax)> GetByOrgIdAsync(Guid organizationId, int projectsToAdd) @@ -28,19 +42,47 @@ public class MaxProjectsQuery : IMaxProjectsQuery throw new NotFoundException(); } - // TODO: PRICING -> https://bitwarden.atlassian.net/browse/PM-17122 - var plan = StaticStore.GetPlan(org.PlanType); - if (plan?.SecretsManager == null) + var (planType, maxProjects) = await GetPlanTypeAndMaxProjectsAsync(org); + + if (planType != PlanType.Free) { - throw new BadRequestException("Existing plan not found."); + return (null, null); } - if (plan.Type == PlanType.Free) + var projects = await _projectRepository.GetProjectCountByOrganizationIdAsync(organizationId); + return ((short? max, bool? overMax))(projects + projectsToAdd > maxProjects ? (maxProjects, true) : (maxProjects, false)); + } + + private async Task<(PlanType planType, int maxProjects)> GetPlanTypeAndMaxProjectsAsync(Organization organization) + { + if (_globalSettings.SelfHosted) { - var projects = await _projectRepository.GetProjectCountByOrganizationIdAsync(organizationId); - return ((short? max, bool? overMax))(projects + projectsToAdd > plan.SecretsManager.MaxProjects ? (plan.SecretsManager.MaxProjects, true) : (plan.SecretsManager.MaxProjects, false)); + var license = await _licensingService.ReadOrganizationLicenseAsync(organization); + + if (license == null) + { + throw new BadRequestException("License not found."); + } + + var claimsPrincipal = _licensingService.GetClaimsPrincipalFromLicense(license); + var maxProjects = claimsPrincipal.GetValue(OrganizationLicenseConstants.SmMaxProjects); + + if (!maxProjects.HasValue) + { + throw new BadRequestException("License does not contain a value for max Secrets Manager projects"); + } + + var planType = claimsPrincipal.GetValue(OrganizationLicenseConstants.PlanType); + return (planType, maxProjects.Value); } - return (null, null); + var plan = await _pricingClient.GetPlan(organization.PlanType); + + if (plan is { SupportsSecretsManager: true }) + { + return (plan.Type, plan.SecretsManager.MaxProjects); + } + + throw new BadRequestException("Existing plan not found."); } } diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/Projects/MaxProjectsQueryTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/Projects/MaxProjectsQueryTests.cs index 347f5b2128..158463fcfa 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/Projects/MaxProjectsQueryTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/Projects/MaxProjectsQueryTests.cs @@ -1,9 +1,16 @@ -using Bit.Commercial.Core.SecretsManager.Queries.Projects; +using System.Security.Claims; +using Bit.Commercial.Core.SecretsManager.Queries.Projects; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Licenses; +using Bit.Core.Billing.Pricing; using Bit.Core.Exceptions; +using Bit.Core.Models.Business; using Bit.Core.Repositories; using Bit.Core.SecretsManager.Repositories; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -32,7 +39,7 @@ public class MaxProjectsQueryTests [BitAutoData(PlanType.FamiliesAnnually2019)] [BitAutoData(PlanType.Custom)] [BitAutoData(PlanType.FamiliesAnnually)] - public async Task GetByOrgIdAsync_SmPlanIsNull_ThrowsBadRequest(PlanType planType, + public async Task GetByOrgIdAsync_Cloud_SmPlanIsNull_ThrowsBadRequest(PlanType planType, SutProvider sutProvider, Organization organization) { organization.PlanType = planType; @@ -40,6 +47,34 @@ public class MaxProjectsQueryTests .GetByIdAsync(organization.Id) .Returns(organization); + sutProvider.GetDependency().SelfHosted.Returns(false); + var plan = StaticStore.GetPlan(planType); + sutProvider.GetDependency().GetPlan(organization.PlanType).Returns(plan); + + await Assert.ThrowsAsync( + async () => await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1)); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .GetProjectCountByOrganizationIdAsync(organization.Id); + } + + [Theory] + [BitAutoData] + public async Task GetByOrgIdAsync_SelfHosted_NoMaxProjectsClaim_ThrowsBadRequest( + SutProvider sutProvider, Organization organization) + { + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + + sutProvider.GetDependency().SelfHosted.Returns(true); + + var license = new OrganizationLicense(); + var claimsPrincipal = new ClaimsPrincipal(); + sutProvider.GetDependency().ReadOrganizationLicenseAsync(organization).Returns(license); + sutProvider.GetDependency().GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal); + await Assert.ThrowsAsync( async () => await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1)); @@ -62,12 +97,58 @@ public class MaxProjectsQueryTests [BitAutoData(PlanType.EnterpriseAnnually2019)] [BitAutoData(PlanType.EnterpriseAnnually2020)] [BitAutoData(PlanType.EnterpriseAnnually)] - public async Task GetByOrgIdAsync_SmNoneFreePlans_ReturnsNull(PlanType planType, + public async Task GetByOrgIdAsync_Cloud_SmNoneFreePlans_ReturnsNull(PlanType planType, SutProvider sutProvider, Organization organization) { organization.PlanType = planType; sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + sutProvider.GetDependency().SelfHosted.Returns(false); + var plan = StaticStore.GetPlan(planType); + sutProvider.GetDependency().GetPlan(organization.PlanType).Returns(plan); + + var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1); + + Assert.Null(limit); + Assert.Null(overLimit); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .GetProjectCountByOrganizationIdAsync(organization.Id); + } + + [Theory] + [BitAutoData(PlanType.TeamsMonthly2019)] + [BitAutoData(PlanType.TeamsMonthly2020)] + [BitAutoData(PlanType.TeamsMonthly)] + [BitAutoData(PlanType.TeamsAnnually2019)] + [BitAutoData(PlanType.TeamsAnnually2020)] + [BitAutoData(PlanType.TeamsAnnually)] + [BitAutoData(PlanType.TeamsStarter)] + [BitAutoData(PlanType.EnterpriseMonthly2019)] + [BitAutoData(PlanType.EnterpriseMonthly2020)] + [BitAutoData(PlanType.EnterpriseMonthly)] + [BitAutoData(PlanType.EnterpriseAnnually2019)] + [BitAutoData(PlanType.EnterpriseAnnually2020)] + [BitAutoData(PlanType.EnterpriseAnnually)] + public async Task GetByOrgIdAsync_SelfHosted_SmNoneFreePlans_ReturnsNull(PlanType planType, + SutProvider sutProvider, Organization organization) + { + organization.PlanType = planType; + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + sutProvider.GetDependency().SelfHosted.Returns(true); + + var license = new OrganizationLicense(); + var plan = StaticStore.GetPlan(planType); + var claims = new List + { + new (nameof(OrganizationLicenseConstants.PlanType), organization.PlanType.ToString()), + new (nameof(OrganizationLicenseConstants.SmMaxProjects), plan.SecretsManager.MaxProjects.ToString()) + }; + var identity = new ClaimsIdentity(claims, "TestAuthenticationType"); + var claimsPrincipal = new ClaimsPrincipal(identity); + sutProvider.GetDependency().ReadOrganizationLicenseAsync(organization).Returns(license); + sutProvider.GetDependency().GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal); + var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1); Assert.Null(limit); @@ -102,7 +183,7 @@ public class MaxProjectsQueryTests [BitAutoData(PlanType.Free, 3, 4, true)] [BitAutoData(PlanType.Free, 4, 4, true)] [BitAutoData(PlanType.Free, 40, 4, true)] - public async Task GetByOrgIdAsync_SmFreePlan__Success(PlanType planType, int projects, int projectsToAdd, bool expectedOverMax, + public async Task GetByOrgIdAsync_Cloud_SmFreePlan__Success(PlanType planType, int projects, int projectsToAdd, bool expectedOverMax, SutProvider sutProvider, Organization organization) { organization.PlanType = planType; @@ -110,6 +191,67 @@ public class MaxProjectsQueryTests sutProvider.GetDependency().GetProjectCountByOrganizationIdAsync(organization.Id) .Returns(projects); + sutProvider.GetDependency().SelfHosted.Returns(false); + var plan = StaticStore.GetPlan(planType); + sutProvider.GetDependency().GetPlan(organization.PlanType).Returns(plan); + + var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd); + + Assert.NotNull(max); + Assert.NotNull(overMax); + Assert.Equal(3, max.Value); + Assert.Equal(expectedOverMax, overMax); + + await sutProvider.GetDependency().Received(1) + .GetProjectCountByOrganizationIdAsync(organization.Id); + } + + [Theory] + [BitAutoData(PlanType.Free, 0, 1, false)] + [BitAutoData(PlanType.Free, 1, 1, false)] + [BitAutoData(PlanType.Free, 2, 1, false)] + [BitAutoData(PlanType.Free, 3, 1, true)] + [BitAutoData(PlanType.Free, 4, 1, true)] + [BitAutoData(PlanType.Free, 40, 1, true)] + [BitAutoData(PlanType.Free, 0, 2, false)] + [BitAutoData(PlanType.Free, 1, 2, false)] + [BitAutoData(PlanType.Free, 2, 2, true)] + [BitAutoData(PlanType.Free, 3, 2, true)] + [BitAutoData(PlanType.Free, 4, 2, true)] + [BitAutoData(PlanType.Free, 40, 2, true)] + [BitAutoData(PlanType.Free, 0, 3, false)] + [BitAutoData(PlanType.Free, 1, 3, true)] + [BitAutoData(PlanType.Free, 2, 3, true)] + [BitAutoData(PlanType.Free, 3, 3, true)] + [BitAutoData(PlanType.Free, 4, 3, true)] + [BitAutoData(PlanType.Free, 40, 3, true)] + [BitAutoData(PlanType.Free, 0, 4, true)] + [BitAutoData(PlanType.Free, 1, 4, true)] + [BitAutoData(PlanType.Free, 2, 4, true)] + [BitAutoData(PlanType.Free, 3, 4, true)] + [BitAutoData(PlanType.Free, 4, 4, true)] + [BitAutoData(PlanType.Free, 40, 4, true)] + public async Task GetByOrgIdAsync_SelfHosted_SmFreePlan__Success(PlanType planType, int projects, int projectsToAdd, bool expectedOverMax, + SutProvider sutProvider, Organization organization) + { + organization.PlanType = planType; + sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); + sutProvider.GetDependency().GetProjectCountByOrganizationIdAsync(organization.Id) + .Returns(projects); + sutProvider.GetDependency().SelfHosted.Returns(true); + + var license = new OrganizationLicense(); + var plan = StaticStore.GetPlan(planType); + var claims = new List + { + new (nameof(OrganizationLicenseConstants.PlanType), organization.PlanType.ToString()), + new (nameof(OrganizationLicenseConstants.SmMaxProjects), plan.SecretsManager.MaxProjects.ToString()) + }; + var identity = new ClaimsIdentity(claims, "TestAuthenticationType"); + var claimsPrincipal = new ClaimsPrincipal(identity); + sutProvider.GetDependency().ReadOrganizationLicenseAsync(organization).Returns(license); + sutProvider.GetDependency().GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal); + var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd); Assert.NotNull(max); diff --git a/src/Core/Billing/Licenses/LicenseConstants.cs b/src/Core/Billing/Licenses/LicenseConstants.cs index 513578f43e..8ef896d6f9 100644 --- a/src/Core/Billing/Licenses/LicenseConstants.cs +++ b/src/Core/Billing/Licenses/LicenseConstants.cs @@ -34,6 +34,7 @@ public static class OrganizationLicenseConstants public const string UseSecretsManager = nameof(UseSecretsManager); public const string SmSeats = nameof(SmSeats); public const string SmServiceAccounts = nameof(SmServiceAccounts); + public const string SmMaxProjects = nameof(SmMaxProjects); public const string LimitCollectionCreationDeletion = nameof(LimitCollectionCreationDeletion); public const string AllowAdminAccessToAllCollectionItems = nameof(AllowAdminAccessToAllCollectionItems); public const string UseRiskInsights = nameof(UseRiskInsights); diff --git a/src/Core/Billing/Licenses/Models/LicenseContext.cs b/src/Core/Billing/Licenses/Models/LicenseContext.cs index 8dcc24e939..01eb3ac80c 100644 --- a/src/Core/Billing/Licenses/Models/LicenseContext.cs +++ b/src/Core/Billing/Licenses/Models/LicenseContext.cs @@ -7,4 +7,5 @@ public class LicenseContext { public Guid? InstallationId { get; init; } public required SubscriptionInfo SubscriptionInfo { get; init; } + public int? SmMaxProjects { get; set; } } diff --git a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs index 6819d3cc0b..7406ac16d9 100644 --- a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs +++ b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs @@ -112,6 +112,11 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory GetLicenseAsync(Organization organization, Guid installationId, @@ -42,7 +46,11 @@ public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuer var subscriptionInfo = await GetSubscriptionAsync(organization); var license = new OrganizationLicense(organization, subscriptionInfo, installationId, _licensingService, version); - license.Token = await _licensingService.CreateOrganizationTokenAsync(organization, installationId, subscriptionInfo); + var plan = await _pricingClient.GetPlan(organization.PlanType); + int? smMaxProjects = plan?.SupportsSecretsManager ?? false + ? plan.SecretsManager.MaxProjects + : null; + license.Token = await _licensingService.CreateOrganizationTokenAsync(organization, installationId, subscriptionInfo, smMaxProjects); return license; } diff --git a/src/Core/Services/ILicensingService.cs b/src/Core/Services/ILicensingService.cs index 2115e43085..9c497ed538 100644 --- a/src/Core/Services/ILicensingService.cs +++ b/src/Core/Services/ILicensingService.cs @@ -21,7 +21,8 @@ public interface ILicensingService Task CreateOrganizationTokenAsync( Organization organization, Guid installationId, - SubscriptionInfo subscriptionInfo); + SubscriptionInfo subscriptionInfo, + int? smMaxProjects); Task CreateUserTokenAsync(User user, SubscriptionInfo subscriptionInfo); } diff --git a/src/Core/Services/Implementations/LicensingService.cs b/src/Core/Services/Implementations/LicensingService.cs index dd603b4b63..e3509bc964 100644 --- a/src/Core/Services/Implementations/LicensingService.cs +++ b/src/Core/Services/Implementations/LicensingService.cs @@ -339,12 +339,13 @@ public class LicensingService : ILicensingService } } - public async Task CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo) + public async Task CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo, int? smMaxProjects) { var licenseContext = new LicenseContext { InstallationId = installationId, SubscriptionInfo = subscriptionInfo, + SmMaxProjects = smMaxProjects }; var claims = await _organizationLicenseClaimsFactory.GenerateClaims(organization, licenseContext); diff --git a/src/Core/Services/NoopImplementations/NoopLicensingService.cs b/src/Core/Services/NoopImplementations/NoopLicensingService.cs index b181e61138..de5e954d44 100644 --- a/src/Core/Services/NoopImplementations/NoopLicensingService.cs +++ b/src/Core/Services/NoopImplementations/NoopLicensingService.cs @@ -62,7 +62,7 @@ public class NoopLicensingService : ILicensingService return null; } - public Task CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo) + public Task CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo, int? smMaxProjects) { return Task.FromResult(null); } diff --git a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs index cc8ab956ca..7af9044c80 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Pricing; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; @@ -8,6 +9,7 @@ using Bit.Core.OrganizationFeatures.OrganizationLicenses; using Bit.Core.Platform.Installations; using Bit.Core.Services; using Bit.Core.Test.AutoFixture; +using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -76,8 +78,10 @@ public class CloudGetOrganizationLicenseQueryTests sutProvider.GetDependency().GetByIdAsync(installationId).Returns(installation); sutProvider.GetDependency().GetSubscriptionAsync(organization).Returns(subInfo); sutProvider.GetDependency().SignLicense(Arg.Any()).Returns(licenseSignature); + var plan = StaticStore.GetPlan(organization.PlanType); + sutProvider.GetDependency().GetPlan(organization.PlanType).Returns(plan); sutProvider.GetDependency() - .CreateOrganizationTokenAsync(organization, installationId, subInfo) + .CreateOrganizationTokenAsync(organization, installationId, subInfo, plan.SecretsManager.MaxProjects) .Returns(token); var result = await sutProvider.Sut.GetLicenseAsync(organization, installationId); From 887fa463741c36c6a8770f55a13e04d676be5491 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Mon, 5 May 2025 16:17:31 +0100 Subject: [PATCH 019/114] Resolve the send email bug (#5763) --- .../OrganizationSponsorshipsController.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs index 9a328081fe..b007c05730 100644 --- a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs @@ -116,7 +116,7 @@ public class OrganizationSponsorshipsController : Controller [Authorize("Application")] [HttpPost("{sponsoringOrgId}/families-for-enterprise/resend")] [SelfHosted(NotSelfHostedOnly = true)] - public async Task ResendSponsorshipOffer(Guid sponsoringOrgId) + public async Task ResendSponsorshipOffer(Guid sponsoringOrgId, [FromQuery] string sponsoredFriendlyName) { var freeFamiliesSponsorshipPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(sponsoringOrgId, PolicyType.FreeFamiliesSponsorshipPolicy); @@ -129,11 +129,14 @@ public class OrganizationSponsorshipsController : Controller var sponsoringOrgUser = await _organizationUserRepository .GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default); - await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync( - await _organizationRepository.GetByIdAsync(sponsoringOrgId), - sponsoringOrgUser, - await _organizationSponsorshipRepository - .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id)); + var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId); + var filteredSponsorship = sponsorships.FirstOrDefault(s => s.FriendlyName != null && s.FriendlyName.Equals(sponsoredFriendlyName, StringComparison.OrdinalIgnoreCase)); + if (filteredSponsorship != null) + { + await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync( + await _organizationRepository.GetByIdAsync(sponsoringOrgId), + sponsoringOrgUser, filteredSponsorship); + } } [Authorize("Application")] From e2f0ddf373b0669b987eeadac3c9fc7480ce4cbe Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Mon, 5 May 2025 13:36:43 -0400 Subject: [PATCH 020/114] [PM-19383] add admin endpoint, fix typecasting error (#5681) * add admin endpoint, fix typecasting error * fix typecast issue * wip * cleanup --- .../Vault/Controllers/CiphersController.cs | 21 +++++++++++++++---- .../Services/Implementations/CipherService.cs | 8 +++---- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 3bdb6c4bf0..03b83e3de2 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -1241,6 +1241,20 @@ public class CiphersController : Controller return new CipherMiniResponseModel(cipher, _globalSettings, cipher.OrganizationUseTotp); } + [HttpGet("{id}/attachment/{attachmentId}/admin")] + public async Task GetAttachmentDataAdmin(Guid id, string attachmentId) + { + var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(id); + if (cipher == null || !cipher.OrganizationId.HasValue || + !await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id })) + { + throw new NotFoundException(); + } + + var result = await _cipherService.GetAttachmentDownloadDataAsync(cipher, attachmentId); + return new AttachmentResponseModel(result); + } + [HttpGet("{id}/attachment/{attachmentId}")] public async Task GetAttachmentData(Guid id, string attachmentId) { @@ -1287,18 +1301,17 @@ public class CiphersController : Controller [HttpDelete("{id}/attachment/{attachmentId}/admin")] [HttpPost("{id}/attachment/{attachmentId}/delete-admin")] - public async Task DeleteAttachmentAdmin(string id, string attachmentId) + public async Task DeleteAttachmentAdmin(Guid id, string attachmentId) { - var idGuid = new Guid(id); var userId = _userService.GetProperUserId(User).Value; - var cipher = await _cipherRepository.GetByIdAsync(idGuid); + var cipher = await _cipherRepository.GetByIdAsync(id); if (cipher == null || !cipher.OrganizationId.HasValue || !await CanEditCipherAsAdminAsync(cipher.OrganizationId.Value, new[] { cipher.Id })) { throw new NotFoundException(); } - await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, true); + return await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, true); } [AllowAnonymous] diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index 745d90b741..73212ab72e 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -379,7 +379,7 @@ public class CipherService : ICipherService if (!valid || realSize > MAX_FILE_SIZE) { // File reported differs in size from that promised. Must be a rogue client. Delete Send - await DeleteAttachmentAsync(cipher, attachmentData); + await DeleteAttachmentAsync(cipher, attachmentData, false); return false; } // Update Send data if necessary @@ -483,7 +483,7 @@ public class CipherService : ICipherService throw new NotFoundException(); } - return await DeleteAttachmentAsync(cipher, cipher.GetAttachments()[attachmentId]); + return await DeleteAttachmentAsync(cipher, cipher.GetAttachments()[attachmentId], orgAdmin); } public async Task PurgeAsync(Guid organizationId) @@ -877,7 +877,7 @@ public class CipherService : ICipherService } } - private async Task DeleteAttachmentAsync(Cipher cipher, CipherAttachment.MetaData attachmentData) + private async Task DeleteAttachmentAsync(Cipher cipher, CipherAttachment.MetaData attachmentData, bool orgAdmin) { if (attachmentData == null || string.IsNullOrWhiteSpace(attachmentData.AttachmentId)) { @@ -891,7 +891,7 @@ public class CipherService : ICipherService // Update the revision date when an attachment is deleted cipher.RevisionDate = DateTime.UtcNow; - await _cipherRepository.ReplaceAsync((CipherDetails)cipher); + await _cipherRepository.ReplaceAsync(orgAdmin ? cipher : (CipherDetails)cipher); // push await _pushService.PushSyncCipherUpdateAsync(cipher, null); From 10fcff58b223a6b78c85bc9974e732f9de7694ac Mon Sep 17 00:00:00 2001 From: Graham Walker Date: Tue, 6 May 2025 03:48:40 -0500 Subject: [PATCH 021/114] PM-19715 & PM-19712 Move Files to DIRT ownership on Server (#5769) * PM-19715 PM-19711 moving reports to dirt directory and adding dirt as codeowners * PM-19715 creating two sub folders for reports and events * PM-19714 changing dirt paths for codeowners * PM-19714 fixing codeowners file * PM-19714 fixing codeowners * PM-19714 moving hibpController to dirt ownership * PM-19715 moving controller --- .github/CODEOWNERS | 8 ++++++++ src/Api/{Tools => Dirt}/Controllers/HibpController.cs | 0 src/Api/{Tools => Dirt}/Controllers/ReportsController.cs | 0 .../Models/PasswordHealthReportApplicationModel.cs | 0 .../Models/Response/MemberAccessReportModel.cs | 0 .../Models/Response/MemberCipherDetailsResponseModel.cs | 0 .../Reports}/Entities/PasswordHealthReportApplication.cs | 0 .../Reports}/Models/Data/MemberAccessCipherDetails.cs | 0 .../AddPasswordHealthReportApplicationCommand.cs | 0 .../DropPasswordHealthReportApplicationCommand.cs | 0 .../GetPasswordHealthReportApplicationQuery.cs | 0 .../IAddPasswordHealthReportApplicationCommand.cs | 0 .../IDropPasswordHealthReportApplicationCommand.cs | 0 .../IGetPasswordHealthReportApplicationQuery.cs | 0 .../ReportFeatures/MemberAccessCipherDetailsQuery.cs | 0 .../Interfaces/IMemberAccessCipherDetailsQuery.cs | 0 .../ReportingServiceCollectionExtensions.cs | 0 .../Requests/AddPasswordHealthReportApplicationRequest.cs | 0 .../DropPasswordHealthReportApplicationRequest.cs | 0 .../Requests/MemberAccessCipherDetailsRequest.cs | 0 .../IPasswordHealthReportApplicationRepository.cs | 0 .../PasswordHealthReportApplicationRepository.cs | 0 .../{Tools/Controllers => Dirt}/ReportsControllerTests.cs | 0 .../AddPasswordHealthReportApplicationCommandTests.cs | 0 .../DeletePasswordHealthReportApplicationCommandTests.cs | 0 .../GetPasswordHealthReportApplicationQueryTests.cs | 0 26 files changed, 8 insertions(+) rename src/Api/{Tools => Dirt}/Controllers/HibpController.cs (100%) rename src/Api/{Tools => Dirt}/Controllers/ReportsController.cs (100%) rename src/Api/{Tools => Dirt}/Models/PasswordHealthReportApplicationModel.cs (100%) rename src/Api/{Tools => Dirt}/Models/Response/MemberAccessReportModel.cs (100%) rename src/Api/{Tools => Dirt}/Models/Response/MemberCipherDetailsResponseModel.cs (100%) rename src/Core/{Tools => Dirt/Reports}/Entities/PasswordHealthReportApplication.cs (100%) rename src/Core/{Tools => Dirt/Reports}/Models/Data/MemberAccessCipherDetails.cs (100%) rename src/Core/{Tools => Dirt/Reports}/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs (100%) rename src/Core/{Tools => Dirt/Reports}/ReportFeatures/DropPasswordHealthReportApplicationCommand.cs (100%) rename src/Core/{Tools => Dirt/Reports}/ReportFeatures/GetPasswordHealthReportApplicationQuery.cs (100%) rename src/Core/{Tools => Dirt/Reports}/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs (100%) rename src/Core/{Tools => Dirt/Reports}/ReportFeatures/Interfaces/IDropPasswordHealthReportApplicationCommand.cs (100%) rename src/Core/{Tools => Dirt/Reports}/ReportFeatures/Interfaces/IGetPasswordHealthReportApplicationQuery.cs (100%) rename src/Core/{Tools => Dirt/Reports}/ReportFeatures/MemberAccessCipherDetailsQuery.cs (100%) rename src/Core/{Tools => Dirt/Reports}/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs (100%) rename src/Core/{Tools => Dirt/Reports}/ReportFeatures/ReportingServiceCollectionExtensions.cs (100%) rename src/Core/{Tools => Dirt/Reports}/ReportFeatures/Requests/AddPasswordHealthReportApplicationRequest.cs (100%) rename src/Core/{Tools => Dirt/Reports}/ReportFeatures/Requests/DropPasswordHealthReportApplicationRequest.cs (100%) rename src/Core/{Tools => Dirt/Reports}/ReportFeatures/Requests/MemberAccessCipherDetailsRequest.cs (100%) rename src/Core/{Tools => Dirt/Reports}/Repositories/IPasswordHealthReportApplicationRepository.cs (100%) rename src/Infrastructure.Dapper/{Tools/Repositories => Dirt}/PasswordHealthReportApplicationRepository.cs (100%) rename test/Api.Test/{Tools/Controllers => Dirt}/ReportsControllerTests.cs (100%) rename test/Core.Test/{Tools => Dirt}/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs (100%) rename test/Core.Test/{Tools => Dirt}/ReportFeatures/DeletePasswordHealthReportApplicationCommandTests.cs (100%) rename test/Core.Test/{Tools => Dirt}/ReportFeatures/GetPasswordHealthReportApplicationQueryTests.cs (100%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 973405dea5..9f3048a340 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -43,8 +43,16 @@ src/Core/IdentityServer @bitwarden/team-auth-dev # Key Management team **/KeyManagement @bitwarden/team-key-management-dev +# Tools team **/Tools @bitwarden/team-tools-dev +# Dirt (Data Insights & Reporting) team +src/Api/Controllers/Dirt @bitwarden/team-data-insights-and-reporting-dev +src/Core/Dirt @bitwarden/team-data-insights-and-reporting-dev +src/Infrastructure.Dapper/Dirt @bitwarden/team-data-insights-and-reporting-dev +test/Api.Test/Dirt @bitwarden/team-data-insights-and-reporting-dev +test/Core.Test/Dirt @bitwarden/team-data-insights-and-reporting-dev + # Vault team **/Vault @bitwarden/team-vault-dev **/Vault/AuthorizationHandlers @bitwarden/team-vault-dev @bitwarden/team-admin-console-dev # joint ownership over authorization handlers that affect organization users diff --git a/src/Api/Tools/Controllers/HibpController.cs b/src/Api/Dirt/Controllers/HibpController.cs similarity index 100% rename from src/Api/Tools/Controllers/HibpController.cs rename to src/Api/Dirt/Controllers/HibpController.cs diff --git a/src/Api/Tools/Controllers/ReportsController.cs b/src/Api/Dirt/Controllers/ReportsController.cs similarity index 100% rename from src/Api/Tools/Controllers/ReportsController.cs rename to src/Api/Dirt/Controllers/ReportsController.cs diff --git a/src/Api/Tools/Models/PasswordHealthReportApplicationModel.cs b/src/Api/Dirt/Models/PasswordHealthReportApplicationModel.cs similarity index 100% rename from src/Api/Tools/Models/PasswordHealthReportApplicationModel.cs rename to src/Api/Dirt/Models/PasswordHealthReportApplicationModel.cs diff --git a/src/Api/Tools/Models/Response/MemberAccessReportModel.cs b/src/Api/Dirt/Models/Response/MemberAccessReportModel.cs similarity index 100% rename from src/Api/Tools/Models/Response/MemberAccessReportModel.cs rename to src/Api/Dirt/Models/Response/MemberAccessReportModel.cs diff --git a/src/Api/Tools/Models/Response/MemberCipherDetailsResponseModel.cs b/src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs similarity index 100% rename from src/Api/Tools/Models/Response/MemberCipherDetailsResponseModel.cs rename to src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs diff --git a/src/Core/Tools/Entities/PasswordHealthReportApplication.cs b/src/Core/Dirt/Reports/Entities/PasswordHealthReportApplication.cs similarity index 100% rename from src/Core/Tools/Entities/PasswordHealthReportApplication.cs rename to src/Core/Dirt/Reports/Entities/PasswordHealthReportApplication.cs diff --git a/src/Core/Tools/Models/Data/MemberAccessCipherDetails.cs b/src/Core/Dirt/Reports/Models/Data/MemberAccessCipherDetails.cs similarity index 100% rename from src/Core/Tools/Models/Data/MemberAccessCipherDetails.cs rename to src/Core/Dirt/Reports/Models/Data/MemberAccessCipherDetails.cs diff --git a/src/Core/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs rename to src/Core/Dirt/Reports/ReportFeatures/AddPasswordHealthReportApplicationCommand.cs diff --git a/src/Core/Tools/ReportFeatures/DropPasswordHealthReportApplicationCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/DropPasswordHealthReportApplicationCommand.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/DropPasswordHealthReportApplicationCommand.cs rename to src/Core/Dirt/Reports/ReportFeatures/DropPasswordHealthReportApplicationCommand.cs diff --git a/src/Core/Tools/ReportFeatures/GetPasswordHealthReportApplicationQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/GetPasswordHealthReportApplicationQuery.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/GetPasswordHealthReportApplicationQuery.cs rename to src/Core/Dirt/Reports/ReportFeatures/GetPasswordHealthReportApplicationQuery.cs diff --git a/src/Core/Tools/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs rename to src/Core/Dirt/Reports/ReportFeatures/Interfaces/IAddPasswordHealthReportApplicationCommand.cs diff --git a/src/Core/Tools/ReportFeatures/Interfaces/IDropPasswordHealthReportApplicationCommand.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IDropPasswordHealthReportApplicationCommand.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/Interfaces/IDropPasswordHealthReportApplicationCommand.cs rename to src/Core/Dirt/Reports/ReportFeatures/Interfaces/IDropPasswordHealthReportApplicationCommand.cs diff --git a/src/Core/Tools/ReportFeatures/Interfaces/IGetPasswordHealthReportApplicationQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetPasswordHealthReportApplicationQuery.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/Interfaces/IGetPasswordHealthReportApplicationQuery.cs rename to src/Core/Dirt/Reports/ReportFeatures/Interfaces/IGetPasswordHealthReportApplicationQuery.cs diff --git a/src/Core/Tools/ReportFeatures/MemberAccessCipherDetailsQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/MemberAccessCipherDetailsQuery.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/MemberAccessCipherDetailsQuery.cs rename to src/Core/Dirt/Reports/ReportFeatures/MemberAccessCipherDetailsQuery.cs diff --git a/src/Core/Tools/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs b/src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs rename to src/Core/Dirt/Reports/ReportFeatures/OrganizationReportMembers/Interfaces/IMemberAccessCipherDetailsQuery.cs diff --git a/src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs b/src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/ReportingServiceCollectionExtensions.cs rename to src/Core/Dirt/Reports/ReportFeatures/ReportingServiceCollectionExtensions.cs diff --git a/src/Core/Tools/ReportFeatures/Requests/AddPasswordHealthReportApplicationRequest.cs b/src/Core/Dirt/Reports/ReportFeatures/Requests/AddPasswordHealthReportApplicationRequest.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/Requests/AddPasswordHealthReportApplicationRequest.cs rename to src/Core/Dirt/Reports/ReportFeatures/Requests/AddPasswordHealthReportApplicationRequest.cs diff --git a/src/Core/Tools/ReportFeatures/Requests/DropPasswordHealthReportApplicationRequest.cs b/src/Core/Dirt/Reports/ReportFeatures/Requests/DropPasswordHealthReportApplicationRequest.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/Requests/DropPasswordHealthReportApplicationRequest.cs rename to src/Core/Dirt/Reports/ReportFeatures/Requests/DropPasswordHealthReportApplicationRequest.cs diff --git a/src/Core/Tools/ReportFeatures/Requests/MemberAccessCipherDetailsRequest.cs b/src/Core/Dirt/Reports/ReportFeatures/Requests/MemberAccessCipherDetailsRequest.cs similarity index 100% rename from src/Core/Tools/ReportFeatures/Requests/MemberAccessCipherDetailsRequest.cs rename to src/Core/Dirt/Reports/ReportFeatures/Requests/MemberAccessCipherDetailsRequest.cs diff --git a/src/Core/Tools/Repositories/IPasswordHealthReportApplicationRepository.cs b/src/Core/Dirt/Reports/Repositories/IPasswordHealthReportApplicationRepository.cs similarity index 100% rename from src/Core/Tools/Repositories/IPasswordHealthReportApplicationRepository.cs rename to src/Core/Dirt/Reports/Repositories/IPasswordHealthReportApplicationRepository.cs diff --git a/src/Infrastructure.Dapper/Tools/Repositories/PasswordHealthReportApplicationRepository.cs b/src/Infrastructure.Dapper/Dirt/PasswordHealthReportApplicationRepository.cs similarity index 100% rename from src/Infrastructure.Dapper/Tools/Repositories/PasswordHealthReportApplicationRepository.cs rename to src/Infrastructure.Dapper/Dirt/PasswordHealthReportApplicationRepository.cs diff --git a/test/Api.Test/Tools/Controllers/ReportsControllerTests.cs b/test/Api.Test/Dirt/ReportsControllerTests.cs similarity index 100% rename from test/Api.Test/Tools/Controllers/ReportsControllerTests.cs rename to test/Api.Test/Dirt/ReportsControllerTests.cs diff --git a/test/Core.Test/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs b/test/Core.Test/Dirt/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs similarity index 100% rename from test/Core.Test/Tools/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs rename to test/Core.Test/Dirt/ReportFeatures/AddPasswordHealthReportApplicationCommandTests.cs diff --git a/test/Core.Test/Tools/ReportFeatures/DeletePasswordHealthReportApplicationCommandTests.cs b/test/Core.Test/Dirt/ReportFeatures/DeletePasswordHealthReportApplicationCommandTests.cs similarity index 100% rename from test/Core.Test/Tools/ReportFeatures/DeletePasswordHealthReportApplicationCommandTests.cs rename to test/Core.Test/Dirt/ReportFeatures/DeletePasswordHealthReportApplicationCommandTests.cs diff --git a/test/Core.Test/Tools/ReportFeatures/GetPasswordHealthReportApplicationQueryTests.cs b/test/Core.Test/Dirt/ReportFeatures/GetPasswordHealthReportApplicationQueryTests.cs similarity index 100% rename from test/Core.Test/Tools/ReportFeatures/GetPasswordHealthReportApplicationQueryTests.cs rename to test/Core.Test/Dirt/ReportFeatures/GetPasswordHealthReportApplicationQueryTests.cs From 28467fc8f6d24614a793551eaff2fe292c1c1459 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Tue, 6 May 2025 13:45:05 -0400 Subject: [PATCH 022/114] [PM-20092] Refactor OrganizationUsersController Get to return account recovery users (#5756) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip * wip * add dict conversion to Get * wip * clean up * clean up * continue refactor * Fix feature flag Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> --------- Co-authored-by: Rui Tomé <108268980+r-tome@users.noreply.github.com> --- .../ManageAccountRecoveryRequirement.cs | 20 +++++++ .../OrganizationUsersController.cs | 40 +++++++++++++- .../OrganizationUserResponseModel.cs | 20 +++++++ .../IOrganizationUserUserDetailsQuery.cs | 4 ++ .../OrganizationUserUserDetailsQuery.cs | 55 ++++++++++++++++++- src/Core/Constants.cs | 1 + 6 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 src/Api/AdminConsole/Authorization/Requirements/ManageAccountRecoveryRequirement.cs diff --git a/src/Api/AdminConsole/Authorization/Requirements/ManageAccountRecoveryRequirement.cs b/src/Api/AdminConsole/Authorization/Requirements/ManageAccountRecoveryRequirement.cs new file mode 100644 index 0000000000..268fee5d95 --- /dev/null +++ b/src/Api/AdminConsole/Authorization/Requirements/ManageAccountRecoveryRequirement.cs @@ -0,0 +1,20 @@ +#nullable enable + +using Bit.Core.Context; +using Bit.Core.Enums; + +namespace Bit.Api.AdminConsole.Authorization.Requirements; + +public class ManageAccountRecoveryRequirement : IOrganizationRequirement +{ + public async Task AuthorizeAsync( + CurrentContextOrganization? organizationClaims, + Func> isProviderUserForOrg) + => organizationClaims switch + { + { Type: OrganizationUserType.Owner } => true, + { Type: OrganizationUserType.Admin } => true, + { Permissions.ManageResetPassword: true } => true, + _ => await isProviderUserForOrg() + }; +} diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index e21dd3de49..536914b56f 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -1,4 +1,5 @@ -using Bit.Api.AdminConsole.Models.Request.Organizations; +using Bit.Api.AdminConsole.Authorization.Requirements; +using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Response; @@ -162,6 +163,12 @@ public class OrganizationUsersController : Controller [HttpGet("")] public async Task> Get(Guid orgId, bool includeGroups = false, bool includeCollections = false) { + + if (_featureService.IsEnabled(FeatureFlagKeys.SeparateCustomRolePermissions)) + { + return await GetvNextAsync(orgId, includeGroups, includeCollections); + } + var authorized = (await _authorizationService.AuthorizeAsync( User, new OrganizationScope(orgId), OrganizationUserUserDetailsOperations.ReadAll)).Succeeded; if (!authorized) @@ -191,6 +198,37 @@ public class OrganizationUsersController : Controller return new ListResponseModel(responses); } + private async Task> GetvNextAsync(Guid orgId, bool includeGroups = false, bool includeCollections = false) + { + var request = new OrganizationUserUserDetailsQueryRequest + { + OrganizationId = orgId, + IncludeGroups = includeGroups, + IncludeCollections = includeCollections, + }; + + if ((await _authorizationService.AuthorizeAsync(User, new ManageUsersRequirement())).Succeeded) + { + return GetResultListResponseModel(await _organizationUserUserDetailsQuery.Get(request)); + } + + if ((await _authorizationService.AuthorizeAsync(User, new ManageAccountRecoveryRequirement())).Succeeded) + { + return GetResultListResponseModel(await _organizationUserUserDetailsQuery.GetAccountRecoveryEnrolledUsers(request)); + } + + throw new NotFoundException(); + } + + private ListResponseModel GetResultListResponseModel(IEnumerable<(OrganizationUserUserDetails OrgUser, + bool TwoFactorEnabled, bool ClaimedByOrganization)> results) + { + return new ListResponseModel(results + .Select(result => new OrganizationUserUserDetailsResponseModel(result)) + .ToList()); + } + + [HttpGet("{id}/groups")] public async Task> GetGroups(string orgId, string id) { diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs index 4e869f59b1..057841c7d2 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs @@ -126,6 +126,26 @@ public class OrganizationUserUserMiniDetailsResponseModel : ResponseModel public class OrganizationUserUserDetailsResponseModel : OrganizationUserResponseModel { + public OrganizationUserUserDetailsResponseModel((OrganizationUserUserDetails OrgUser, bool TwoFactorEnabled, bool ClaimedByOrganization) data, string obj = "organizationUserUserDetails") + : base(data.OrgUser, obj) + { + if (data.OrgUser == null) + { + throw new ArgumentNullException(nameof(data.OrgUser)); + } + + Name = data.OrgUser.Name; + Email = data.OrgUser.Email; + AvatarColor = data.OrgUser.AvatarColor; + TwoFactorEnabled = data.TwoFactorEnabled; + SsoBound = !string.IsNullOrWhiteSpace(data.OrgUser.SsoExternalId); + Collections = data.OrgUser.Collections.Select(c => new SelectionReadOnlyResponseModel(c)); + Groups = data.OrgUser.Groups; + // Prevent reset password when using key connector. + ResetPasswordEnrolled = ResetPasswordEnrolled && !data.OrgUser.UsesKeyConnector; + ClaimedByOrganization = data.ClaimedByOrganization; + } + public OrganizationUserUserDetailsResponseModel(OrganizationUserUserDetails organizationUser, bool twoFactorEnabled, bool claimedByOrganization, string obj = "organizationUserUserDetails") : base(organizationUser, obj) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IOrganizationUserUserDetailsQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IOrganizationUserUserDetailsQuery.cs index 8494a6d4ca..59162230da 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IOrganizationUserUserDetailsQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IOrganizationUserUserDetailsQuery.cs @@ -6,4 +6,8 @@ namespace Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; public interface IOrganizationUserUserDetailsQuery { Task> GetOrganizationUserUserDetails(OrganizationUserUserDetailsQueryRequest request); + + Task> Get(OrganizationUserUserDetailsQueryRequest request); + + Task> GetAccountRecoveryEnrolledUsers(OrganizationUserUserDetailsQueryRequest request); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationUserUserDetailsQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationUserUserDetailsQuery.cs index 22fce08021..587e04826b 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationUserUserDetailsQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/OrganizationUserUserDetailsQuery.cs @@ -1,5 +1,9 @@ -using Bit.Core.Models.Data.Organizations.OrganizationUsers; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; +using Bit.Core.Enums; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; +using Bit.Core.Services; using Bit.Core.Utilities; using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; @@ -9,12 +13,21 @@ namespace Core.AdminConsole.OrganizationFeatures.OrganizationUsers; public class OrganizationUserUserDetailsQuery : IOrganizationUserUserDetailsQuery { private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; + private readonly IFeatureService _featureService; + private readonly IGetOrganizationUsersClaimedStatusQuery _getOrganizationUsersClaimedStatusQuery; public OrganizationUserUserDetailsQuery( - IOrganizationUserRepository organizationUserRepository + IOrganizationUserRepository organizationUserRepository, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, + IFeatureService featureService, + IGetOrganizationUsersClaimedStatusQuery getOrganizationUsersClaimedStatusQuery ) { _organizationUserRepository = organizationUserRepository; + _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; + _featureService = featureService; + _getOrganizationUsersClaimedStatusQuery = getOrganizationUsersClaimedStatusQuery; } /// @@ -37,4 +50,42 @@ public class OrganizationUserUserDetailsQuery : IOrganizationUserUserDetailsQuer return o; }); } + + /// + /// Get the organization user user details, two factor enabled status, and + /// claimed status for the provided request. + /// + /// Request details for the query + /// List of OrganizationUserUserDetails + public async Task> Get(OrganizationUserUserDetailsQueryRequest request) + { + var organizationUsers = await GetOrganizationUserUserDetails(request); + + var organizationUsersTwoFactorEnabled = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers)).ToDictionary(u => u.user.Id); + var organizationUsersClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(request.OrganizationId, organizationUsers.Select(o => o.Id)); + var responses = organizationUsers.Select(o => (o, organizationUsersTwoFactorEnabled[o.Id].twoFactorIsEnabled, organizationUsersClaimedStatus[o.Id])); + + + return responses; + } + + /// + /// Get the organization users user details, two factor enabled status, and + /// claimed status for confirmed users that are enrolled in account recovery + /// + /// Request details for the query + /// List of OrganizationUserUserDetails + public async Task> GetAccountRecoveryEnrolledUsers(OrganizationUserUserDetailsQueryRequest request) + { + var organizationUsers = (await GetOrganizationUserUserDetails(request)) + .Where(o => o.Status.Equals(OrganizationUserStatusType.Confirmed) && o.UsesKeyConnector == false && !String.IsNullOrEmpty(o.ResetPasswordKey)); + + var organizationUsersTwoFactorEnabled = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers)).ToDictionary(u => u.user.Id); + var organizationUsersClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(request.OrganizationId, organizationUsers.Select(o => o.Id)); + var responses = organizationUsers + .Select(o => (o, organizationUsersTwoFactorEnabled[o.Id].twoFactorIsEnabled, organizationUsersClaimedStatus[o.Id])); + + return responses; + } + } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 13d0bad495..16b9849451 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -205,6 +205,7 @@ public static class FeatureFlagKeys public const string DesktopCipherForms = "pm-18520-desktop-cipher-forms"; public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk"; public const string EndUserNotifications = "pm-10609-end-user-notifications"; + public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions"; public const string PhishingDetection = "phishing-detection"; public static List GetAllKeys() From ee2399f5008fc68bec455b89d2d3248a6f507c63 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Tue, 6 May 2025 15:36:28 -0400 Subject: [PATCH 023/114] [PM-19383] admins unable to delete attachments (#5774) * add admin endpoint, fix typecasting error * fix typecast issue * wip * cleanup * remove ternary statement --- src/Core/Vault/Services/Implementations/CipherService.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Core/Vault/Services/Implementations/CipherService.cs b/src/Core/Vault/Services/Implementations/CipherService.cs index 73212ab72e..f81e404db8 100644 --- a/src/Core/Vault/Services/Implementations/CipherService.cs +++ b/src/Core/Vault/Services/Implementations/CipherService.cs @@ -891,7 +891,14 @@ public class CipherService : ICipherService // Update the revision date when an attachment is deleted cipher.RevisionDate = DateTime.UtcNow; - await _cipherRepository.ReplaceAsync(orgAdmin ? cipher : (CipherDetails)cipher); + if (orgAdmin) + { + await _cipherRepository.ReplaceAsync(cipher); + } + else + { + await _cipherRepository.ReplaceAsync((CipherDetails)cipher); + } // push await _pushService.PushSyncCipherUpdateAsync(cipher, null); From e465f2ed475fac5d9cce45287faa6765044b0be0 Mon Sep 17 00:00:00 2001 From: Nick Krantz <125900171+nick-livefront@users.noreply.github.com> Date: Wed, 7 May 2025 08:11:29 -0500 Subject: [PATCH 024/114] remove new device verification flags (#5773) --- src/Core/Constants.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 16b9849451..dfb40260c9 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -197,8 +197,6 @@ public static class FeatureFlagKeys /* Vault Team */ public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge"; public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form"; - public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss"; - public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss"; public const string RestrictProviderAccess = "restrict-provider-access"; public const string SecurityTasks = "security-tasks"; public const string CipherKeyEncryption = "cipher-key-encryption"; From e6c4d78fc1b357d3cf271fcc63d2138759f790cf Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Wed, 7 May 2025 13:57:11 -0400 Subject: [PATCH 025/114] chore(feature-flag): [PM-12432] Remove 2fa-authenticator-token feature flag * Completed grouping of feature flags by team. * Completed grouping feature flags by team. * Remove email delay feature flag * Removed feature flag * Fixed reference. * Remove flag after merge. * Removed flag from server. --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index dfb40260c9..90e9e46619 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -115,7 +115,6 @@ public static class FeatureFlagKeys public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence"; public const string EmailVerification = "email-verification"; public const string DeviceTrustLogging = "pm-8285-device-trust-logging"; - public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token"; public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh"; public const string NewDeviceVerification = "new-device-verification"; public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor"; From 051f200d4b8c307c5285ba1c1d1fca3007727236 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Wed, 7 May 2025 17:18:18 -0400 Subject: [PATCH 026/114] [PM-17239] Update Renovate config to configure patch behavior and reassign dependencies (#5775) * Update config to send patch updates to dashboard * Added trailing commas. --- .github/renovate.json5 | 66 ++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 38 deletions(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 344a326519..ac34903c1b 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -20,7 +20,7 @@ ], commitMessagePrefix: "[deps] BRE:", reviewers: ["team:dept-bre"], - addLabels: ["hold"] + addLabels: ["hold"], }, { groupName: "dockerfile minor", @@ -37,6 +37,16 @@ matchManagers: ["github-actions"], matchUpdateTypes: ["minor"], }, + { + // For any Microsoft.Extensions.* and Microsoft.AspNetCore.* packages, we want to create PRs for patch updates. + // This overrides the default that ignores patch updates for nuget dependencies. + matchPackageNames: [ + "/^Microsoft\\.Extensions\\./", + "/^Microsoft\\.AspNetCore\\./", + ], + matchUpdateTypes: ["patch"], + dependencyDashboardApproval: false, + }, { matchManagers: ["dockerfile", "docker-compose"], commitMessagePrefix: "[deps] BRE:", @@ -59,6 +69,7 @@ "DuoUniversal", "Fido2.AspNet", "Duende.IdentityServer", + "Microsoft.AspNetCore.Authentication.JwtBearer", "Microsoft.Extensions.Identity.Stores", "Otp.NET", "Sustainsys.Saml2.AspNetCore2", @@ -79,8 +90,6 @@ "CsvHelper", "Kralizek.AutoFixture.Extensions.MockHttp", "Microsoft.AspNetCore.Mvc.Testing", - "Microsoft.Extensions.Logging", - "Microsoft.Extensions.Logging.Console", "Newtonsoft.Json", "NSubstitute", "Sentry.Serilog", @@ -100,9 +109,9 @@ reviewers: ["team:team-billing-dev"], }, { - matchPackagePatterns: ["^Microsoft.Extensions.Logging"], - groupName: "Microsoft.Extensions.Logging", - description: "Group Microsoft.Extensions.Logging to exclude them from the dotnet monorepo preset", + matchPackageNames: ["/^Microsoft\\.EntityFrameworkCore\\./", "/^dotnet-ef/"], + groupName: "EntityFrameworkCore", + description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset", }, { matchPackageNames: [ @@ -117,9 +126,6 @@ "Microsoft.EntityFrameworkCore.Relational", "Microsoft.EntityFrameworkCore.Sqlite", "Microsoft.EntityFrameworkCore.SqlServer", - "Microsoft.Extensions.Caching.Cosmos", - "Microsoft.Extensions.Caching.SqlServer", - "Microsoft.Extensions.Caching.StackExchangeRedis", "Npgsql.EntityFrameworkCore.PostgreSQL", "Pomelo.EntityFrameworkCore.MySql", ], @@ -142,56 +148,40 @@ "Azure.Messaging.ServiceBus", "Azure.Storage.Blobs", "Azure.Storage.Queues", - "Microsoft.AspNetCore.Authentication.JwtBearer", + "LaunchDarkly.ServerSdk", "Microsoft.AspNetCore.Http", + "Microsoft.AspNetCore.SignalR.Protocols.MessagePack", + "Microsoft.AspNetCore.SignalR.StackExchangeRedis", + "Microsoft.Extensions.Configuration.EnvironmentVariables", + "Microsoft.Extensions.Configuration.UserSecrets", + "Microsoft.Extensions.Configuration", + "Microsoft.Extensions.DependencyInjection.Abstractions", + "Microsoft.Extensions.DependencyInjection", + "Microsoft.Extensions.Logging", + "Microsoft.Extensions.Logging.Console", + "Microsoft.Extensions.Caching.Cosmos", + "Microsoft.Extensions.Caching.SqlServer", + "Microsoft.Extensions.Caching.StackExchangeRedis", "Quartz", ], description: "Platform owned dependencies", commitMessagePrefix: "[deps] Platform:", reviewers: ["team:team-platform-dev"], }, - { - matchPackagePatterns: ["EntityFrameworkCore", "^dotnet-ef"], - groupName: "EntityFrameworkCore", - description: "Group EntityFrameworkCore to exclude them from the dotnet monorepo preset", - }, { matchPackageNames: [ "AutoMapper.Extensions.Microsoft.DependencyInjection", "AWSSDK.SimpleEmail", "AWSSDK.SQS", "Handlebars.Net", - "LaunchDarkly.ServerSdk", "MailKit", - "Microsoft.AspNetCore.SignalR.Protocols.MessagePack", - "Microsoft.AspNetCore.SignalR.StackExchangeRedis", "Microsoft.Azure.NotificationHubs", - "Microsoft.Extensions.Configuration.EnvironmentVariables", - "Microsoft.Extensions.Configuration.UserSecrets", - "Microsoft.Extensions.Configuration", - "Microsoft.Extensions.DependencyInjection.Abstractions", - "Microsoft.Extensions.DependencyInjection", "SendGrid", ], description: "Tools owned dependencies", commitMessagePrefix: "[deps] Tools:", reviewers: ["team:team-tools-dev"], }, - { - matchPackagePatterns: ["^Microsoft.AspNetCore.SignalR"], - groupName: "SignalR", - description: "Group SignalR to exclude them from the dotnet monorepo preset", - }, - { - matchPackagePatterns: ["^Microsoft.Extensions.Configuration"], - groupName: "Microsoft.Extensions.Configuration", - description: "Group Microsoft.Extensions.Configuration to exclude them from the dotnet monorepo preset", - }, - { - matchPackagePatterns: ["^Microsoft.Extensions.DependencyInjection"], - groupName: "Microsoft.Extensions.DependencyInjection", - description: "Group Microsoft.Extensions.DependencyInjection to exclude them from the dotnet monorepo preset", - }, { matchPackageNames: [ "AngleSharp", From 1228fe51c8db1f41be4731fd72e191cb88e60792 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 8 May 2025 07:49:16 -0400 Subject: [PATCH 027/114] Resolve auth warnings (#5784) --- .../TokenProviders/DuoUniversalTokenProvider.cs | 9 +++++---- src/Core/Core.csproj | 2 +- src/Identity/Identity.csproj | 2 -- .../Repositories/DeviceRepository.cs | 6 +----- .../Auth/Controllers/DevicesControllerTests.cs | 2 -- test/Identity.Test/Identity.Test.csproj | 2 -- .../Wrappers/BaseRequestValidatorTestWrapper.cs | 3 +++ .../Wrappers/UserManagerTestWrapper.cs | 16 ++++++++-------- 8 files changed, 18 insertions(+), 24 deletions(-) diff --git a/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs index 21311326c0..cbb994fa09 100644 --- a/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs @@ -16,10 +16,11 @@ public class DuoUniversalTokenProvider( IDuoUniversalTokenService duoUniversalTokenService) : IUserTwoFactorTokenProvider { /// - /// We need the IServiceProvider to resolve the IUserService. There is a complex dependency dance - /// occurring between IUserService, which extends the UserManager, and the usage of the - /// UserManager within this class. Trying to resolve the IUserService using the DI pipeline - /// will not allow the server to start and it will hang and give no helpful indication as to the problem. + /// We need the IServiceProvider to resolve the . There is a complex dependency dance + /// occurring between , which extends the , and the usage + /// of the within this class. Trying to resolve the using + /// the DI pipeline will not allow the server to start and it will hang and give no helpful indication as to the + /// problem. /// private readonly IServiceProvider _serviceProvider = serviceProvider; private readonly IDataProtectorTokenFactory _tokenDataFactory = tokenDataFactory; diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index c7e812fd2c..ba48b6175b 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -4,7 +4,7 @@ false bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml - $(WarningsNotAsErrors);CS1570;CS1574;CS9113;CS1998 + $(WarningsNotAsErrors);CS1574;CS9113;CS1998 diff --git a/src/Identity/Identity.csproj b/src/Identity/Identity.csproj index e9e188b53f..cb506d86e9 100644 --- a/src/Identity/Identity.csproj +++ b/src/Identity/Identity.csproj @@ -3,8 +3,6 @@ bitwarden-Identity false - - $(WarningsNotAsErrors);CS0162 diff --git a/src/Infrastructure.Dapper/Repositories/DeviceRepository.cs b/src/Infrastructure.Dapper/Repositories/DeviceRepository.cs index 723200ff1c..33643eba88 100644 --- a/src/Infrastructure.Dapper/Repositories/DeviceRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/DeviceRepository.cs @@ -17,15 +17,11 @@ public class DeviceRepository : Repository, IDeviceRepository private readonly IGlobalSettings _globalSettings; public DeviceRepository(GlobalSettings globalSettings) - : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + : base(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) { _globalSettings = globalSettings; } - public DeviceRepository(string connectionString, string readOnlyConnectionString) - : base(connectionString, readOnlyConnectionString) - { } - public async Task GetByIdAsync(Guid id, Guid userId) { var device = await GetByIdAsync(id); diff --git a/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs b/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs index 81e100c58c..540d23f98b 100644 --- a/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/DevicesControllerTests.cs @@ -8,7 +8,6 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Services; -using Bit.Core.Settings; using Microsoft.Extensions.Logging; using NSubstitute; using Xunit; @@ -23,7 +22,6 @@ public class DevicesControllerTest private readonly IUntrustDevicesCommand _untrustDevicesCommand; private readonly IUserRepository _userRepositoryMock; private readonly ICurrentContext _currentContextMock; - private readonly IGlobalSettings _globalSettingsMock; private readonly ILogger _loggerMock; private readonly DevicesController _sut; diff --git a/test/Identity.Test/Identity.Test.csproj b/test/Identity.Test/Identity.Test.csproj index 34010d811b..fc0cf07b63 100644 --- a/test/Identity.Test/Identity.Test.csproj +++ b/test/Identity.Test/Identity.Test.csproj @@ -2,8 +2,6 @@ false - - $(WarningsNotAsErrors);CS0672;CS1998 diff --git a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs index ed28f00ce7..c204e380b8 100644 --- a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs +++ b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs @@ -96,6 +96,7 @@ IBaseRequestValidatorTestWrapper return context.ValidatedTokenRequest.Subject ?? new ClaimsPrincipal(); } + [Obsolete] protected override void SetErrorResult( BaseRequestValidationContextFake context, Dictionary customResponse) @@ -103,6 +104,7 @@ IBaseRequestValidatorTestWrapper context.GrantResult = new GrantValidationResult(TokenRequestErrors.InvalidGrant, customResponse: customResponse); } + [Obsolete] protected override void SetSsoResult( BaseRequestValidationContextFake context, Dictionary customResponse) @@ -121,6 +123,7 @@ IBaseRequestValidatorTestWrapper return Task.CompletedTask; } + [Obsolete] protected override void SetTwoFactorResult( BaseRequestValidationContextFake context, Dictionary customResponse) diff --git a/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs b/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs index f1207a4b9a..3152f2327f 100644 --- a/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs +++ b/test/Identity.Test/Wrappers/UserManagerTestWrapper.cs @@ -56,9 +56,9 @@ public class UserManagerTestWrapper : UserManager where TUser : cl /// /// /// - public override async Task GetTwoFactorEnabledAsync(TUser user) + public override Task GetTwoFactorEnabledAsync(TUser user) { - return TWO_FACTOR_ENABLED; + return Task.FromResult(TWO_FACTOR_ENABLED); } /// @@ -66,9 +66,9 @@ public class UserManagerTestWrapper : UserManager where TUser : cl /// /// /// - public override async Task> GetValidTwoFactorProvidersAsync(TUser user) + public override Task> GetValidTwoFactorProvidersAsync(TUser user) { - return TWO_FACTOR_PROVIDERS; + return Task.FromResult(TWO_FACTOR_PROVIDERS); } /// @@ -77,9 +77,9 @@ public class UserManagerTestWrapper : UserManager where TUser : cl /// /// /// - public override async Task GenerateTwoFactorTokenAsync(TUser user, string tokenProvider) + public override Task GenerateTwoFactorTokenAsync(TUser user, string tokenProvider) { - return TWO_FACTOR_TOKEN; + return Task.FromResult(TWO_FACTOR_TOKEN); } /// @@ -89,8 +89,8 @@ public class UserManagerTestWrapper : UserManager where TUser : cl /// /// /// - public override async Task VerifyTwoFactorTokenAsync(TUser user, string tokenProvider, string token) + public override Task VerifyTwoFactorTokenAsync(TUser user, string tokenProvider, string token) { - return TWO_FACTOR_TOKEN_VERIFIED; + return Task.FromResult(TWO_FACTOR_TOKEN_VERIFIED); } } From e4a93b24f13c72714085a35699bf71484fb78e20 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 8 May 2025 09:15:27 -0400 Subject: [PATCH 028/114] Resolve AC warnings (#5785) --- .../IConfirmOrganizationUserCommand.cs | 1 + .../Controllers/GroupsControllerPutTests.cs | 2 +- .../InviteOrganizationUserCommandTests.cs | 4 ++-- .../InviteOrganizationUsersValidatorTests.cs | 2 +- .../Services/EventRouteServiceTests.cs | 16 ++++++++-------- .../Services/SlackEventHandlerTests.cs | 6 +++--- 6 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs index 302ee0901d..e574d29e48 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IConfirmOrganizationUserCommand.cs @@ -1,4 +1,5 @@ using Bit.Core.Entities; +using Bit.Core.Exceptions; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; diff --git a/test/Api.Test/AdminConsole/Controllers/GroupsControllerPutTests.cs b/test/Api.Test/AdminConsole/Controllers/GroupsControllerPutTests.cs index 0e260e73e6..71dc5e5aea 100644 --- a/test/Api.Test/AdminConsole/Controllers/GroupsControllerPutTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/GroupsControllerPutTests.cs @@ -304,7 +304,7 @@ public class GroupsControllerPutTests // Arrange repositories sutProvider.GetDependency().GetManyUserIdsByIdAsync(group.Id).Returns(currentGroupUsers ?? []); sutProvider.GetDependency().GetByIdWithCollectionsAsync(group.Id) - .Returns(new Tuple>(group, currentCollectionAccess ?? [])); + .Returns(new Tuple>(group, currentCollectionAccess ?? [])); if (savingUser != null) { sutProvider.GetDependency().GetByOrganizationAsync(orgId, savingUser.UserId!.Value) diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs index 0592b481d3..80ce4cf481 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs @@ -677,7 +677,7 @@ public class InviteOrganizationUserCommandTests // Assert Assert.IsType>(result); - sutProvider.GetDependency().Received(1) + await sutProvider.GetDependency().Received(1) .SendOrganizationMaxSeatLimitReachedEmailAsync(organization, 2, Arg.Is>(emails => emails.Any(email => email == "provider@email.com"))); } @@ -768,7 +768,7 @@ public class InviteOrganizationUserCommandTests // Assert Assert.IsType>(result); - sutProvider.GetDependency().Received(1) + await sutProvider.GetDependency().Received(1) .SendOrganizationAutoscaledEmailAsync(organization, 1, Arg.Is>(emails => emails.Any(email => email == "provider@email.com"))); } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs index 191ef05603..ee40fb1152 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs @@ -61,7 +61,7 @@ public class InviteOrganizationUsersValidatorTests _ = await sutProvider.Sut.ValidateAsync(request); - sutProvider.GetDependency() + await sutProvider.GetDependency() .Received(1) .ValidateUpdateAsync(Arg.Is(x => x.SmSeatsChanged == true && x.SmSeats == 12)); diff --git a/test/Core.Test/AdminConsole/Services/EventRouteServiceTests.cs b/test/Core.Test/AdminConsole/Services/EventRouteServiceTests.cs index f593a4628b..1a42d846f2 100644 --- a/test/Core.Test/AdminConsole/Services/EventRouteServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/EventRouteServiceTests.cs @@ -26,8 +26,8 @@ public class EventRouteServiceTests await Subject.CreateAsync(eventMessage); - _broadcastEventWriteService.DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any()); - _storageEventWriteService.Received(1).CreateAsync(eventMessage); + await _broadcastEventWriteService.DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any()); + await _storageEventWriteService.Received(1).CreateAsync(eventMessage); } [Theory, BitAutoData] @@ -37,8 +37,8 @@ public class EventRouteServiceTests await Subject.CreateAsync(eventMessage); - _broadcastEventWriteService.Received(1).CreateAsync(eventMessage); - _storageEventWriteService.DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any()); + await _broadcastEventWriteService.Received(1).CreateAsync(eventMessage); + await _storageEventWriteService.DidNotReceiveWithAnyArgs().CreateAsync(Arg.Any()); } [Theory, BitAutoData] @@ -48,8 +48,8 @@ public class EventRouteServiceTests await Subject.CreateManyAsync(eventMessages); - _broadcastEventWriteService.DidNotReceiveWithAnyArgs().CreateManyAsync(Arg.Any>()); - _storageEventWriteService.Received(1).CreateManyAsync(eventMessages); + await _broadcastEventWriteService.DidNotReceiveWithAnyArgs().CreateManyAsync(Arg.Any>()); + await _storageEventWriteService.Received(1).CreateManyAsync(eventMessages); } [Theory, BitAutoData] @@ -59,7 +59,7 @@ public class EventRouteServiceTests await Subject.CreateManyAsync(eventMessages); - _broadcastEventWriteService.Received(1).CreateManyAsync(eventMessages); - _storageEventWriteService.DidNotReceiveWithAnyArgs().CreateManyAsync(Arg.Any>()); + await _broadcastEventWriteService.Received(1).CreateManyAsync(eventMessages); + await _storageEventWriteService.DidNotReceiveWithAnyArgs().CreateManyAsync(Arg.Any>()); } } diff --git a/test/Core.Test/AdminConsole/Services/SlackEventHandlerTests.cs b/test/Core.Test/AdminConsole/Services/SlackEventHandlerTests.cs index 798ba219eb..558bded8b3 100644 --- a/test/Core.Test/AdminConsole/Services/SlackEventHandlerTests.cs +++ b/test/Core.Test/AdminConsole/Services/SlackEventHandlerTests.cs @@ -89,7 +89,7 @@ public class SlackEventHandlerTests var sutProvider = GetSutProvider(OneConfiguration()); await sutProvider.Sut.HandleEventAsync(eventMessage); - sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync( + await sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync( Arg.Is(AssertHelper.AssertPropertyEqual(_token)), Arg.Is(AssertHelper.AssertPropertyEqual( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")), @@ -103,13 +103,13 @@ public class SlackEventHandlerTests var sutProvider = GetSutProvider(TwoConfigurations()); await sutProvider.Sut.HandleEventAsync(eventMessage); - sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync( + await sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync( Arg.Is(AssertHelper.AssertPropertyEqual(_token)), Arg.Is(AssertHelper.AssertPropertyEqual( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")), Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)) ); - sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync( + await sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync( Arg.Is(AssertHelper.AssertPropertyEqual(_token2)), Arg.Is(AssertHelper.AssertPropertyEqual( $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")), From c9b6e5de86298a4bf770ad5a9a7a383a06fab9d7 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 8 May 2025 10:43:19 -0400 Subject: [PATCH 029/114] [PM-20084] [PM-20086] Add `TrialLength` parameter to trial initiation endpoint and email (#5770) * Add trial length parameter to trial initiation endpoint and email * Add feature flag that pegs trial length to 7 when disabled * Add optionality to Identity * Move feature service injection to identity accounts controller --- .../TrialSendVerificationEmailRequestModel.cs | 1 + .../Models/Mail/TrialInititaionVerifyEmail.cs | 16 +++++++++++++++- ...ialInitiationEmailForRegistrationCommand.cs | 3 ++- ...ialInitiationEmailForRegistrationCommand.cs | 10 ++++++++-- src/Core/Constants.cs | 1 + src/Core/Enums/EnumExtensions.cs | 18 ++++++++++++++++++ .../TrialInitiationVerifyEmail.html.hbs | 2 +- .../TrialInitiationVerifyEmail.text.hbs | 2 +- src/Core/Services/IMailService.cs | 3 ++- .../Implementations/HandlebarsMailService.cs | 6 ++++-- .../NoopImplementations/NoopMailService.cs | 3 ++- .../Billing/Controller/AccountsController.cs | 14 +++++++++++--- src/Identity/Startup.cs | 1 + 13 files changed, 67 insertions(+), 13 deletions(-) create mode 100644 src/Core/Enums/EnumExtensions.cs diff --git a/src/Core/Billing/Models/Api/Requests/Accounts/TrialSendVerificationEmailRequestModel.cs b/src/Core/Billing/Models/Api/Requests/Accounts/TrialSendVerificationEmailRequestModel.cs index 2e8780e6a3..b31da9efbc 100644 --- a/src/Core/Billing/Models/Api/Requests/Accounts/TrialSendVerificationEmailRequestModel.cs +++ b/src/Core/Billing/Models/Api/Requests/Accounts/TrialSendVerificationEmailRequestModel.cs @@ -7,4 +7,5 @@ public class TrialSendVerificationEmailRequestModel : RegisterSendVerificationEm { public ProductTierType ProductTier { get; set; } public IEnumerable Products { get; set; } + public int? TrialLength { get; set; } } diff --git a/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs b/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs index 33b9578d0e..b97390dcc9 100644 --- a/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs +++ b/src/Core/Billing/Models/Mail/TrialInititaionVerifyEmail.cs @@ -1,5 +1,6 @@ using Bit.Core.Auth.Models.Mail; using Bit.Core.Billing.Enums; +using Bit.Core.Enums; namespace Bit.Core.Billing.Models.Mail; @@ -16,13 +17,26 @@ public class TrialInitiationVerifyEmail : RegisterVerifyEmail $"&email={Email}" + $"&fromEmail=true" + $"&productTier={(int)ProductTier}" + - $"&product={string.Join(",", Product.Select(p => (int)p))}"; + $"&product={string.Join(",", Product.Select(p => (int)p))}" + + $"&trialLength={TrialLength}"; } + public string VerifyYourEmailHTMLCopy => + TrialLength == 7 + ? "Verify your email address below to finish signing up for your free trial." + : $"Verify your email address below to finish signing up for your {ProductTier.GetDisplayName()} plan."; + + public string VerifyYourEmailTextCopy => + TrialLength == 7 + ? "Verify your email address using the link below and start your free trial of Bitwarden." + : $"Verify your email address using the link below and start your {ProductTier.GetDisplayName()} Bitwarden plan."; + public ProductTierType ProductTier { get; set; } public IEnumerable Product { get; set; } + public int TrialLength { get; set; } + /// /// Currently we only support one product type at a time, despite Product being a collection. /// If we receive both PasswordManager and SecretsManager, we'll send the user to the PM trial route diff --git a/src/Core/Billing/TrialInitiation/Registration/ISendTrialInitiationEmailForRegistrationCommand.cs b/src/Core/Billing/TrialInitiation/Registration/ISendTrialInitiationEmailForRegistrationCommand.cs index 01550228be..6ec31d7b8f 100644 --- a/src/Core/Billing/TrialInitiation/Registration/ISendTrialInitiationEmailForRegistrationCommand.cs +++ b/src/Core/Billing/TrialInitiation/Registration/ISendTrialInitiationEmailForRegistrationCommand.cs @@ -10,5 +10,6 @@ public interface ISendTrialInitiationEmailForRegistrationCommand string? name, bool receiveMarketingEmails, ProductTierType productTier, - IEnumerable products); + IEnumerable products, + int trialLength); } diff --git a/src/Core/Billing/TrialInitiation/Registration/Implementations/SendTrialInitiationEmailForRegistrationCommand.cs b/src/Core/Billing/TrialInitiation/Registration/Implementations/SendTrialInitiationEmailForRegistrationCommand.cs index 385d7ebbd6..3e5b056ec6 100644 --- a/src/Core/Billing/TrialInitiation/Registration/Implementations/SendTrialInitiationEmailForRegistrationCommand.cs +++ b/src/Core/Billing/TrialInitiation/Registration/Implementations/SendTrialInitiationEmailForRegistrationCommand.cs @@ -22,7 +22,8 @@ public class SendTrialInitiationEmailForRegistrationCommand( string? name, bool receiveMarketingEmails, ProductTierType productTier, - IEnumerable products) + IEnumerable products, + int trialLength) { ArgumentException.ThrowIfNullOrWhiteSpace(email, nameof(email)); @@ -43,7 +44,12 @@ public class SendTrialInitiationEmailForRegistrationCommand( await PerformConstantTimeOperationsAsync(); - await mailService.SendTrialInitiationSignupEmailAsync(userExists, email, token, productTier, products); + if (trialLength != 0 && trialLength != 7) + { + trialLength = 7; + } + + await mailService.SendTrialInitiationSignupEmailAsync(userExists, email, token, productTier, products, trialLength); return null; } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 90e9e46619..f12e804a61 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -150,6 +150,7 @@ public static class FeatureFlagKeys public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically"; public const string PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup"; public const string UseOrganizationWarningsService = "use-organization-warnings-service"; + public const string PM20322_AllowTrialLength0 = "pm-20322-allow-trial-length-0"; /* Data Insights and Reporting Team */ public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application"; diff --git a/src/Core/Enums/EnumExtensions.cs b/src/Core/Enums/EnumExtensions.cs new file mode 100644 index 0000000000..d60b530ffb --- /dev/null +++ b/src/Core/Enums/EnumExtensions.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; +using System.Reflection; + +namespace Bit.Core.Enums; + +public static class EnumExtensions +{ + public static string GetDisplayName(this Enum value) + { + var field = value.GetType().GetField(value.ToString()); + if (field?.GetCustomAttribute() is { } attribute) + { + return attribute.Name ?? value.ToString(); + } + + return value.ToString(); + } +} diff --git a/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.html.hbs b/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.html.hbs index 6c1b9edec0..5d379288ef 100644 --- a/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.html.hbs +++ b/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.html.hbs @@ -2,7 +2,7 @@ diff --git a/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.text.hbs b/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.text.hbs index 690cf77734..4e0d064e36 100644 --- a/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.text.hbs +++ b/src/Core/MailTemplates/Handlebars/Billing/TrialInitiationVerifyEmail.text.hbs @@ -1,5 +1,5 @@ {{#>BasicTextLayout}} -Verify your email address using the link below and start your free trial of Bitwarden. +{{VerifyYourEmailTextCopy}} If you did not request this email from Bitwarden, you can safely ignore it. diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 9b05810eaa..11d9603a07 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -21,7 +21,8 @@ public interface IMailService string email, string token, ProductTierType productTier, - IEnumerable products); + IEnumerable products, + int trialLength); Task SendVerifyDeleteEmailAsync(string email, Guid userId, string token); Task SendCannotDeleteClaimedAccountEmailAsync(string email); Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail); diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 1fca85eff4..3266cc9c2e 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -84,7 +84,8 @@ public class HandlebarsMailService : IMailService string email, string token, ProductTierType productTier, - IEnumerable products) + IEnumerable products, + int trialLength) { var message = CreateDefaultMessage("Verify your email", email); var model = new TrialInitiationVerifyEmail @@ -95,7 +96,8 @@ public class HandlebarsMailService : IMailService WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, SiteName = _globalSettings.SiteName, ProductTier = productTier, - Product = products + Product = products, + TrialLength = trialLength }; await AddMessageContentAsync(message, "Billing.TrialInitiationVerifyEmail", model); message.MetaData.Add("SendGridBypassListManagement", true); diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index cd5c1af8a8..bbad5965f4 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -33,7 +33,8 @@ public class NoopMailService : IMailService string email, string token, ProductTierType productTier, - IEnumerable products) + IEnumerable products, + int trailLength) { return Task.FromResult(0); } diff --git a/src/Identity/Billing/Controller/AccountsController.cs b/src/Identity/Billing/Controller/AccountsController.cs index 96ec1280cd..b83940d3aa 100644 --- a/src/Identity/Billing/Controller/AccountsController.cs +++ b/src/Identity/Billing/Controller/AccountsController.cs @@ -1,6 +1,8 @@ -using Bit.Core.Billing.Models.Api.Requests.Accounts; +using Bit.Core; +using Bit.Core.Billing.Models.Api.Requests.Accounts; using Bit.Core.Billing.TrialInitiation.Registration; using Bit.Core.Context; +using Bit.Core.Services; using Bit.Core.Tools.Enums; using Bit.Core.Tools.Models.Business; using Bit.Core.Tools.Services; @@ -15,18 +17,24 @@ namespace Bit.Identity.Billing.Controller; public class AccountsController( ICurrentContext currentContext, ISendTrialInitiationEmailForRegistrationCommand sendTrialInitiationEmailForRegistrationCommand, - IReferenceEventService referenceEventService) : Microsoft.AspNetCore.Mvc.Controller + IReferenceEventService referenceEventService, + IFeatureService featureService) : Microsoft.AspNetCore.Mvc.Controller { [HttpPost("trial/send-verification-email")] [SelfHosted(NotSelfHostedOnly = true)] public async Task PostTrialInitiationSendVerificationEmailAsync([FromBody] TrialSendVerificationEmailRequestModel model) { + var allowTrialLength0 = featureService.IsEnabled(FeatureFlagKeys.PM20322_AllowTrialLength0); + + var trialLength = allowTrialLength0 ? model.TrialLength ?? 7 : 7; + var token = await sendTrialInitiationEmailForRegistrationCommand.Handle( model.Email, model.Name, model.ReceiveMarketingEmails, model.ProductTier, - model.Products); + model.Products, + trialLength); var refEvent = new ReferenceEvent { diff --git a/src/Identity/Startup.cs b/src/Identity/Startup.cs index 320c91b248..2d8ca55def 100644 --- a/src/Identity/Startup.cs +++ b/src/Identity/Startup.cs @@ -145,6 +145,7 @@ public class Startup // Services services.AddBaseServices(globalSettings); services.AddDefaultServices(globalSettings); + services.AddOptionality(); services.AddCoreLocalizationServices(); services.AddBillingOperations(); From af08d4b2a5ff5d16a3c8f4fbbc305d03870d6940 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Thu, 8 May 2025 11:27:06 -0400 Subject: [PATCH 030/114] chore(workflows): Update image tag logic to handle forked branches --- .github/workflows/build.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 33edd075a0..5077f1ba32 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,6 +14,7 @@ on: env: _AZ_REGISTRY: "bitwardenprod.azurecr.io" + _GITHUB_PR_REPO_NAME: ${{ github.event.pull_request.head.repo.full_name }} jobs: lint: @@ -234,12 +235,18 @@ jobs: - name: Generate Docker image tag id: tag run: | - if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then - IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s#/#-#g") + if [[ "${GITHUB_EVENT_NAME}" == "pull_request" || "${GITHUB_EVENT_NAME}" == "pull_request_target" ]]; then + IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize branch name to alphanumeric only else IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g") fi + if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then + SANITIZED_REPO_NAME=$(echo "$_GITHUB_PR_REPO_NAME" | sed "s/[^a-zA-Z0-9]/-/g") # Sanitize repo name to alphanumeric only + IMAGE_TAG=$SANITIZED_REPO_NAME-$IMAGE_TAG # Add repo name to the tag + IMAGE_TAG=${IMAGE_TAG:0:128} # Limit to 128 characters, as that's the max length for Docker image tags + fi + if [[ "$IMAGE_TAG" == "main" ]]; then IMAGE_TAG=dev fi From e3f6562d3a8e483c205a6f498ed4a74d56837d59 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Thu, 8 May 2025 14:07:35 -0400 Subject: [PATCH 031/114] [PM-21345] Re-add existing customer coupon after subscription update (#5788) * Re-add existing customer coupon after subscription update * Run dotnet format --- .../Implementations/StripePaymentService.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 51be369527..85ad7d64d7 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -112,6 +112,8 @@ public class StripePaymentService : IPaymentService throw new BadRequestException("You do not have an active subscription. Reinstate your subscription to make changes."); } + var existingCoupon = sub.Customer.Discount?.Coupon?.Id; + var collectionMethod = sub.CollectionMethod; var daysUntilDue = sub.DaysUntilDue; var chargeNow = collectionMethod == "charge_automatically"; @@ -216,6 +218,19 @@ public class StripePaymentService : IPaymentService DaysUntilDue = daysUntilDue, }); } + + var customer = await _stripeAdapter.CustomerGetAsync(sub.CustomerId); + + var newCoupon = customer.Discount?.Coupon?.Id; + + if (!string.IsNullOrEmpty(existingCoupon) && string.IsNullOrEmpty(newCoupon)) + { + // Re-add the lost coupon due to the update. + await _stripeAdapter.CustomerUpdateAsync(sub.CustomerId, new CustomerUpdateOptions + { + Coupon = existingCoupon + }); + } } return paymentIntentClientSecret; From 547df250455f350d8d76dd5403c45466b14ac1bb Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Thu, 8 May 2025 15:57:24 -0400 Subject: [PATCH 032/114] chore(feature-flag): [PM-12433] Remove device-trust-logging feature flag * Completed grouping of feature flags by team. * Completed grouping feature flags by team. * Remove email delay feature flag * Removed feature flag * Fixed reference. * Remove flag after merge. * Removed flag from server. * Removed feature flag from server --- src/Core/Constants.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index f12e804a61..a27738fd19 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -114,7 +114,6 @@ public static class FeatureFlagKeys public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence"; public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence"; public const string EmailVerification = "email-verification"; - public const string DeviceTrustLogging = "pm-8285-device-trust-logging"; public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh"; public const string NewDeviceVerification = "new-device-verification"; public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor"; From 5b3d3d6e205c6d78f31586ee10ea5a8bc68b3171 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Fri, 9 May 2025 10:46:49 +1000 Subject: [PATCH 033/114] CommandResult and ValidationResult tweaks (#5772) * Simplify and align CommandResult and ValidationResult. In particular, 1 error per Failure/Invalid. * Move these files to a common namespace * Remove unused code --- .../src/Scim/Users/PostUserCommand.cs | 7 +- src/Api/Utilities/CommandResultExtensions.cs | 31 ----- ...vokeNonCompliantOrganizationUserCommand.cs | 2 +- .../InviteUsers/Errors/ErrorMapper.cs | 2 +- .../Errors/FailedToInviteUsersError.cs | 4 +- .../Errors/NoUsersToInviteError.cs | 4 +- .../Errors/UserAlreadyExistsError.cs | 4 +- .../IInviteOrganizationUsersCommand.cs | 2 +- .../InviteOrganizationUsersCommand.cs | 12 +- .../CannotAutoScaleOnSelfHostError.cs | 2 +- .../InviteUsersEnvironmentValidator.cs | 2 +- .../InviteOrganizationUserValidator.cs | 6 +- .../Validation/Organization/Errors.cs | 4 +- .../InviteUsersOrganizationValidator.cs | 2 +- .../Validation/PasswordManager/Errors.cs | 2 +- .../InviteUsersPasswordManagerValidator.cs | 2 +- .../InviteUsers/Validation/Payments/Errors.cs | 4 +- .../Payments/InviteUserPaymentValidation.cs | 2 +- .../InviteUsers/Validation/Provider/Errors.cs | 2 +- ...vitingUserOrganizationProviderValidator.cs | 2 +- ...vokeNonCompliantOrganizationUserCommand.cs | 2 +- .../Shared/Validation/ValidationResult.cs | 44 ------- .../Utilities/Commands/CommandResult.cs | 51 +++++++++ .../{ => Utilities}/Errors/Error.cs | 2 +- .../Errors/InsufficientPermissionsError.cs | 2 +- .../Errors/InvalidResultTypeError.cs | 2 +- .../Errors/RecordNotFoundError.cs | 2 +- .../Validation/IValidator.cs | 2 +- .../Utilities/Validation/ValidationResult.cs | 20 ++++ src/Core/Models/Commands/BadRequestFailure.cs | 23 ---- src/Core/Models/Commands/CommandResult.cs | 88 -------------- .../Models/Commands/NoRecordFoundFailure.cs | 24 ---- .../Utilities/CommandResultExtensionTests.cs | 107 ------------------ .../InviteOrganizationUserCommandTests.cs | 12 +- .../InviteOrganizationUsersValidatorTests.cs | 4 +- .../InviteUserOrganizationValidationTests.cs | 6 +- .../InviteUserPaymentValidationTests.cs | 4 +- ...PasswordManagerInviteUserValidatorTests.cs | 6 +- .../SingleOrgPolicyValidatorTests.cs | 2 +- ...actorAuthenticationPolicyValidatorTests.cs | 2 +- .../AdminConsole/Shared/IValidatorTests.cs | 14 +-- .../Utilities}/Commands/CommandResultTests.cs | 6 +- 42 files changed, 137 insertions(+), 386 deletions(-) delete mode 100644 src/Api/Utilities/CommandResultExtensions.cs delete mode 100644 src/Core/AdminConsole/Shared/Validation/ValidationResult.cs create mode 100644 src/Core/AdminConsole/Utilities/Commands/CommandResult.cs rename src/Core/AdminConsole/{ => Utilities}/Errors/Error.cs (80%) rename src/Core/AdminConsole/{ => Utilities}/Errors/InsufficientPermissionsError.cs (83%) rename src/Core/AdminConsole/{ => Utilities}/Errors/InvalidResultTypeError.cs (71%) rename src/Core/AdminConsole/{ => Utilities}/Errors/RecordNotFoundError.cs (82%) rename src/Core/AdminConsole/{Shared => Utilities}/Validation/IValidator.cs (62%) create mode 100644 src/Core/AdminConsole/Utilities/Validation/ValidationResult.cs delete mode 100644 src/Core/Models/Commands/BadRequestFailure.cs delete mode 100644 src/Core/Models/Commands/CommandResult.cs delete mode 100644 src/Core/Models/Commands/NoRecordFoundFailure.cs delete mode 100644 test/Api.Test/Utilities/CommandResultExtensionTests.cs rename test/Core.Test/{Models => AdminConsole/Utilities}/Commands/CommandResultTests.cs (92%) diff --git a/bitwarden_license/src/Scim/Users/PostUserCommand.cs b/bitwarden_license/src/Scim/Users/PostUserCommand.cs index 46116a46ae..5b4a0c29cd 100644 --- a/bitwarden_license/src/Scim/Users/PostUserCommand.cs +++ b/bitwarden_license/src/Scim/Users/PostUserCommand.cs @@ -6,10 +6,10 @@ using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.Utilities.Commands; using Bit.Core.Billing.Pricing; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Models.Commands; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; @@ -76,9 +76,8 @@ public class PostUserCommand( var invitedOrganizationUserId = result switch { Success success => success.Value.InvitedUser.Id, - Failure failure when failure.Errors - .Any(x => x.Message == NoUsersToInviteError.Code) => (Guid?)null, - Failure failure when failure.Errors.Length != 0 => throw MapToBitException(failure.Errors), + Failure { Error.Message: NoUsersToInviteError.Code } => (Guid?)null, + Failure failure => throw MapToBitException(failure.Error), _ => throw new InvalidOperationException() }; diff --git a/src/Api/Utilities/CommandResultExtensions.cs b/src/Api/Utilities/CommandResultExtensions.cs deleted file mode 100644 index c7315a0fa0..0000000000 --- a/src/Api/Utilities/CommandResultExtensions.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Bit.Core.Models.Commands; -using Microsoft.AspNetCore.Mvc; - -namespace Bit.Api.Utilities; - -public static class CommandResultExtensions -{ - public static IActionResult MapToActionResult(this CommandResult commandResult) - { - return commandResult switch - { - NoRecordFoundFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status404NotFound }, - BadRequestFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest }, - Failure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest }, - Success success => new ObjectResult(success.Value) { StatusCode = StatusCodes.Status200OK }, - _ => throw new InvalidOperationException($"Unhandled commandResult type: {commandResult.GetType().Name}") - }; - } - - public static IActionResult MapToActionResult(this CommandResult commandResult) - { - return commandResult switch - { - NoRecordFoundFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status404NotFound }, - BadRequestFailure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest }, - Failure failure => new ObjectResult(failure.ErrorMessages) { StatusCode = StatusCodes.Status400BadRequest }, - Success => new ObjectResult(new { }) { StatusCode = StatusCodes.Status200OK }, - _ => throw new InvalidOperationException($"Unhandled commandResult type: {commandResult.GetType().Name}") - }; - } -} diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRevokeNonCompliantOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRevokeNonCompliantOrganizationUserCommand.cs index c9768a8905..024d56e8c3 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRevokeNonCompliantOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/Interfaces/IRevokeNonCompliantOrganizationUserCommand.cs @@ -1,5 +1,5 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; -using Bit.Core.Models.Commands; +using Bit.Core.AdminConsole.Utilities.Commands; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/ErrorMapper.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/ErrorMapper.cs index c66d366de5..38fa35b29a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/ErrorMapper.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/ErrorMapper.cs @@ -1,4 +1,4 @@ -using Bit.Core.AdminConsole.Errors; +using Bit.Core.AdminConsole.Utilities.Errors; using Bit.Core.Exceptions; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/FailedToInviteUsersError.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/FailedToInviteUsersError.cs index 810ef744c9..48faf4cac0 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/FailedToInviteUsersError.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/FailedToInviteUsersError.cs @@ -1,5 +1,5 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/NoUsersToInviteError.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/NoUsersToInviteError.cs index 52697572e6..8cd70391a2 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/NoUsersToInviteError.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/NoUsersToInviteError.cs @@ -1,5 +1,5 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/UserAlreadyExistsError.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/UserAlreadyExistsError.cs index 475ad4a886..4fbb8f2bad 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/UserAlreadyExistsError.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Errors/UserAlreadyExistsError.cs @@ -1,5 +1,5 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs index 3e4c7652a5..7e0a8dc3cd 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/IInviteOrganizationUsersCommand.cs @@ -1,5 +1,5 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; -using Bit.Core.Models.Commands; +using Bit.Core.AdminConsole.Utilities.Commands; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs index 662ed314ce..072bc5fc05 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUsersCommand.cs @@ -1,17 +1,17 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums.Provider; -using Bit.Core.AdminConsole.Errors; using Bit.Core.AdminConsole.Interfaces; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Errors; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Commands; +using Bit.Core.AdminConsole.Utilities.Errors; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models.Business; -using Bit.Core.Models.Commands; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; using Bit.Core.Services; @@ -50,11 +50,11 @@ public class InviteOrganizationUsersCommand(IEventService eventService, { case Failure failure: return new Failure( - failure.Errors.Select(error => new Error(error.Message, + new Error(failure.Error.Message, new ScimInviteOrganizationUsersResponse { - InvitedUser = error.ErroredValue.InvitedUsers.FirstOrDefault() - }))); + InvitedUser = failure.Error.ErroredValue.InvitedUsers.FirstOrDefault() + })); case Success success when success.Value.InvitedUsers.Any(): var user = success.Value.InvitedUsers.First(); diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/CannotAutoScaleOnSelfHostError.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/CannotAutoScaleOnSelfHostError.cs index 0624ffe027..e7e331686d 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/CannotAutoScaleOnSelfHostError.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/CannotAutoScaleOnSelfHostError.cs @@ -1,4 +1,4 @@ -using Bit.Core.AdminConsole.Errors; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/InviteUsersEnvironmentValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/InviteUsersEnvironmentValidator.cs index fd0441753a..fb50fd58dd 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/InviteUsersEnvironmentValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/GlobalSettings/InviteUsersEnvironmentValidator.cs @@ -1,4 +1,4 @@ -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.GlobalSettings; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs index 79a3487d19..54f26cb46a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUserValidator.cs @@ -1,7 +1,7 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Errors; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Models.Business; using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface; using Bit.Core.Repositories; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/Errors.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/Errors.cs index 5d072ca17d..f9e9f4eebf 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/Errors.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/Errors.cs @@ -1,5 +1,5 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.Models.Business; +using Bit.Core.AdminConsole.Models.Business; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/InviteUsersOrganizationValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/InviteUsersOrganizationValidator.cs index 9e2ca8d9a6..ce617a2db3 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/InviteUsersOrganizationValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Organization/InviteUsersOrganizationValidator.cs @@ -1,5 +1,5 @@ using Bit.Core.AdminConsole.Models.Business; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/Errors.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/Errors.cs index 6ff7181456..40afa5e9d0 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/Errors.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/Errors.cs @@ -1,4 +1,4 @@ -using Bit.Core.AdminConsole.Errors; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/InviteUsersPasswordManagerValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/InviteUsersPasswordManagerValidator.cs index 6a8ec8e6d3..a1536ad439 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/InviteUsersPasswordManagerValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManager/InviteUsersPasswordManagerValidator.cs @@ -4,7 +4,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.V using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/Errors.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/Errors.cs index c74d1048ad..865a3cb83a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/Errors.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/Errors.cs @@ -1,5 +1,5 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/InviteUserPaymentValidation.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/InviteUserPaymentValidation.cs index cc17a673f9..496dddc916 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/InviteUserPaymentValidation.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Payments/InviteUserPaymentValidation.cs @@ -1,6 +1,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/Errors.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/Errors.cs index 104ce5cc7e..759ac1b780 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/Errors.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/Errors.cs @@ -1,4 +1,4 @@ -using Bit.Core.AdminConsole.Errors; +using Bit.Core.AdminConsole.Utilities.Errors; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/InvitingUserOrganizationProviderValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/InvitingUserOrganizationProviderValidator.cs index f84b25f76f..eeb19eec98 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/InvitingUserOrganizationProviderValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/Provider/InvitingUserOrganizationProviderValidator.cs @@ -1,5 +1,5 @@ using Bit.Core.AdminConsole.Enums.Provider; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Billing.Extensions; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Provider; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs index 971ed02b29..0773cf4f9c 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RevokeNonCompliantOrganizationUserCommand.cs @@ -1,8 +1,8 @@ using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; +using Bit.Core.AdminConsole.Utilities.Commands; using Bit.Core.Enums; -using Bit.Core.Models.Commands; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; diff --git a/src/Core/AdminConsole/Shared/Validation/ValidationResult.cs b/src/Core/AdminConsole/Shared/Validation/ValidationResult.cs deleted file mode 100644 index ba78601637..0000000000 --- a/src/Core/AdminConsole/Shared/Validation/ValidationResult.cs +++ /dev/null @@ -1,44 +0,0 @@ -using Bit.Core.AdminConsole.Errors; - -namespace Bit.Core.AdminConsole.Shared.Validation; - -public abstract record ValidationResult; - -public record Valid : ValidationResult -{ - public Valid() { } - - public Valid(T Value) - { - this.Value = Value; - } - - public T Value { get; init; } -} - -public record Invalid : ValidationResult -{ - public IEnumerable> Errors { get; init; } = []; - - public string ErrorMessageString => string.Join(" ", Errors.Select(e => e.Message)); - - public Invalid() { } - - public Invalid(Error error) : this([error]) { } - - public Invalid(IEnumerable> errors) - { - Errors = errors; - } -} - -public static class ValidationResultMappers -{ - public static ValidationResult Map(this ValidationResult validationResult, B invalidValue) => - validationResult switch - { - Valid => new Valid(invalidValue), - Invalid invalid => new Invalid(invalid.Errors.Select(x => x.ToError(invalidValue))), - _ => throw new ArgumentOutOfRangeException(nameof(validationResult), "Unhandled validation result type") - }; -} diff --git a/src/Core/AdminConsole/Utilities/Commands/CommandResult.cs b/src/Core/AdminConsole/Utilities/Commands/CommandResult.cs new file mode 100644 index 0000000000..274b1a8ba5 --- /dev/null +++ b/src/Core/AdminConsole/Utilities/Commands/CommandResult.cs @@ -0,0 +1,51 @@ +#nullable enable + +using Bit.Core.AdminConsole.Utilities.Errors; +using Bit.Core.AdminConsole.Utilities.Validation; + +namespace Bit.Core.AdminConsole.Utilities.Commands; + +public abstract class CommandResult; + +public class Success(T value) : CommandResult +{ + public T Value { get; } = value; +} + +public class Failure(Error error) : CommandResult +{ + public Error Error { get; } = error; +} + +public class Partial(IEnumerable successfulItems, IEnumerable> failedItems) + : CommandResult +{ + public IEnumerable Successes { get; } = successfulItems; + public IEnumerable> Failures { get; } = failedItems; +} + +public static class CommandResultExtensions +{ + /// + /// This is to help map between the InvalidT ValidationResult and the FailureT CommandResult types. + /// + /// + /// This is the invalid type from validating the object. + /// This function will map between the two types for the inner ErrorT + /// Invalid object's type + /// Failure object's type + /// + public static CommandResult MapToFailure(this Invalid invalidResult, Func mappingFunction) => + new Failure(invalidResult.Error.ToError(mappingFunction(invalidResult.Error.ErroredValue))); +} + +[Obsolete("Use CommandResult instead. This will be removed once old code is updated.")] +public class CommandResult(IEnumerable errors) +{ + public CommandResult(string error) : this([error]) { } + + public bool Success => ErrorMessages.Count == 0; + public bool HasErrors => ErrorMessages.Count > 0; + public List ErrorMessages { get; } = errors.ToList(); + public CommandResult() : this(Array.Empty()) { } +} diff --git a/src/Core/AdminConsole/Errors/Error.cs b/src/Core/AdminConsole/Utilities/Errors/Error.cs similarity index 80% rename from src/Core/AdminConsole/Errors/Error.cs rename to src/Core/AdminConsole/Utilities/Errors/Error.cs index 7ad057d6ed..949c6903a0 100644 --- a/src/Core/AdminConsole/Errors/Error.cs +++ b/src/Core/AdminConsole/Utilities/Errors/Error.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Errors; +namespace Bit.Core.AdminConsole.Utilities.Errors; public record Error(string Message, T ErroredValue); diff --git a/src/Core/AdminConsole/Errors/InsufficientPermissionsError.cs b/src/Core/AdminConsole/Utilities/Errors/InsufficientPermissionsError.cs similarity index 83% rename from src/Core/AdminConsole/Errors/InsufficientPermissionsError.cs rename to src/Core/AdminConsole/Utilities/Errors/InsufficientPermissionsError.cs index d04ceba7c9..c1a524fa0b 100644 --- a/src/Core/AdminConsole/Errors/InsufficientPermissionsError.cs +++ b/src/Core/AdminConsole/Utilities/Errors/InsufficientPermissionsError.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Errors; +namespace Bit.Core.AdminConsole.Utilities.Errors; public record InsufficientPermissionsError(string Message, T ErroredValue) : Error(Message, ErroredValue) { diff --git a/src/Core/AdminConsole/Errors/InvalidResultTypeError.cs b/src/Core/AdminConsole/Utilities/Errors/InvalidResultTypeError.cs similarity index 71% rename from src/Core/AdminConsole/Errors/InvalidResultTypeError.cs rename to src/Core/AdminConsole/Utilities/Errors/InvalidResultTypeError.cs index 67b5b634fb..f39aea68ce 100644 --- a/src/Core/AdminConsole/Errors/InvalidResultTypeError.cs +++ b/src/Core/AdminConsole/Utilities/Errors/InvalidResultTypeError.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Errors; +namespace Bit.Core.AdminConsole.Utilities.Errors; public record InvalidResultTypeError(T Value) : Error(Code, Value) { diff --git a/src/Core/AdminConsole/Errors/RecordNotFoundError.cs b/src/Core/AdminConsole/Utilities/Errors/RecordNotFoundError.cs similarity index 82% rename from src/Core/AdminConsole/Errors/RecordNotFoundError.cs rename to src/Core/AdminConsole/Utilities/Errors/RecordNotFoundError.cs index 25a169efe1..748bb57b5f 100644 --- a/src/Core/AdminConsole/Errors/RecordNotFoundError.cs +++ b/src/Core/AdminConsole/Utilities/Errors/RecordNotFoundError.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Errors; +namespace Bit.Core.AdminConsole.Utilities.Errors; public record RecordNotFoundError(string Message, T ErroredValue) : Error(Message, ErroredValue) { diff --git a/src/Core/AdminConsole/Shared/Validation/IValidator.cs b/src/Core/AdminConsole/Utilities/Validation/IValidator.cs similarity index 62% rename from src/Core/AdminConsole/Shared/Validation/IValidator.cs rename to src/Core/AdminConsole/Utilities/Validation/IValidator.cs index d90386f00e..1598e4472f 100644 --- a/src/Core/AdminConsole/Shared/Validation/IValidator.cs +++ b/src/Core/AdminConsole/Utilities/Validation/IValidator.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.AdminConsole.Shared.Validation; +namespace Bit.Core.AdminConsole.Utilities.Validation; public interface IValidator { diff --git a/src/Core/AdminConsole/Utilities/Validation/ValidationResult.cs b/src/Core/AdminConsole/Utilities/Validation/ValidationResult.cs new file mode 100644 index 0000000000..c62aa880ec --- /dev/null +++ b/src/Core/AdminConsole/Utilities/Validation/ValidationResult.cs @@ -0,0 +1,20 @@ +using Bit.Core.AdminConsole.Utilities.Errors; + +namespace Bit.Core.AdminConsole.Utilities.Validation; + +public abstract record ValidationResult; + +public record Valid(T Value) : ValidationResult; + +public record Invalid(Error Error) : ValidationResult; + +public static class ValidationResultMappers +{ + public static ValidationResult Map(this ValidationResult validationResult, B invalidValue) => + validationResult switch + { + Valid => new Valid(invalidValue), + Invalid invalid => new Invalid(invalid.Error.ToError(invalidValue)), + _ => throw new ArgumentOutOfRangeException(nameof(validationResult), "Unhandled validation result type") + }; +} diff --git a/src/Core/Models/Commands/BadRequestFailure.cs b/src/Core/Models/Commands/BadRequestFailure.cs deleted file mode 100644 index bd2753d4e4..0000000000 --- a/src/Core/Models/Commands/BadRequestFailure.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Bit.Core.Models.Commands; - -public class BadRequestFailure : Failure -{ - public BadRequestFailure(IEnumerable errorMessage) : base(errorMessage) - { - } - - public BadRequestFailure(string errorMessage) : base(errorMessage) - { - } -} - -public class BadRequestFailure : Failure -{ - public BadRequestFailure(IEnumerable errorMessage) : base(errorMessage) - { - } - - public BadRequestFailure(string errorMessage) : base(errorMessage) - { - } -} diff --git a/src/Core/Models/Commands/CommandResult.cs b/src/Core/Models/Commands/CommandResult.cs deleted file mode 100644 index 4a9477067e..0000000000 --- a/src/Core/Models/Commands/CommandResult.cs +++ /dev/null @@ -1,88 +0,0 @@ -#nullable enable - -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.Shared.Validation; - -namespace Bit.Core.Models.Commands; - -public class CommandResult(IEnumerable errors) -{ - public CommandResult(string error) : this([error]) { } - - public bool Success => ErrorMessages.Count == 0; - public bool HasErrors => ErrorMessages.Count > 0; - public List ErrorMessages { get; } = errors.ToList(); - public CommandResult() : this(Array.Empty()) { } -} - -public class Failure : CommandResult -{ - protected Failure(IEnumerable errorMessages) : base(errorMessages) - { - - } - public Failure(string errorMessage) : base(errorMessage) - { - - } -} - -public class Success : CommandResult -{ -} - -public abstract class CommandResult; - -public class Success(T value) : CommandResult -{ - public T Value { get; } = value; -} - -public class Failure(IEnumerable errorMessages) : CommandResult -{ - public List ErrorMessages { get; } = errorMessages.ToList(); - public Error[] Errors { get; set; } = []; - - public string ErrorMessage => string.Join(" ", ErrorMessages); - - public Failure(string error) : this([error]) - { - } - - public Failure(IEnumerable> errors) : this(errors.Select(e => e.Message)) - { - Errors = errors.ToArray(); - } - - public Failure(Error error) : this([error.Message]) - { - Errors = [error]; - } -} - -public class Partial : CommandResult -{ - public T[] Successes { get; set; } = []; - public Error[] Failures { get; set; } = []; - - public Partial(IEnumerable successfulItems, IEnumerable> failedItems) - { - Successes = successfulItems.ToArray(); - Failures = failedItems.ToArray(); - } -} - -public static class CommandResultExtensions -{ - /// - /// This is to help map between the InvalidT ValidationResult and the FailureT CommandResult types. - /// - /// - /// This is the invalid type from validating the object. - /// This function will map between the two types for the inner ErrorT - /// Invalid object's type - /// Failure object's type - /// - public static CommandResult MapToFailure(this Invalid invalidResult, Func mappingFunction) => - new Failure(invalidResult.Errors.Select(errorA => errorA.ToError(mappingFunction(errorA.ErroredValue)))); -} diff --git a/src/Core/Models/Commands/NoRecordFoundFailure.cs b/src/Core/Models/Commands/NoRecordFoundFailure.cs deleted file mode 100644 index a8a322b928..0000000000 --- a/src/Core/Models/Commands/NoRecordFoundFailure.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Bit.Core.Models.Commands; - -public class NoRecordFoundFailure : Failure -{ - public NoRecordFoundFailure(IEnumerable errorMessage) : base(errorMessage) - { - } - - public NoRecordFoundFailure(string errorMessage) : base(errorMessage) - { - } -} - -public class NoRecordFoundFailure : Failure -{ - public NoRecordFoundFailure(IEnumerable errorMessage) : base(errorMessage) - { - } - - public NoRecordFoundFailure(string errorMessage) : base(errorMessage) - { - } -} - diff --git a/test/Api.Test/Utilities/CommandResultExtensionTests.cs b/test/Api.Test/Utilities/CommandResultExtensionTests.cs deleted file mode 100644 index dafae10b5b..0000000000 --- a/test/Api.Test/Utilities/CommandResultExtensionTests.cs +++ /dev/null @@ -1,107 +0,0 @@ -using Bit.Api.Utilities; -using Bit.Core.Models.Commands; -using Bit.Core.Vault.Entities; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Xunit; - -namespace Bit.Api.Test.Utilities; - -public class CommandResultExtensionTests -{ - public static IEnumerable WithGenericTypeTestCases() - { - yield return new object[] - { - new NoRecordFoundFailure(new[] { "Error 1", "Error 2" }), - new ObjectResult(new[] { "Error 1", "Error 2" }) { StatusCode = StatusCodes.Status404NotFound } - }; - yield return new object[] - { - new BadRequestFailure("Error 3"), - new ObjectResult(new[] { "Error 3" }) { StatusCode = StatusCodes.Status400BadRequest } - }; - yield return new object[] - { - new Failure("Error 4"), - new ObjectResult(new[] { "Error 4" }) { StatusCode = StatusCodes.Status400BadRequest } - }; - var cipher = new Cipher() { Id = Guid.NewGuid() }; - - yield return new object[] - { - new Success(cipher), - new ObjectResult(cipher) { StatusCode = StatusCodes.Status200OK } - }; - } - - - [Theory] - [MemberData(nameof(WithGenericTypeTestCases))] - public void MapToActionResult_WithGenericType_ShouldMapToHttpResponse(CommandResult input, ObjectResult expected) - { - var result = input.MapToActionResult(); - - Assert.Equivalent(expected, result); - } - - - [Fact] - public void MapToActionResult_WithGenericType_ShouldThrowExceptionForUnhandledCommandResult() - { - var result = new NotImplementedCommandResult(); - - Assert.Throws(() => result.MapToActionResult()); - } - - public static IEnumerable TestCases() - { - yield return new object[] - { - new NoRecordFoundFailure(new[] { "Error 1", "Error 2" }), - new ObjectResult(new[] { "Error 1", "Error 2" }) { StatusCode = StatusCodes.Status404NotFound } - }; - yield return new object[] - { - new BadRequestFailure("Error 3"), - new ObjectResult(new[] { "Error 3" }) { StatusCode = StatusCodes.Status400BadRequest } - }; - yield return new object[] - { - new Failure("Error 4"), - new ObjectResult(new[] { "Error 4" }) { StatusCode = StatusCodes.Status400BadRequest } - }; - yield return new object[] - { - new Success(), - new ObjectResult(new { }) { StatusCode = StatusCodes.Status200OK } - }; - } - - [Theory] - [MemberData(nameof(TestCases))] - public void MapToActionResult_ShouldMapToHttpResponse(CommandResult input, ObjectResult expected) - { - var result = input.MapToActionResult(); - - Assert.Equivalent(expected, result); - } - - [Fact] - public void MapToActionResult_ShouldThrowExceptionForUnhandledCommandResult() - { - var result = new NotImplementedCommandResult(); - - Assert.Throws(() => result.MapToActionResult()); - } -} - -public class NotImplementedCommandResult : CommandResult -{ - -} - -public class NotImplementedCommandResult : CommandResult -{ - -} diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs index 80ce4cf481..e54e4aa99b 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/InviteOrganizationUserCommandTests.cs @@ -2,7 +2,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; -using Bit.Core.AdminConsole.Errors; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers; @@ -11,12 +10,13 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.M using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Commands; +using Bit.Core.AdminConsole.Utilities.Errors; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; -using Bit.Core.Models.Commands; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.StaticStore; @@ -80,7 +80,7 @@ public class InviteOrganizationUserCommandTests // Assert Assert.IsType>(result); - Assert.Equal(NoUsersToInviteError.Code, (result as Failure).ErrorMessage); + Assert.Equal(NoUsersToInviteError.Code, (result as Failure)!.Error.Message); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() @@ -209,7 +209,7 @@ public class InviteOrganizationUserCommandTests Assert.IsType>(result); var failure = result as Failure; - Assert.Equal(errorMessage, failure!.ErrorMessage); + Assert.Equal(errorMessage, failure!.Error.Message); await sutProvider.GetDependency() .DidNotReceive() @@ -571,7 +571,7 @@ public class InviteOrganizationUserCommandTests // Assert Assert.IsType>(result); - Assert.Equal(FailedToInviteUsersError.Code, (result as Failure)!.ErrorMessage); + Assert.Equal(FailedToInviteUsersError.Code, (result as Failure)!.Error.Message); // org user revert await orgUserRepository.Received(1).DeleteManyAsync(Arg.Is>(x => x.Count() == 1)); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs index ee40fb1152..7c06e04256 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteOrganizationUsersValidatorTests.cs @@ -2,7 +2,7 @@ using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Exceptions; using Bit.Core.Models.Business; @@ -156,6 +156,6 @@ public class InviteOrganizationUsersValidatorTests var result = await sutProvider.Sut.ValidateAsync(request); Assert.IsType>(result); - Assert.Equal("Some Secrets Manager Failure", (result as Invalid)!.ErrorMessageString); + Assert.Equal("Some Secrets Manager Failure", (result as Invalid)!.Error.Message); } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserOrganizationValidationTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserOrganizationValidationTests.cs index 508b9f3cb0..be5586f8a6 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserOrganizationValidationTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserOrganizationValidationTests.cs @@ -1,7 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Organization; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -36,7 +36,7 @@ public class InviteUserOrganizationValidationTests var result = await sutProvider.Sut.ValidateAsync(inviteOrganization); Assert.IsType>(result); - Assert.Equal(OrganizationNoPaymentMethodFoundError.Code, (result as Invalid)!.ErrorMessageString); + Assert.Equal(OrganizationNoPaymentMethodFoundError.Code, (result as Invalid)!.Error.Message); } [Theory] @@ -53,6 +53,6 @@ public class InviteUserOrganizationValidationTests var result = await sutProvider.Sut.ValidateAsync(inviteOrganization); Assert.IsType>(result); - Assert.Equal(OrganizationNoSubscriptionFoundError.Code, (result as Invalid)!.ErrorMessageString); + Assert.Equal(OrganizationNoSubscriptionFoundError.Code, (result as Invalid)!.Error.Message); } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserPaymentValidationTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserPaymentValidationTests.cs index bcca89e1d2..d508f7cc5e 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserPaymentValidationTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/InviteUserPaymentValidationTests.cs @@ -3,7 +3,7 @@ using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.Payments; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.StaticStore.Plans; @@ -39,7 +39,7 @@ public class InviteUserPaymentValidationTests }); Assert.IsType>(result); - Assert.Equal(PaymentCancelledSubscriptionError.Code, (result as Invalid)!.ErrorMessageString); + Assert.Equal(PaymentCancelledSubscriptionError.Code, (result as Invalid)!.Error.Message); } [Fact] diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidatorTests.cs index c320ada8cb..571832d675 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/InviteUsers/Validation/PasswordManagerInviteUserValidatorTests.cs @@ -1,7 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Validation.PasswordManager; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Validation; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Test.Common.AutoFixture; @@ -67,7 +67,7 @@ public class InviteUsersPasswordManagerValidatorTests var result = await sutProvider.Sut.ValidateAsync(subscriptionUpdate); Assert.IsType>(result); - Assert.Equal(PasswordManagerSeatLimitHasBeenReachedError.Code, (result as Invalid)!.ErrorMessageString); + Assert.Equal(PasswordManagerSeatLimitHasBeenReachedError.Code, (result as Invalid)!.Error.Message); } [Theory] @@ -88,6 +88,6 @@ public class InviteUsersPasswordManagerValidatorTests var result = await sutProvider.Sut.ValidateAsync(subscriptionUpdate); Assert.IsType>(result); - Assert.Equal(PasswordManagerPlanDoesNotAllowAdditionalSeatsError.Code, (result as Invalid)!.ErrorMessageString); + Assert.Equal(PasswordManagerPlanDoesNotAllowAdditionalSeatsError.Code, (result as Invalid)!.Error.Message); } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs index d2809102aa..6048ed54d5 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs @@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; +using Bit.Core.AdminConsole.Utilities.Commands; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; @@ -11,7 +12,6 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.Models.Commands; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs index 0edc2b5973..e368f77699 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs @@ -4,11 +4,11 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; +using Bit.Core.AdminConsole.Utilities.Commands; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Models.Commands; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; diff --git a/test/Core.Test/AdminConsole/Shared/IValidatorTests.cs b/test/Core.Test/AdminConsole/Shared/IValidatorTests.cs index abb49c25c6..1bc673426d 100644 --- a/test/Core.Test/AdminConsole/Shared/IValidatorTests.cs +++ b/test/Core.Test/AdminConsole/Shared/IValidatorTests.cs @@ -1,5 +1,5 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.AdminConsole.Shared.Validation; +using Bit.Core.AdminConsole.Utilities.Errors; +using Bit.Core.AdminConsole.Utilities.Validation; using Xunit; namespace Bit.Core.Test.AdminConsole.Shared; @@ -22,13 +22,11 @@ public class IValidatorTests { if (string.IsNullOrWhiteSpace(value.Name)) { - return Task.FromResult>(new Invalid - { - Errors = [new InvalidRequestError(value)] - }); + return Task.FromResult>( + new Invalid(new InvalidRequestError(value))); } - return Task.FromResult>(new Valid { Value = value }); + return Task.FromResult>(new Valid(value)); } } @@ -41,7 +39,7 @@ public class IValidatorTests Assert.IsType>(result); var invalidResult = result as Invalid; - Assert.Equal(InvalidRequestError.Code, invalidResult.Errors.First().Message); + Assert.Equal(InvalidRequestError.Code, invalidResult!.Error.Message); } [Fact] diff --git a/test/Core.Test/Models/Commands/CommandResultTests.cs b/test/Core.Test/AdminConsole/Utilities/Commands/CommandResultTests.cs similarity index 92% rename from test/Core.Test/Models/Commands/CommandResultTests.cs rename to test/Core.Test/AdminConsole/Utilities/Commands/CommandResultTests.cs index c500fef4f5..67ff59c95b 100644 --- a/test/Core.Test/Models/Commands/CommandResultTests.cs +++ b/test/Core.Test/AdminConsole/Utilities/Commands/CommandResultTests.cs @@ -1,9 +1,9 @@ -using Bit.Core.AdminConsole.Errors; -using Bit.Core.Models.Commands; +using Bit.Core.AdminConsole.Utilities.Commands; +using Bit.Core.AdminConsole.Utilities.Errors; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; -namespace Bit.Core.Test.Models.Commands; +namespace Bit.Core.Test.AdminConsole.Utilities.Commands; public class CommandResultTests { From 5f7e2b8a81c1b0f785e59322eee11357001c5cbd Mon Sep 17 00:00:00 2001 From: Oscar Hinton Date: Fri, 9 May 2025 15:00:26 +0200 Subject: [PATCH 034/114] [PM-21075] Initial database seeder (#5703) Adds a database seeder which can be used standalone using a CLI for seeding your local development environment, or used in unit tests to seed complex scenarios. --------- Co-authored-by: Robert Y --- bitwarden-server.sln | 14 ++++++ ...nizationUsersControllerPerformanceTests.cs | 39 ++++++++++++++++ .../Api.IntegrationTest.csproj | 1 + util/DbSeederUtility/DbSeederUtility.csproj | 22 ++++++++++ util/DbSeederUtility/GlobalSettingsFactory.cs | 34 ++++++++++++++ util/DbSeederUtility/Program.cs | 39 ++++++++++++++++ util/DbSeederUtility/README.md | 40 +++++++++++++++++ .../ServiceCollectionExtension.cs | 25 +++++++++++ util/Seeder/Factories/OrganizationSeeder.cs | 44 +++++++++++++++++++ util/Seeder/Factories/UserSeeder.cs | 25 +++++++++++ util/Seeder/README.md | 18 ++++++++ .../Recipes/OrganizationWithUsersRecipe.cs | 37 ++++++++++++++++ util/Seeder/Seeder.csproj | 29 ++++++++++++ 13 files changed, 367 insertions(+) create mode 100644 test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs create mode 100644 util/DbSeederUtility/DbSeederUtility.csproj create mode 100644 util/DbSeederUtility/GlobalSettingsFactory.cs create mode 100644 util/DbSeederUtility/Program.cs create mode 100644 util/DbSeederUtility/README.md create mode 100644 util/DbSeederUtility/ServiceCollectionExtension.cs create mode 100644 util/Seeder/Factories/OrganizationSeeder.cs create mode 100644 util/Seeder/Factories/UserSeeder.cs create mode 100644 util/Seeder/README.md create mode 100644 util/Seeder/Recipes/OrganizationWithUsersRecipe.cs create mode 100644 util/Seeder/Seeder.csproj diff --git a/bitwarden-server.sln b/bitwarden-server.sln index 892d2f4255..2ec8d86e0e 100644 --- a/bitwarden-server.sln +++ b/bitwarden-server.sln @@ -129,6 +129,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Events.IntegrationTest", "t EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core.IntegrationTest", "test\Core.IntegrationTest\Core.IntegrationTest.csproj", "{3631BA42-6731-4118-A917-DAA43C5032B9}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Seeder", "util\Seeder\Seeder.csproj", "{9A612EBA-1C0E-42B8-982B-62F0EE81000A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DbSeederUtility", "util\DbSeederUtility\DbSeederUtility.csproj", "{17A89266-260A-4A03-81AE-C0468C6EE06E}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -325,6 +329,14 @@ Global {3631BA42-6731-4118-A917-DAA43C5032B9}.Debug|Any CPU.Build.0 = Debug|Any CPU {3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.ActiveCfg = Release|Any CPU {3631BA42-6731-4118-A917-DAA43C5032B9}.Release|Any CPU.Build.0 = Release|Any CPU + {9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A612EBA-1C0E-42B8-982B-62F0EE81000A}.Release|Any CPU.Build.0 = Release|Any CPU + {17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {17A89266-260A-4A03-81AE-C0468C6EE06E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {17A89266-260A-4A03-81AE-C0468C6EE06E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -377,6 +389,8 @@ Global {4A725DB3-BE4F-4C23-9087-82D0610D67AF} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {4F4C63A9-AEE2-48C4-AB86-A5BCD665E401} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} {3631BA42-6731-4118-A917-DAA43C5032B9} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84F} + {9A612EBA-1C0E-42B8-982B-62F0EE81000A} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} + {17A89266-260A-4A03-81AE-C0468C6EE06E} = {DD5BD056-4AAE-43EF-BBD2-0B569B8DA84E} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {E01CBF68-2E20-425F-9EDB-E0A6510CA92F} diff --git a/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs new file mode 100644 index 0000000000..94432b05a0 --- /dev/null +++ b/test/Api.IntegrationTest/AdminConsole/Controllers/OrganizationUsersControllerPerformanceTests.cs @@ -0,0 +1,39 @@ +using System.Net; +using System.Net.Http.Headers; +using Bit.Api.IntegrationTest.Factories; +using Bit.Seeder.Recipes; +using Xunit; +using Xunit.Abstractions; + +namespace Bit.Api.IntegrationTest.AdminConsole.Controllers; + +public class OrganizationUsersControllerPerformanceTest(ITestOutputHelper testOutputHelper) +{ + [Theory(Skip = "Performance test")] + [InlineData(100)] + [InlineData(60000)] + public async Task GetAsync(int seats) + { + await using var factory = new ApiApplicationFactory(); + var client = factory.CreateClient(); + + var db = factory.GetDatabaseContext(); + var seeder = new OrganizationWithUsersRecipe(db); + + var orgId = seeder.Seed("Org", seats, "large.test"); + + var tokens = await factory.LoginAsync("admin@large.test", "c55hlJ/cfdvTd4awTXUqow6X3cOQCfGwn11o3HblnPs="); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token); + + var stopwatch = System.Diagnostics.Stopwatch.StartNew(); + + var response = await client.GetAsync($"/organizations/{orgId}/users?includeCollections=true"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var result = await response.Content.ReadAsStringAsync(); + Assert.NotEmpty(result); + + stopwatch.Stop(); + testOutputHelper.WriteLine($"Seed: {seats}; Request duration: {stopwatch.ElapsedMilliseconds} ms"); + } +} diff --git a/test/Api.IntegrationTest/Api.IntegrationTest.csproj b/test/Api.IntegrationTest/Api.IntegrationTest.csproj index 8fa74f98d4..a9d7fd502e 100644 --- a/test/Api.IntegrationTest/Api.IntegrationTest.csproj +++ b/test/Api.IntegrationTest/Api.IntegrationTest.csproj @@ -18,6 +18,7 @@ + diff --git a/util/DbSeederUtility/DbSeederUtility.csproj b/util/DbSeederUtility/DbSeederUtility.csproj new file mode 100644 index 0000000000..90ac7f22b4 --- /dev/null +++ b/util/DbSeederUtility/DbSeederUtility.csproj @@ -0,0 +1,22 @@ + + + + Exe + net8.0 + enable + enable + Bit.DbSeederUtility + DbSeeder + true + 2294c6ba-7cd0-4293-a797-3882e41c61cb + + + + + + + + + + + diff --git a/util/DbSeederUtility/GlobalSettingsFactory.cs b/util/DbSeederUtility/GlobalSettingsFactory.cs new file mode 100644 index 0000000000..e4ad275a0e --- /dev/null +++ b/util/DbSeederUtility/GlobalSettingsFactory.cs @@ -0,0 +1,34 @@ +using Bit.Core.Settings; +using Microsoft.Extensions.Configuration; + +namespace Bit.DbSeederUtility; + +public static class GlobalSettingsFactory +{ + private static GlobalSettings? _globalSettings; + + public static GlobalSettings GlobalSettings + { + get { return _globalSettings ??= LoadGlobalSettings(); } + } + + private static GlobalSettings LoadGlobalSettings() + { + Console.WriteLine("Loading global settings..."); + + var configBuilder = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile($"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Production"}.json", optional: true, reloadOnChange: true) + .AddUserSecrets("bitwarden-Api") // Load user secrets from the API project + .AddEnvironmentVariables(); + + var configuration = configBuilder.Build(); + var globalSettingsSection = configuration.GetSection("globalSettings"); + + var settings = new GlobalSettings(); + globalSettingsSection.Bind(settings); + + return settings; + } +} diff --git a/util/DbSeederUtility/Program.cs b/util/DbSeederUtility/Program.cs new file mode 100644 index 0000000000..2d75b31934 --- /dev/null +++ b/util/DbSeederUtility/Program.cs @@ -0,0 +1,39 @@ +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Seeder.Recipes; +using CommandDotNet; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.DbSeederUtility; + +public class Program +{ + private static int Main(string[] args) + { + return new AppRunner() + .Run(args); + } + + [Command("organization", Description = "Seed an organization and organization users")] + public void Organization( + [Option('n', "Name", Description = "Name of organization")] + string name, + [Option('u', "users", Description = "Number of users to generate")] + int users, + [Option('d', "domain", Description = "Email domain for users")] + string domain + ) + { + // Create service provider with necessary services + var services = new ServiceCollection(); + ServiceCollectionExtension.ConfigureServices(services); + var serviceProvider = services.BuildServiceProvider(); + + // Get a scoped DB context + using var scope = serviceProvider.CreateScope(); + var scopedServices = scope.ServiceProvider; + var db = scopedServices.GetRequiredService(); + + var recipe = new OrganizationWithUsersRecipe(db); + recipe.Seed(name, users, domain); + } +} diff --git a/util/DbSeederUtility/README.md b/util/DbSeederUtility/README.md new file mode 100644 index 0000000000..0eb21ae6c5 --- /dev/null +++ b/util/DbSeederUtility/README.md @@ -0,0 +1,40 @@ +# Bitwarden Database Seeder Utility + +A command-line utility for generating and managing test data for Bitwarden databases. + +## Overview + +DbSeederUtility is an executable wrapper around the Seeder class library that provides a convenient command-line +interface for executing seed-recipes in your local environment. + +## Installation + +The utility can be built and run as a .NET 8 application: + +``` +dotnet build +dotnet run -- [options] +``` + +Or directly using the compiled executable: + +``` +DbSeeder.exe [options] +``` + +## Examples + +### Generate and load test organization + +```bash +# Generate an organization called "seeded" with 10000 users using the @large.test email domain. +# Login using "admin@large.test" with password "asdfasdfasdf" +DbSeeder.exe organization -n seeded -u 10000 -d large.test +``` + +## Dependencies + +This utility depends on: +- The Seeder class library +- CommandDotNet for command-line parsing +- .NET 8.0 runtime diff --git a/util/DbSeederUtility/ServiceCollectionExtension.cs b/util/DbSeederUtility/ServiceCollectionExtension.cs new file mode 100644 index 0000000000..0653bb1801 --- /dev/null +++ b/util/DbSeederUtility/ServiceCollectionExtension.cs @@ -0,0 +1,25 @@ +using Bit.SharedWeb.Utilities; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; + +namespace Bit.DbSeederUtility; + +public static class ServiceCollectionExtension +{ + public static void ConfigureServices(ServiceCollection services) + { + // Load configuration using the GlobalSettingsFactory + var globalSettings = GlobalSettingsFactory.GlobalSettings; + + // Register services + services.AddLogging(builder => builder.AddConsole()); + services.AddSingleton(globalSettings); + + // Add Data Protection services + services.AddDataProtection() + .SetApplicationName("Bitwarden"); + + services.AddDatabaseRepositories(globalSettings); + } +} diff --git a/util/Seeder/Factories/OrganizationSeeder.cs b/util/Seeder/Factories/OrganizationSeeder.cs new file mode 100644 index 0000000000..5e5cb17419 --- /dev/null +++ b/util/Seeder/Factories/OrganizationSeeder.cs @@ -0,0 +1,44 @@ +using Bit.Core.Billing.Enums; +using Bit.Core.Enums; +using Bit.Infrastructure.EntityFramework.AdminConsole.Models; +using Bit.Infrastructure.EntityFramework.Models; + +namespace Bit.Seeder.Factories; + +public class OrganizationSeeder +{ + public static Organization CreateEnterprise(string name, string domain, int seats) + { + return new Organization + { + Id = Guid.NewGuid(), + Name = name, + BillingEmail = $"billing@{domain}", + Plan = "Enterprise (Annually)", + PlanType = PlanType.EnterpriseAnnually, + Seats = seats, + + // Currently hardcoded to the values from https://github.com/bitwarden/sdk-internal/blob/main/crates/bitwarden-core/src/client/test_accounts.rs. + // TODO: These should be dynamically generated by the SDK. + PublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmIJbGMk6eZqVE7UxhZ46Weu2jKciqOiOkSVYtGvs61rfe9AXxtLaaZEKN4d4DmkZcF6dna2eXNxZmb7U4pwlttye8ksqISe6IUAZQox7auBpjopdCEPhKRg3BD/u8ks9UxSxgWe+fpebjt6gd5hsl1/5HOObn7SeU6EEU04cp3/eH7a4OTdXxB8oN62HGV9kM/ubM1goILgjoSJDbihMK0eb7b8hPHwcA/YOgKKiu/N3FighccdSMD5Pk+HfjacsFNZQa2EsqW09IvvSZ+iL6HQeZ1vwc/6TO1J7EOfJZFQcjoEL9LVI693efYoMZSmrPEWziZ4PvwpOOGo6OObyMQIDAQAB", + PrivateKey = "2.6FggyKVyaKQsfohi5yqgbg==|UU2JeafOB41L5UscGmf4kq15JGDf3Bkf67KECiehTODzbWctVLTgyDk0Qco8/6CMN6nZGXjxR2A4r5ExhmwRNsNxd77G+MprkmiJz+7w33ROZ1ouQO5XjD3wbQ3ssqNiTKId6yAUPBvuAZRixVApauTuADc8QWGixqCQcqZzmU7YSBBIPf652/AEYr4Tk64YihoE39pHiK8MRbTLdRt3EF4LSMugPAPM24vCgUv3w1TD3Fj6sDg/6oi3flOV9SJZX4vCiUXbDNEuD/p2aQrEXVbaxweFOHjTe7F4iawjXw3nG3SO8rUBHcxbhDDVx5rjYactbW5QvHWiyla6uLb6o8WHBneg2EjTEwAHOZE/rBjcqmAJb2sVp1E0Kwq8ycGmL69vmqJPC1GqVTohAQvmEkaxIPpfq24Yb9ZPrADA7iEXBKuAQ1FphFUVgJBJGJbd60sOV1Rz1T+gUwS4wCNQ4l3LG1S22+wzUVlEku5DXFnT932tatqTyWEthqPqLCt6dL1+qa94XLpeHagXAx2VGe8n8IlcADtxqS+l8xQ4heT12WO9kC316vqvg1mnsI56faup9hb3eT9ZpKyxSBGYOphlTWfV1Y/v64f5PYvTo4aL0IYHyLY/9Qi72vFmOpPeHBYgD5t3j+H2CsiU1PkYsBggOmD7xW8FDuT6HWVvwhEJqeibVPK0Lhyj6tgvlSIAvFUaSMFPlmwFNmwfj/AHUhr9KuTfsBFTZ10yy9TZVgf+EofwnrxHBaWUgdD40aHoY1VjfG33iEuajb6buxG3pYFyPNhJNzeLZisUKIDRMQpUHrsE22EyrFFran3tZGdtcyIEK4Q1F0ULYzJ6T9iY25/ZgPy3pEAAMZCtqo3s+GjX295fWIHfMcnjMgNUHPjExjWBHa+ggK9iQXkFpBVyYB1ga/+0eiIhiek3PlgtvpDrqF7TsLK+ROiBw2GJ7uaO3EEXOj2GpNBuEJ5CdodhZkwzhwMcSatgDHkUuNVu0iVbF6/MxVdOxWXKO+jCYM6PZk/vAhLYqpPzu2T2Uyz4nkDs2Tiq61ez6FoCrzdHIiyIxVTzUQH8G9FgSmtaZ7GCbqlhnurYgcMciwPzxg0hpAQT+NZw1tVEii9vFSpJJbGJqNhORKfKh/Mu1P/9LOQq7Y0P2FIR3x/eUVEQ7CGv2jVtO5ryGSmKeq/P9Fr54wTPaNiqN2K+leACUznCdUWw8kZo/AsBcrOe4OkRX6k8LC3oeJXy06DEToatxEvPYemUauhxiXRw8nfNMqc4LyJq2bbT0zCgJHoqpozPdNg6AYWcoIobgAGu7ZQGq+oE1MT3GZxotMPe/NUJiAc5YE9Thb5Yf3gyno71pyqPTVl/6IQuh4SUz7rkgwF/aVHEnr4aUYNoc0PEzd2Me0jElsA3GAneq1I/wngutOWgTViTK4Nptr5uIzMVQs9H1rOMJNorP8b02t1NDu010rSsib9GaaJJq4r4iy46laQOxWoU0ex26arYnk+jw4833WSCTVBIprTgizZ+fKjoY0xwXvI2oOvGNEUCtGFvKFORTaQrlaXZIg1toa2BBVNicyONbwnI3KIu3MgGJ2SlCVXJn8oHFppVHFCdwgN1uDzGiKAhjvr0sZTUtXin2f2CszPTbbo=|fUhbVKrr8CSKE7TZJneXpDGraj5YhRrq9ESo206S+BY=", + }; + } +} + +public static class OrgnaizationExtensions +{ + public static OrganizationUser CreateOrganizationUser(this Organization organization, User user) + { + return new OrganizationUser + { + Id = Guid.NewGuid(), + OrganizationId = organization.Id, + UserId = user.Id, + + Key = "4.rY01mZFXHOsBAg5Fq4gyXuklWfm6mQASm42DJpx05a+e2mmp+P5W6r54WU2hlREX0uoTxyP91bKKwickSPdCQQ58J45LXHdr9t2uzOYyjVzpzebFcdMw1eElR9W2DW8wEk9+mvtWvKwu7yTebzND+46y1nRMoFydi5zPVLSlJEf81qZZ4Uh1UUMLwXz+NRWfixnGXgq2wRq1bH0n3mqDhayiG4LJKgGdDjWXC8W8MMXDYx24SIJrJu9KiNEMprJE+XVF9nQVNijNAjlWBqkDpsfaWTUfeVLRLctfAqW1blsmIv4RQ91PupYJZDNc8nO9ZTF3TEVM+2KHoxzDJrLs2Q==", + Type = OrganizationUserType.Admin, + Status = OrganizationUserStatusType.Confirmed + }; + } +} diff --git a/util/Seeder/Factories/UserSeeder.cs b/util/Seeder/Factories/UserSeeder.cs new file mode 100644 index 0000000000..90cadf0b78 --- /dev/null +++ b/util/Seeder/Factories/UserSeeder.cs @@ -0,0 +1,25 @@ +using Bit.Core.Enums; +using Bit.Infrastructure.EntityFramework.Models; + +namespace Bit.Seeder.Factories; + +public class UserSeeder +{ + public static User CreateUser(string email) + { + return new User + { + Id = Guid.NewGuid(), + Email = email, + MasterPassword = "AQAAAAIAAYagAAAAEBATmF66OHMpHuHKc1CsGZQ1ltHUHyhYK+7e4re3bVFi16SOpLpDfzdFswnvFQs2Rg==", + SecurityStamp = "4830e359-e150-4eae-be2a-996c81c5e609", + Key = "2.z/eLKFhd62qy9RzXu3UHgA==|fF6yNupiCIguFKSDTB3DoqcGR0Xu4j+9VlnMyT5F3PaWIcGhzQKIzxdB95nhslaCQv3c63M7LBnvzVo1J9SUN85RMbP/57bP1HvhhU1nvL8=|IQPtf8v7k83MFZEhazSYXSdu98BBU5rqtvC4keVWyHM=", + PublicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0Ww2chogqCpaAR7Uw448am4b7vDFXiM5kXjFlGfXBlrAdAqTTggEvTDlMNYqPlCo+mBM6iFmTTUY9rpZBvFskMnKvsvpJ47/fehAH2o2e3Ulv/5NFevaVCMCmpkBDtbMbO1A4a3btdRtCP8DsKWMefHauEpaoLxNTLWnOIZVfCMjsSgx2EvULHAZPTtbFwm4+UVKniM4ds4jvOsD85h4jn2aLs/jWJXFfxN8iVSqEqpC2TBvsPdyHb49xQoWWfF0Z6BiNqeNGKEU9Uos1pjL+kzhEzzSpH31PZT/ufJ/oo4+93wrUt57hb6f0jxiXhwd5yQ+9F6wVwpbfkq0IwhjOwIDAQAB", + PrivateKey = "2.yN7l00BOlUE0Sb0M//Q53w==|EwKG/BduQRQ33Izqc/ogoBROIoI5dmgrxSo82sgzgAMIBt3A2FZ9vPRMY+GWT85JiqytDitGR3TqwnFUBhKUpRRAq4x7rA6A1arHrFp5Tp1p21O3SfjtvB3quiOKbqWk6ZaU1Np9HwqwAecddFcB0YyBEiRX3VwF2pgpAdiPbSMuvo2qIgyob0CUoC/h4Bz1be7Qa7B0Xw9/fMKkB1LpOm925lzqosyMQM62YpMGkjMsbZz0uPopu32fxzDWSPr+kekNNyLt9InGhTpxLmq1go/pXR2uw5dfpXc5yuta7DB0EGBwnQ8Vl5HPdDooqOTD9I1jE0mRyuBpWTTI3FRnu3JUh3rIyGBJhUmHqGZvw2CKdqHCIrQeQkkEYqOeJRJVdBjhv5KGJifqT3BFRwX/YFJIChAQpebNQKXe/0kPivWokHWwXlDB7S7mBZzhaAPidZvnuIhalE2qmTypDwHy22FyqV58T8MGGMchcASDi/QXI6kcdpJzPXSeU9o+NC68QDlOIrMVxKFeE7w7PvVmAaxEo0YwmuAzzKy9QpdlK0aab/xEi8V4iXj4hGepqAvHkXIQd+r3FNeiLfllkb61p6WTjr5urcmDQMR94/wYoilpG5OlybHdbhsYHvIzYoLrC7fzl630gcO6t4nM24vdB6Ymg9BVpEgKRAxSbE62Tqacxqnz9AcmgItb48NiR/He3n3ydGjPYuKk/ihZMgEwAEZvSlNxYONSbYrIGDtOY+8Nbt6KiH3l06wjZW8tcmFeVlWv+tWotnTY9IqlAfvNVTjtsobqtQnvsiDjdEVtNy/s2ci5TH+NdZluca2OVEr91Wayxh70kpM6ib4UGbfdmGgCo74gtKvKSJU0rTHakQ5L9JlaSDD5FamBRyI0qfL43Ad9qOUZ8DaffDCyuaVyuqk7cz9HwmEmvWU3VQ+5t06n/5kRDXttcw8w+3qClEEdGo1KeENcnXCB32dQe3tDTFpuAIMLqwXs6FhpawfZ5kPYvLPczGWaqftIs/RXJ/EltGc0ugw2dmTLpoQhCqrcKEBDoYVk0LDZKsnzitOGdi9mOWse7Se8798ib1UsHFUjGzISEt6upestxOeupSTOh0v4+AjXbDzRUyogHww3V+Bqg71bkcMxtB+WM+pn1XNbVTyl9NR040nhP7KEf6e9ruXAtmrBC2ah5cFEpLIot77VFZ9ilLuitSz+7T8n1yAh1IEG6xxXxninAZIzi2qGbH69O5RSpOJuJTv17zTLJQIIc781JwQ2TTwTGnx5wZLbffhCasowJKd2EVcyMJyhz6ru0PvXWJ4hUdkARJs3Xu8dus9a86N8Xk6aAPzBDqzYb1vyFIfBxP0oO8xFHgd30Cgmz8UrSE3qeWRrF8ftrI6xQnFjHBGWD/JWSvd6YMcQED0aVuQkuNW9ST/DzQThPzRfPUoiL10yAmV7Ytu4fR3x2sF0Yfi87YhHFuCMpV/DsqxmUizyiJuD938eRcH8hzR/VO53Qo3UIsqOLcyXtTv6THjSlTopQ+JOLOnHm1w8dzYbLN44OG44rRsbihMUQp+wUZ6bsI8rrOnm9WErzkbQFbrfAINdoCiNa6cimYIjvvnMTaFWNymqY1vZxGztQiMiHiHYwTfwHTXrb9j0uPM=|09J28iXv9oWzYtzK2LBT6Yht4IT4MijEkk0fwFdrVQ4=", + ApiKey = "7gp59kKHt9kMlks0BuNC4IjNXYkljR", + + Kdf = KdfType.PBKDF2_SHA256, + KdfIterations = 600_000, + }; + } +} diff --git a/util/Seeder/README.md b/util/Seeder/README.md new file mode 100644 index 0000000000..8597ad6e39 --- /dev/null +++ b/util/Seeder/README.md @@ -0,0 +1,18 @@ +# Bitwarden Database Seeder + +A class library for generating and inserting test data. + +## Project Structure + +The project is organized into these main components: + +### Factories + +Factories are helper classes for creating domain entities and populating them with realistic data. This assist in +decreasing the amount of boilerplate code needed to create test data in recipes. + +### Recipes + +Recipes are pre-defined data sets which can be run to generate and load data into the database. They often allow a allow +for a few arguments to customize the data slightly. Recipes should be kept simple and focused on a single task. Default +to creating more recipes rather than adding complexity to existing ones. diff --git a/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs b/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs new file mode 100644 index 0000000000..fb06c091ae --- /dev/null +++ b/util/Seeder/Recipes/OrganizationWithUsersRecipe.cs @@ -0,0 +1,37 @@ +using Bit.Infrastructure.EntityFramework.Models; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Seeder.Factories; +using LinqToDB.EntityFrameworkCore; + +namespace Bit.Seeder.Recipes; + +public class OrganizationWithUsersRecipe(DatabaseContext db) +{ + public Guid Seed(string name, int users, string domain) + { + var organization = OrganizationSeeder.CreateEnterprise(name, domain, users); + var user = UserSeeder.CreateUser($"admin@{domain}"); + var orgUser = organization.CreateOrganizationUser(user); + + var additionalUsers = new List(); + var additionalOrgUsers = new List(); + for (var i = 0; i < users; i++) + { + var additionalUser = UserSeeder.CreateUser($"user{i}@{domain}"); + additionalUsers.Add(additionalUser); + additionalOrgUsers.Add(organization.CreateOrganizationUser(additionalUser)); + } + + db.Add(organization); + db.Add(user); + db.Add(orgUser); + + db.SaveChanges(); + + // Use LinqToDB's BulkCopy for significant better performance + db.BulkCopy(additionalUsers); + db.BulkCopy(additionalOrgUsers); + + return organization.Id; + } +} diff --git a/util/Seeder/Seeder.csproj b/util/Seeder/Seeder.csproj new file mode 100644 index 0000000000..392f6434cc --- /dev/null +++ b/util/Seeder/Seeder.csproj @@ -0,0 +1,29 @@ + + + + + net8.0 + enable + enable + Bit.Seeder + Bit.Seeder + Core library for generating and managing test data for Bitwarden + library + false + + + + + + + + + + + + + + + + + From 3989e3b26bb254f86eafd13e552ce3acc4192069 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Fri, 9 May 2025 09:37:16 -0400 Subject: [PATCH 035/114] chore(feature-flag): [PM-8671] Remove new-device-verification feature flag * Completed grouping of feature flags by team. * Completed grouping feature flags by team. * Remove email delay feature flag * Removed feature flag * Fixed reference. * Remove flag after merge. * Removed flag from server. * Removed feature flag from server * Remove new device verification feature flag. * Removed unnecessary using. * Remove feature flag from Constants --- src/Admin/Controllers/UsersController.cs | 1 - .../Auth/Controllers/AccountsController.cs | 1 - src/Core/Constants.cs | 1 - .../RequestValidators/DeviceValidator.cs | 8 ++---- .../IdentityServer/DeviceValidatorTests.cs | 25 +++---------------- 5 files changed, 5 insertions(+), 31 deletions(-) diff --git a/src/Admin/Controllers/UsersController.cs b/src/Admin/Controllers/UsersController.cs index 71be19a041..cecd7a2142 100644 --- a/src/Admin/Controllers/UsersController.cs +++ b/src/Admin/Controllers/UsersController.cs @@ -167,7 +167,6 @@ public class UsersController : Controller [HttpPost] [ValidateAntiForgeryToken] [RequirePermission(Permission.User_NewDeviceException_Edit)] - [RequireFeature(FeatureFlagKeys.NewDeviceVerification)] public async Task ToggleNewDeviceVerification(Guid id) { var user = await _userRepository.GetByIdAsync(id); diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 621524228a..2134a7fc4e 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -693,7 +693,6 @@ public class AccountsController : Controller } } - [RequireFeature(FeatureFlagKeys.NewDeviceVerification)] [AllowAnonymous] [HttpPost("resend-new-device-otp")] public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index a27738fd19..3399a729d1 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -115,7 +115,6 @@ public static class FeatureFlagKeys public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence"; public const string EmailVerification = "email-verification"; public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh"; - public const string NewDeviceVerification = "new-device-verification"; public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor"; public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor"; public const string RecoveryCodeLogin = "pm-17128-recovery-code-login"; diff --git a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs index 36a08326ab..4dc77c4449 100644 --- a/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/DeviceValidator.cs @@ -22,8 +22,7 @@ public class DeviceValidator( ICurrentContext currentContext, IUserService userService, IDistributedCache distributedCache, - ILogger logger, - IFeatureService featureService) : IDeviceValidator + ILogger logger) : IDeviceValidator { private readonly IDeviceService _deviceService = deviceService; private readonly IDeviceRepository _deviceRepository = deviceRepository; @@ -33,7 +32,6 @@ public class DeviceValidator( private readonly IUserService _userService = userService; private readonly IDistributedCache distributedCache = distributedCache; private readonly ILogger _logger = logger; - private readonly IFeatureService _featureService = featureService; public async Task ValidateRequestDeviceAsync(ValidatedTokenRequest request, CustomValidatorRequestContext context) { @@ -64,9 +62,7 @@ public class DeviceValidator( } // We have established that the device is unknown at this point; begin new device verification - // PM-13340: remove feature flag - if (_featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) && - request.GrantType == "password" && + if (request.GrantType == "password" && request.Raw["AuthRequest"] == null && !context.TwoFactorRequired && !context.SsoRequired && diff --git a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs index b71dd6c230..9e20e630cd 100644 --- a/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/DeviceValidatorTests.cs @@ -1,5 +1,4 @@ -using Bit.Core; -using Bit.Core.Context; +using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Api; @@ -28,7 +27,7 @@ public class DeviceValidatorTests private readonly IUserService _userService; private readonly IDistributedCache _distributedCache; private readonly Logger _logger; - private readonly IFeatureService _featureService; + private readonly DeviceValidator _sut; public DeviceValidatorTests() @@ -41,7 +40,6 @@ public class DeviceValidatorTests _userService = Substitute.For(); _distributedCache = Substitute.For(); _logger = new Logger(Substitute.For()); - _featureService = Substitute.For(); _sut = new DeviceValidator( _deviceService, _deviceRepository, @@ -50,8 +48,7 @@ public class DeviceValidatorTests _currentContext, _userService, _distributedCache, - _logger, - _featureService); + _logger); } [Theory, BitAutoData] @@ -312,8 +309,6 @@ public class DeviceValidatorTests AddValidDeviceToRequest(request); _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) .Returns(null as Device); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) - .Returns(true); request.GrantType = grantType; @@ -336,8 +331,6 @@ public class DeviceValidatorTests AddValidDeviceToRequest(request); _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) .Returns(null as Device); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) - .Returns(true); request.Raw.Add("AuthRequest", "authRequest"); @@ -360,8 +353,6 @@ public class DeviceValidatorTests AddValidDeviceToRequest(request); _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) .Returns(null as Device); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) - .Returns(true); context.TwoFactorRequired = true; @@ -384,8 +375,6 @@ public class DeviceValidatorTests AddValidDeviceToRequest(request); _deviceRepository.GetByIdentifierAsync(context.Device.Identifier, context.User.Id) .Returns(null as Device); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification) - .Returns(true); context.SsoRequired = true; @@ -404,7 +393,6 @@ public class DeviceValidatorTests { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; context.User = null; @@ -430,7 +418,6 @@ public class DeviceValidatorTests { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; context.User.VerifyDevices = false; @@ -454,7 +441,6 @@ public class DeviceValidatorTests { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; _distributedCache.GetAsync(Arg.Any()).Returns(null as byte[]); context.User.CreationDate = DateTime.UtcNow - TimeSpan.FromHours(23); @@ -479,7 +465,6 @@ public class DeviceValidatorTests { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; _distributedCache.GetAsync(Arg.Any()).Returns([1]); @@ -503,7 +488,6 @@ public class DeviceValidatorTests { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; _distributedCache.GetAsync(Arg.Any()).Returns(null as byte[]); @@ -535,7 +519,6 @@ public class DeviceValidatorTests { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; _distributedCache.GetAsync(Arg.Any()).Returns(null as byte[]); @@ -564,7 +547,6 @@ public class DeviceValidatorTests { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; _distributedCache.GetAsync(Arg.Any()).Returns([1]); _deviceRepository.GetManyByUserIdAsync(context.User.Id).Returns([]); @@ -590,7 +572,6 @@ public class DeviceValidatorTests { // Arrange ArrangeForHandleNewDeviceVerificationTest(context, request); - _featureService.IsEnabled(FeatureFlagKeys.NewDeviceVerification).Returns(true); _globalSettings.EnableNewDeviceVerification = true; _deviceRepository.GetManyByUserIdAsync(context.User.Id).Returns([new Device()]); _distributedCache.GetAsync(Arg.Any()).Returns(null as byte[]); From 2918d46b62c674dfd9cfd631f3995c70ae25ebc2 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 9 May 2025 10:12:43 -0400 Subject: [PATCH 036/114] Resolve Vault warnings (#5786) - Also remove extra exclusions --- src/Core/Core.csproj | 2 +- .../Vault/Queries/GetCipherPermissionsForUserQuery.cs | 4 ++-- src/Core/Vault/Repositories/ICipherRepository.cs | 1 + src/Infrastructure.Dapper/Infrastructure.Dapper.csproj | 5 ----- .../Infrastructure.EntityFramework.csproj | 2 +- test/Api.Test/Api.Test.csproj | 2 -- test/Core.Test/Core.Test.csproj | 2 -- .../Vault/Queries/GetTasksForOrganizationQueryTests.cs | 8 ++++---- 8 files changed, 9 insertions(+), 17 deletions(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index ba48b6175b..4411a3de9b 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -4,7 +4,7 @@ false bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml - $(WarningsNotAsErrors);CS1574;CS9113;CS1998 + $(WarningsNotAsErrors);CS1574;CS9113 diff --git a/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs b/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs index 5cce87e958..07e9d07299 100644 --- a/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs +++ b/src/Core/Vault/Queries/GetCipherPermissionsForUserQuery.cs @@ -45,7 +45,7 @@ public class GetCipherPermissionsForUserQuery : IGetCipherPermissionsForUserQuer cipher.Value.ViewPassword = true; } } - else if (await CanAccessUnassignedCiphersAsync(org)) + else if (CanAccessUnassignedCiphers(org)) { var unassignedCiphers = await _cipherRepository.GetManyUnassignedOrganizationDetailsByOrganizationIdAsync(organizationId); foreach (var unassignedCipher in unassignedCiphers) @@ -83,7 +83,7 @@ public class GetCipherPermissionsForUserQuery : IGetCipherPermissionsForUserQuer return false; } - private async Task CanAccessUnassignedCiphersAsync(CurrentContextOrganization org) + private bool CanAccessUnassignedCiphers(CurrentContextOrganization org) { if (org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or diff --git a/src/Core/Vault/Repositories/ICipherRepository.cs b/src/Core/Vault/Repositories/ICipherRepository.cs index b094b42044..f6767fada2 100644 --- a/src/Core/Vault/Repositories/ICipherRepository.cs +++ b/src/Core/Vault/Repositories/ICipherRepository.cs @@ -3,6 +3,7 @@ using Bit.Core.KeyManagement.UserKey; using Bit.Core.Repositories; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Models.Data; +using Bit.Core.Vault.Queries; namespace Bit.Core.Vault.Repositories; diff --git a/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj b/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj index b26dc938cf..8feb455feb 100644 --- a/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj +++ b/src/Infrastructure.Dapper/Infrastructure.Dapper.csproj @@ -1,10 +1,5 @@ - - - $(WarningsNotAsErrors);CS8618 - - diff --git a/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj b/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj index a11a209b39..639d88524b 100644 --- a/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj +++ b/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj @@ -2,7 +2,7 @@ - $(WarningsNotAsErrors);CS0108;CS8632 + $(WarningsNotAsErrors);CS0108 diff --git a/test/Api.Test/Api.Test.csproj b/test/Api.Test/Api.Test.csproj index ec22583caf..d6b31ce930 100644 --- a/test/Api.Test/Api.Test.csproj +++ b/test/Api.Test/Api.Test.csproj @@ -2,8 +2,6 @@ false - - $(WarningsNotAsErrors);CS8620;CS0169 diff --git a/test/Core.Test/Core.Test.csproj b/test/Core.Test/Core.Test.csproj index cc19c50c35..c0f91a7bd3 100644 --- a/test/Core.Test/Core.Test.csproj +++ b/test/Core.Test/Core.Test.csproj @@ -2,8 +2,6 @@ false Bit.Core.Test - - $(WarningsNotAsErrors);CS4014 diff --git a/test/Core.Test/Vault/Queries/GetTasksForOrganizationQueryTests.cs b/test/Core.Test/Vault/Queries/GetTasksForOrganizationQueryTests.cs index 59ec7350da..f72a1f5f82 100644 --- a/test/Core.Test/Vault/Queries/GetTasksForOrganizationQueryTests.cs +++ b/test/Core.Test/Vault/Queries/GetTasksForOrganizationQueryTests.cs @@ -40,12 +40,12 @@ public class GetTasksForOrganizationQueryTests var result = await sutProvider.Sut.GetTasksAsync(org.Id, status); Assert.Equal(2, result.Count); - sutProvider.GetDependency().Received(1).AuthorizeAsync( + await sutProvider.GetDependency().Received(1).AuthorizeAsync( Arg.Any(), org, Arg.Is>( e => e.Contains(SecurityTaskOperations.ListAllForOrganization) ) ); - sutProvider.GetDependency().Received(1).GetManyByOrganizationIdStatusAsync(org.Id, SecurityTaskStatus.Pending); + await sutProvider.GetDependency().Received(1).GetManyByOrganizationIdStatusAsync(org.Id, SecurityTaskStatus.Pending); } [Theory, BitAutoData] @@ -82,11 +82,11 @@ public class GetTasksForOrganizationQueryTests await Assert.ThrowsAsync(() => sutProvider.Sut.GetTasksAsync(org.Id)); - sutProvider.GetDependency().Received(1).AuthorizeAsync( + await sutProvider.GetDependency().Received(1).AuthorizeAsync( Arg.Any(), org, Arg.Is>( e => e.Contains(SecurityTaskOperations.ListAllForOrganization) ) ); - sutProvider.GetDependency().Received(0).GetManyByOrganizationIdStatusAsync(org.Id, SecurityTaskStatus.Pending); + await sutProvider.GetDependency().Received(0).GetManyByOrganizationIdStatusAsync(org.Id, SecurityTaskStatus.Pending); } } From 80e7a0afd60da516481731fe4a9a3c1145aff6bc Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Fri, 9 May 2025 10:44:38 -0400 Subject: [PATCH 037/114] chore(captcha): [PM-15162] Remove captcha enforcement and issuing of bypass token * Remove captcha enforcement and issuing/verification of bypass token * Removed more captcha logic. * Removed logic to enforce failed login attempts * Linting. * Fixed order of initialization. * Fixed merge conflicts * Renamed registration finish response for clarity * Remove unnecessary mailService references. --- .../Api/Request/ICaptchaProtectedModel.cs | 6 - .../Auth/Models/Business/CaptchaResponse.cs | 9 -- .../Business/Tokenables/HCaptchaTokenable.cs | 43 ------ .../Services/ICaptchaValidationService.cs | 15 -- .../HCaptchaValidationService.cs | 132 ------------------ .../NoopCaptchaValidationService.cs | 18 --- .../Utilities/CaptchaProtectedAttribute.cs | 36 ----- .../Auth/FailedLoginAttempts.html.hbs | 31 ---- .../Auth/FailedLoginAttempts.text.hbs | 13 -- .../Auth/FailedTwoFactorAttempts.html.hbs | 31 ---- .../Auth/FailedTwoFactorAttempts.text.hbs | 13 -- src/Core/Services/IMailService.cs | 2 - .../Implementations/HandlebarsMailService.cs | 34 ----- .../NoopImplementations/NoopMailService.cs | 10 -- src/Core/Settings/GlobalSettings.cs | 11 -- .../Controllers/AccountsController.cs | 11 +- .../CustomValidatorRequestContext.cs | 6 +- .../RequestValidators/BaseRequestValidator.cs | 50 +------ .../CustomTokenRequestValidator.cs | 2 - .../ResourceOwnerPasswordValidator.cs | 36 ----- .../WebAuthnGrantValidator.cs | 2 - .../Request/Accounts/RegisterRequestModel.cs | 4 +- .../ICaptchaProtectedResponseModel.cs | 5 - .../Accounts/RegisterFinishResponseModel.cs | 10 ++ .../Accounts/RegisterResponseModel.cs | 14 -- src/Identity/appsettings.Production.json | 3 - src/Identity/appsettings.SelfHosted.json | 3 - .../Utilities/ServiceCollectionExtensions.cs | 18 --- .../Tokenables/HCaptchaTokenableTests.cs | 87 ------------ .../Business/Tokenables/SsoTokenableTests.cs | 2 +- .../Controllers/AccountsControllerTests.cs | 4 - .../BaseRequestValidatorTests.cs | 81 ----------- .../BaseRequestValidatorTestWrapper.cs | 2 - .../Factories/WebApplicationFactoryBase.cs | 3 - util/Setup/Configuration.cs | 3 - util/Setup/NginxConfigBuilder.cs | 2 - util/Setup/Templates/NginxConfig.hbs | 10 -- 37 files changed, 22 insertions(+), 740 deletions(-) delete mode 100644 src/Core/Auth/Models/Api/Request/ICaptchaProtectedModel.cs delete mode 100644 src/Core/Auth/Models/Business/CaptchaResponse.cs delete mode 100644 src/Core/Auth/Models/Business/Tokenables/HCaptchaTokenable.cs delete mode 100644 src/Core/Auth/Services/ICaptchaValidationService.cs delete mode 100644 src/Core/Auth/Services/Implementations/HCaptchaValidationService.cs delete mode 100644 src/Core/Auth/Services/NoopImplementations/NoopCaptchaValidationService.cs delete mode 100644 src/Core/Auth/Utilities/CaptchaProtectedAttribute.cs delete mode 100644 src/Core/MailTemplates/Handlebars/Auth/FailedLoginAttempts.html.hbs delete mode 100644 src/Core/MailTemplates/Handlebars/Auth/FailedLoginAttempts.text.hbs delete mode 100644 src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempts.html.hbs delete mode 100644 src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempts.text.hbs delete mode 100644 src/Identity/Models/Response/Accounts/ICaptchaProtectedResponseModel.cs create mode 100644 src/Identity/Models/Response/Accounts/RegisterFinishResponseModel.cs delete mode 100644 src/Identity/Models/Response/Accounts/RegisterResponseModel.cs delete mode 100644 test/Core.Test/Auth/Models/Business/Tokenables/HCaptchaTokenableTests.cs diff --git a/src/Core/Auth/Models/Api/Request/ICaptchaProtectedModel.cs b/src/Core/Auth/Models/Api/Request/ICaptchaProtectedModel.cs deleted file mode 100644 index 6968a904b0..0000000000 --- a/src/Core/Auth/Models/Api/Request/ICaptchaProtectedModel.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Bit.Core.Auth.Models.Api; - -public interface ICaptchaProtectedModel -{ - string CaptchaResponse { get; set; } -} diff --git a/src/Core/Auth/Models/Business/CaptchaResponse.cs b/src/Core/Auth/Models/Business/CaptchaResponse.cs deleted file mode 100644 index 1a4b039ec0..0000000000 --- a/src/Core/Auth/Models/Business/CaptchaResponse.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Bit.Core.Auth.Models.Business; - -public class CaptchaResponse -{ - public bool Success { get; set; } - public bool MaybeBot { get; set; } - public bool IsBot { get; set; } - public double Score { get; set; } -} diff --git a/src/Core/Auth/Models/Business/Tokenables/HCaptchaTokenable.cs b/src/Core/Auth/Models/Business/Tokenables/HCaptchaTokenable.cs deleted file mode 100644 index 72994563c1..0000000000 --- a/src/Core/Auth/Models/Business/Tokenables/HCaptchaTokenable.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Text.Json.Serialization; -using Bit.Core.Entities; -using Bit.Core.Tokens; - -namespace Bit.Core.Auth.Models.Business.Tokenables; - -public class HCaptchaTokenable : ExpiringTokenable -{ - private const double _tokenLifetimeInHours = (double)5 / 60; // 5 minutes - public const string ClearTextPrefix = "BWCaptchaBypass_"; - public const string DataProtectorPurpose = "CaptchaServiceDataProtector"; - public const string TokenIdentifier = "CaptchaBypassToken"; - - public string Identifier { get; set; } = TokenIdentifier; - public Guid Id { get; set; } - public string Email { get; set; } - - [JsonConstructor] - public HCaptchaTokenable() - { - ExpirationDate = DateTime.UtcNow.AddHours(_tokenLifetimeInHours); - } - - public HCaptchaTokenable(User user) : this() - { - Id = user?.Id ?? default; - Email = user?.Email; - } - - public bool TokenIsValid(User user) - { - if (Id == default || Email == default || user == null) - { - return false; - } - - return Id == user.Id && - Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase); - } - - // Validates deserialized - protected override bool TokenIsValid() => Identifier == TokenIdentifier && Id != default && !string.IsNullOrWhiteSpace(Email); -} diff --git a/src/Core/Auth/Services/ICaptchaValidationService.cs b/src/Core/Auth/Services/ICaptchaValidationService.cs deleted file mode 100644 index 8547c68f7a..0000000000 --- a/src/Core/Auth/Services/ICaptchaValidationService.cs +++ /dev/null @@ -1,15 +0,0 @@ -using Bit.Core.Auth.Models.Business; -using Bit.Core.Context; -using Bit.Core.Entities; - -namespace Bit.Core.Auth.Services; - -public interface ICaptchaValidationService -{ - string SiteKey { get; } - string SiteKeyResponseKeyName { get; } - bool RequireCaptchaValidation(ICurrentContext currentContext, User user = null); - Task ValidateCaptchaResponseAsync(string captchResponse, string clientIpAddress, - User user = null); - string GenerateCaptchaBypassToken(User user); -} diff --git a/src/Core/Auth/Services/Implementations/HCaptchaValidationService.cs b/src/Core/Auth/Services/Implementations/HCaptchaValidationService.cs deleted file mode 100644 index cdd6c2017e..0000000000 --- a/src/Core/Auth/Services/Implementations/HCaptchaValidationService.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System.Net.Http.Json; -using System.Text.Json.Serialization; -using Bit.Core.Auth.Models.Business; -using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Context; -using Bit.Core.Entities; -using Bit.Core.Settings; -using Bit.Core.Tokens; -using Microsoft.Extensions.Logging; - -namespace Bit.Core.Auth.Services; - -public class HCaptchaValidationService : ICaptchaValidationService -{ - private readonly ILogger _logger; - private readonly IHttpClientFactory _httpClientFactory; - private readonly GlobalSettings _globalSettings; - private readonly IDataProtectorTokenFactory _tokenizer; - - public HCaptchaValidationService( - ILogger logger, - IHttpClientFactory httpClientFactory, - IDataProtectorTokenFactory tokenizer, - GlobalSettings globalSettings) - { - _logger = logger; - _httpClientFactory = httpClientFactory; - _globalSettings = globalSettings; - _tokenizer = tokenizer; - } - - public string SiteKeyResponseKeyName => "HCaptcha_SiteKey"; - public string SiteKey => _globalSettings.Captcha.HCaptchaSiteKey; - - public string GenerateCaptchaBypassToken(User user) => _tokenizer.Protect(new HCaptchaTokenable(user)); - - public async Task ValidateCaptchaResponseAsync(string captchaResponse, string clientIpAddress, - User user = null) - { - var response = new CaptchaResponse { Success = false }; - if (string.IsNullOrWhiteSpace(captchaResponse)) - { - return response; - } - - if (user != null && ValidateCaptchaBypassToken(captchaResponse, user)) - { - response.Success = true; - return response; - } - - var httpClient = _httpClientFactory.CreateClient("HCaptchaValidationService"); - - var requestMessage = new HttpRequestMessage - { - Method = HttpMethod.Post, - RequestUri = new Uri("https://hcaptcha.com/siteverify"), - Content = new FormUrlEncodedContent(new Dictionary - { - { "response", captchaResponse.TrimStart("hcaptcha|".ToCharArray()) }, - { "secret", _globalSettings.Captcha.HCaptchaSecretKey }, - { "sitekey", SiteKey }, - { "remoteip", clientIpAddress } - }) - }; - - HttpResponseMessage responseMessage; - try - { - responseMessage = await httpClient.SendAsync(requestMessage); - } - catch (Exception e) - { - _logger.LogError(11389, e, "Unable to verify with HCaptcha."); - return response; - } - - if (!responseMessage.IsSuccessStatusCode) - { - return response; - } - - using var hcaptchaResponse = await responseMessage.Content.ReadFromJsonAsync(); - response.Success = hcaptchaResponse.Success; - var score = hcaptchaResponse.Score.GetValueOrDefault(); - response.MaybeBot = score >= _globalSettings.Captcha.MaybeBotScoreThreshold; - response.IsBot = score >= _globalSettings.Captcha.IsBotScoreThreshold; - response.Score = score; - return response; - } - - public bool RequireCaptchaValidation(ICurrentContext currentContext, User user = null) - { - if (user == null) - { - return currentContext.IsBot || _globalSettings.Captcha.ForceCaptchaRequired; - } - - var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts; - var failedLoginCount = user?.FailedLoginCount ?? 0; - var requireOnCloud = !_globalSettings.SelfHosted && !user.EmailVerified && - user.CreationDate < DateTime.UtcNow.AddHours(-24); - return currentContext.IsBot || - _globalSettings.Captcha.ForceCaptchaRequired || - requireOnCloud || - failedLoginCeiling > 0 && failedLoginCount >= failedLoginCeiling; - } - - private static bool TokenIsValidApiKey(string bypassToken, User user) => - !string.IsNullOrWhiteSpace(bypassToken) && user != null && user.ApiKey == bypassToken; - - private bool TokenIsValidCaptchaBypassToken(string encryptedToken, User user) - { - return _tokenizer.TryUnprotect(encryptedToken, out var data) && - data.Valid && data.TokenIsValid(user); - } - - private bool ValidateCaptchaBypassToken(string bypassToken, User user) => - TokenIsValidApiKey(bypassToken, user) || TokenIsValidCaptchaBypassToken(bypassToken, user); - - public class HCaptchaResponse : IDisposable - { - [JsonPropertyName("success")] - public bool Success { get; set; } - [JsonPropertyName("score")] - public double? Score { get; set; } - [JsonPropertyName("score_reason")] - public List ScoreReason { get; set; } - - public void Dispose() { } - } -} diff --git a/src/Core/Auth/Services/NoopImplementations/NoopCaptchaValidationService.cs b/src/Core/Auth/Services/NoopImplementations/NoopCaptchaValidationService.cs deleted file mode 100644 index 47e1a38567..0000000000 --- a/src/Core/Auth/Services/NoopImplementations/NoopCaptchaValidationService.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Bit.Core.Auth.Models.Business; -using Bit.Core.Context; -using Bit.Core.Entities; - -namespace Bit.Core.Auth.Services; - -public class NoopCaptchaValidationService : ICaptchaValidationService -{ - public string SiteKeyResponseKeyName => null; - public string SiteKey => null; - public bool RequireCaptchaValidation(ICurrentContext currentContext, User user = null) => false; - public string GenerateCaptchaBypassToken(User user) => ""; - public Task ValidateCaptchaResponseAsync(string captchaResponse, string clientIpAddress, - User user = null) - { - return Task.FromResult(new CaptchaResponse { Success = true }); - } -} diff --git a/src/Core/Auth/Utilities/CaptchaProtectedAttribute.cs b/src/Core/Auth/Utilities/CaptchaProtectedAttribute.cs deleted file mode 100644 index 052f178165..0000000000 --- a/src/Core/Auth/Utilities/CaptchaProtectedAttribute.cs +++ /dev/null @@ -1,36 +0,0 @@ -using Bit.Core.Auth.Models.Api; -using Bit.Core.Auth.Services; -using Bit.Core.Context; -using Bit.Core.Exceptions; -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.Extensions.DependencyInjection; - -namespace Bit.Core.Auth.Utilities; - -public class CaptchaProtectedAttribute : ActionFilterAttribute -{ - public string ModelParameterName { get; set; } = "model"; - - public override void OnActionExecuting(ActionExecutingContext context) - { - var currentContext = context.HttpContext.RequestServices.GetRequiredService(); - var captchaValidationService = context.HttpContext.RequestServices.GetRequiredService(); - - if (captchaValidationService.RequireCaptchaValidation(currentContext, null)) - { - var captchaResponse = (context.ActionArguments[ModelParameterName] as ICaptchaProtectedModel)?.CaptchaResponse; - - if (string.IsNullOrWhiteSpace(captchaResponse)) - { - throw new BadRequestException(captchaValidationService.SiteKeyResponseKeyName, captchaValidationService.SiteKey); - } - - var captchaValidationResponse = captchaValidationService.ValidateCaptchaResponseAsync(captchaResponse, - currentContext.IpAddress, null).GetAwaiter().GetResult(); - if (!captchaValidationResponse.Success || captchaValidationResponse.IsBot) - { - throw new BadRequestException("Captcha is invalid. Please refresh and try again"); - } - } - } -} diff --git a/src/Core/MailTemplates/Handlebars/Auth/FailedLoginAttempts.html.hbs b/src/Core/MailTemplates/Handlebars/Auth/FailedLoginAttempts.html.hbs deleted file mode 100644 index 43531ef242..0000000000 --- a/src/Core/MailTemplates/Handlebars/Auth/FailedLoginAttempts.html.hbs +++ /dev/null @@ -1,31 +0,0 @@ -{{#>FullHtmlLayout}} -
- Verify your email address below to finish signing up for your free trial. + {{VerifyYourEmailHTMLCopy}}
- - - - - - - - - - - - - - - -
- Additional security has been placed on your Bitwarden account. -
- We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha. -
- Account: {{AffectedEmail}}
- Date: {{TheDate}} at {{TheTime}} {{TimeZone}}
- IP Address: {{IpAddress}}
-
- If this was you, you can remove the captcha requirement by successfully logging in. -
- If this was not you, don't worry. The login attempt was not successful and your account has been given additional protection. -
-{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/Auth/FailedLoginAttempts.text.hbs b/src/Core/MailTemplates/Handlebars/Auth/FailedLoginAttempts.text.hbs deleted file mode 100644 index 3393210e4e..0000000000 --- a/src/Core/MailTemplates/Handlebars/Auth/FailedLoginAttempts.text.hbs +++ /dev/null @@ -1,13 +0,0 @@ -{{#>BasicTextLayout}} -Additional security has been placed on your Bitwarden account. - -We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha. - -Account: {{AffectedEmail}} -Date: {{TheDate}} at {{TheTime}} {{TimeZone}} -IP Address: {{IpAddress}} - -If this was you, you can remove the captcha requirement by successfully logging in. - -If this was not you, don't worry. The login attempt was not successful and your account has been given additional protection. -{{/BasicTextLayout}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempts.html.hbs b/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempts.html.hbs deleted file mode 100644 index d73775f8e8..0000000000 --- a/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempts.html.hbs +++ /dev/null @@ -1,31 +0,0 @@ -{{#>FullHtmlLayout}} - - - - - - - - - - - - - - - - -
- Additional security has been placed on your Bitwarden account. -
- We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha. -
- Account: {{AffectedEmail}}
- Date: {{TheDate}} at {{TheTime}} {{TimeZone}}
- IP Address: {{IpAddress}}
-
- If this was you, you can remove the captcha requirement by successfully logging in. If you're having trouble with two step login, you can login using a recovery code. -
- If this was not you, you should change your master password immediately. You can view our tips for selecting a secure master password here. -
-{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempts.text.hbs b/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempts.text.hbs deleted file mode 100644 index e742d35578..0000000000 --- a/src/Core/MailTemplates/Handlebars/Auth/FailedTwoFactorAttempts.text.hbs +++ /dev/null @@ -1,13 +0,0 @@ -{{#>BasicTextLayout}} -Additional security has been placed on your Bitwarden account. - -We've detected several failed attempts to log into your Bitwarden account. Future login attempts for your account will be protected by a captcha. - -Account: {{AffectedEmail}} -Date: {{TheDate}} at {{TheTime}} {{TimeZone}} -IP Address: {{IpAddress}} - -If this was you, you can remove the captcha requirement by successfully logging in. If you're having trouble with two step login, you can login using a recovery code (https://bitwarden.com/help/two-step-recovery-code/). - -If this was not you, you should change your master password (https://bitwarden.com/help/master-password/#change-master-password) immediately. You can view our tips for selecting a secure master password here (https://bitwarden.com/blog/picking-the-right-password-for-your-password-manager/). -{{/BasicTextLayout}} \ No newline at end of file diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 11d9603a07..7de75a5143 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -88,8 +88,6 @@ public interface IMailService Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail); Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, DateTime expirationDate); Task SendOTPEmailAsync(string email, string token); - Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip); - Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip); Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName); Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName); Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable ownerEmails); diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 3266cc9c2e..315e180721 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -1137,40 +1137,6 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } - public async Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip) - { - var message = CreateDefaultMessage("Failed login attempts detected", email); - var model = new FailedAuthAttemptsModel() - { - TheDate = utcNow.ToLongDateString(), - TheTime = utcNow.ToShortTimeString(), - TimeZone = _utcTimeZoneDisplay, - IpAddress = ip, - AffectedEmail = email - - }; - await AddMessageContentAsync(message, "Auth.FailedLoginAttempts", model); - message.Category = "FailedLoginAttempts"; - await _mailDeliveryService.SendEmailAsync(message); - } - - public async Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip) - { - var message = CreateDefaultMessage("Failed login attempts detected", email); - var model = new FailedAuthAttemptsModel() - { - TheDate = utcNow.ToLongDateString(), - TheTime = utcNow.ToShortTimeString(), - TimeZone = _utcTimeZoneDisplay, - IpAddress = ip, - AffectedEmail = email - - }; - await AddMessageContentAsync(message, "Auth.FailedTwoFactorAttempts", model); - message.Category = "FailedTwoFactorAttempts"; - await _mailDeliveryService.SendEmailAsync(message); - } - public async Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName) { var message = CreateDefaultMessage("Domain not verified", adminEmails); diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index bbad5965f4..83bc3ba7cf 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -268,16 +268,6 @@ public class NoopMailService : IMailService return Task.FromResult(0); } - public Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip) - { - return Task.FromResult(0); - } - - public Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip) - { - return Task.FromResult(0); - } - public Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName) { return Task.FromResult(0); diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 519889db45..d31e18b955 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -45,7 +45,6 @@ public class GlobalSettings : IGlobalSettings public virtual bool EnableCloudCommunication { get; set; } = false; public virtual int OrganizationInviteExpirationHours { get; set; } = 120; // 5 days public virtual string EventGridKey { get; set; } - public virtual CaptchaSettings Captcha { get; set; } = new CaptchaSettings(); public virtual IInstallationSettings Installation { get; set; } = new InstallationSettings(); public virtual IBaseServiceUriSettings BaseServiceUri { get; set; } public virtual string DatabaseProvider { get; set; } @@ -629,16 +628,6 @@ public class GlobalSettings : IGlobalSettings public bool EnforceSsoPolicyForAllUsers { get; set; } } - public class CaptchaSettings - { - public bool ForceCaptchaRequired { get; set; } = false; - public string HCaptchaSecretKey { get; set; } - public string HCaptchaSiteKey { get; set; } - public int MaximumFailedLoginAttempts { get; set; } - public double MaybeBotScoreThreshold { get; set; } = double.MaxValue; - public double IsBotScoreThreshold { get; set; } = double.MaxValue; - } - public class StripeSettings { public string ApiKey { get; set; } diff --git a/src/Identity/Controllers/AccountsController.cs b/src/Identity/Controllers/AccountsController.cs index fd42074359..80e9536ea3 100644 --- a/src/Identity/Controllers/AccountsController.cs +++ b/src/Identity/Controllers/AccountsController.cs @@ -5,7 +5,6 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Api.Response.Accounts; using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Auth.Services; using Bit.Core.Auth.UserFeatures.Registration; using Bit.Core.Auth.UserFeatures.WebAuthnLogin; using Bit.Core.Context; @@ -37,7 +36,6 @@ public class AccountsController : Controller private readonly ILogger _logger; private readonly IUserRepository _userRepository; private readonly IRegisterUserCommand _registerUserCommand; - private readonly ICaptchaValidationService _captchaValidationService; private readonly IDataProtectorTokenFactory _assertionOptionsDataProtector; private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand; private readonly ISendVerificationEmailForRegistrationCommand _sendVerificationEmailForRegistrationCommand; @@ -85,7 +83,6 @@ public class AccountsController : Controller ILogger logger, IUserRepository userRepository, IRegisterUserCommand registerUserCommand, - ICaptchaValidationService captchaValidationService, IDataProtectorTokenFactory assertionOptionsDataProtector, IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand, ISendVerificationEmailForRegistrationCommand sendVerificationEmailForRegistrationCommand, @@ -99,7 +96,6 @@ public class AccountsController : Controller _logger = logger; _userRepository = userRepository; _registerUserCommand = registerUserCommand; - _captchaValidationService = captchaValidationService; _assertionOptionsDataProtector = assertionOptionsDataProtector; _getWebAuthnLoginCredentialAssertionOptionsCommand = getWebAuthnLoginCredentialAssertionOptionsCommand; _sendVerificationEmailForRegistrationCommand = sendVerificationEmailForRegistrationCommand; @@ -167,7 +163,7 @@ public class AccountsController : Controller } [HttpPost("register/finish")] - public async Task PostRegisterFinish([FromBody] RegisterFinishRequestModel model) + public async Task PostRegisterFinish([FromBody] RegisterFinishRequestModel model) { var user = model.ToUser(); @@ -208,12 +204,11 @@ public class AccountsController : Controller } } - private RegisterResponseModel ProcessRegistrationResult(IdentityResult result, User user) + private RegisterFinishResponseModel ProcessRegistrationResult(IdentityResult result, User user) { if (result.Succeeded) { - var captchaBypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user); - return new RegisterResponseModel(captchaBypassToken); + return new RegisterFinishResponseModel(); } foreach (var error in result.Errors.Where(e => e.Code != "DuplicateUserName")) diff --git a/src/Identity/IdentityServer/CustomValidatorRequestContext.cs b/src/Identity/IdentityServer/CustomValidatorRequestContext.cs index bce460c5c4..eb441e7941 100644 --- a/src/Identity/IdentityServer/CustomValidatorRequestContext.cs +++ b/src/Identity/IdentityServer/CustomValidatorRequestContext.cs @@ -1,5 +1,4 @@ -using Bit.Core.Auth.Models.Business; -using Bit.Core.Entities; +using Bit.Core.Entities; using Duende.IdentityServer.Validation; namespace Bit.Identity.IdentityServer; @@ -9,7 +8,7 @@ public class CustomValidatorRequestContext public User User { get; set; } /// /// This is the device that the user is using to authenticate. It can be either known or unknown. - /// We set it here since the ResourceOwnerPasswordValidator needs the device to know if CAPTCHA is required. + /// We set it here since the ResourceOwnerPasswordValidator needs the device to do device validation. /// The option to set it here saves a trip to the database. /// public Device Device { get; set; } @@ -39,5 +38,4 @@ public class CustomValidatorRequestContext /// This will be null if the authentication request is successful. ///
public Dictionary CustomResponse { get; set; } - public CaptchaResponse CaptchaResponse { get; set; } } diff --git a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs index 8b7034c9d7..9afdcacf14 100644 --- a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs @@ -29,7 +29,6 @@ public abstract class BaseRequestValidator where T : class private readonly IDeviceValidator _deviceValidator; private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator; private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IMailService _mailService; private readonly ILogger _logger; private readonly GlobalSettings _globalSettings; private readonly IUserRepository _userRepository; @@ -49,7 +48,6 @@ public abstract class BaseRequestValidator where T : class IDeviceValidator deviceValidator, ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, IOrganizationUserRepository organizationUserRepository, - IMailService mailService, ILogger logger, ICurrentContext currentContext, GlobalSettings globalSettings, @@ -66,7 +64,6 @@ public abstract class BaseRequestValidator where T : class _deviceValidator = deviceValidator; _twoFactorAuthenticationValidator = twoFactorAuthenticationValidator; _organizationUserRepository = organizationUserRepository; - _mailService = mailService; _logger = logger; CurrentContext = currentContext; _globalSettings = globalSettings; @@ -81,23 +78,12 @@ public abstract class BaseRequestValidator where T : class protected async Task ValidateAsync(T context, ValidatedTokenRequest request, CustomValidatorRequestContext validatorContext) { - // 1. We need to check if the user is a bot and if their master password hash is correct. - var isBot = validatorContext.CaptchaResponse?.IsBot ?? false; + // 1. We need to check if the user's master password hash is correct. var valid = await ValidateContextAsync(context, validatorContext); var user = validatorContext.User; - if (!valid || isBot) + if (!valid) { - if (isBot) - { - _logger.LogInformation(Constants.BypassFiltersEventId, - "Login attempt for {UserName} detected as a captcha bot with score {CaptchaScore}.", - request.UserName, validatorContext.CaptchaResponse.Score); - } - - if (!valid) - { - await UpdateFailedAuthDetailsAsync(user, false, !validatorContext.KnownDevice); - } + await UpdateFailedAuthDetailsAsync(user); await BuildErrorResultAsync("Username or password is incorrect. Try again.", false, context, user); return; @@ -167,7 +153,7 @@ public abstract class BaseRequestValidator where T : class } else { - await UpdateFailedAuthDetailsAsync(user, true, !validatorContext.KnownDevice); + await UpdateFailedAuthDetailsAsync(user); await BuildErrorResultAsync("Two-step token is invalid. Try again.", true, context, user); } return; @@ -379,7 +365,7 @@ public abstract class BaseRequestValidator where T : class await _userRepository.ReplaceAsync(user); } - private async Task UpdateFailedAuthDetailsAsync(User user, bool twoFactorInvalid, bool unknownDevice) + private async Task UpdateFailedAuthDetailsAsync(User user) { if (user == null) { @@ -390,32 +376,6 @@ public abstract class BaseRequestValidator where T : class user.FailedLoginCount = ++user.FailedLoginCount; user.LastFailedLoginDate = user.RevisionDate = utcNow; await _userRepository.ReplaceAsync(user); - - if (ValidateFailedAuthEmailConditions(unknownDevice, user)) - { - if (twoFactorInvalid) - { - await _mailService.SendFailedTwoFactorAttemptsEmailAsync(user.Email, utcNow, CurrentContext.IpAddress); - } - else - { - await _mailService.SendFailedLoginAttemptsEmailAsync(user.Email, utcNow, CurrentContext.IpAddress); - } - } - } - - /// - /// checks to see if a user is trying to log into a new device - /// and has reached the maximum number of failed login attempts. - /// - /// boolean - /// current user - /// - private bool ValidateFailedAuthEmailConditions(bool unknownDevice, User user) - { - var failedLoginCeiling = _globalSettings.Captcha.MaximumFailedLoginAttempts; - var failedLoginCount = user?.FailedLoginCount ?? 0; - return unknownDevice && failedLoginCeiling > 0 && failedLoginCount == failedLoginCeiling; } private async Task GetMasterPasswordPolicyAsync(User user) diff --git a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs index 841cd14137..6f2d81bd1b 100644 --- a/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/CustomTokenRequestValidator.cs @@ -35,7 +35,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator logger, ICurrentContext currentContext, GlobalSettings globalSettings, @@ -53,7 +52,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator _userManager; private readonly ICurrentContext _currentContext; - private readonly ICaptchaValidationService _captchaValidationService; private readonly IAuthRequestRepository _authRequestRepository; private readonly IDeviceValidator _deviceValidator; public ResourceOwnerPasswordValidator( @@ -31,11 +29,9 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator logger, ICurrentContext currentContext, GlobalSettings globalSettings, - ICaptchaValidationService captchaValidationService, IAuthRequestRepository authRequestRepository, IUserRepository userRepository, IPolicyService policyService, @@ -50,7 +46,6 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator - { - { _captchaValidationService.SiteKeyResponseKeyName, _captchaValidationService.SiteKey }, - }); - return; - } - - validatorContext.CaptchaResponse = await _captchaValidationService.ValidateCaptchaResponseAsync( - captchaResponse, _currentContext.IpAddress, user); - if (!validatorContext.CaptchaResponse.Success) - { - await BuildErrorResultAsync("Captcha is invalid. Please refresh and try again", false, context, null); - return; - } - bypassToken = _captchaValidationService.GenerateCaptchaBypassToken(user); - } - await ValidateAsync(context, context.Request, validatorContext); - if (context.Result.CustomResponse != null && bypassToken != null) - { - context.Result.CustomResponse["CaptchaBypassToken"] = bypassToken; - } } protected async override Task ValidateContextAsync(ResourceOwnerPasswordValidationContext context, diff --git a/src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs b/src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs index 654edeabe8..76949eb5f7 100644 --- a/src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/WebAuthnGrantValidator.cs @@ -35,7 +35,6 @@ public class WebAuthnGrantValidator : BaseRequestValidator logger, ICurrentContext currentContext, GlobalSettings globalSettings, @@ -54,7 +53,6 @@ public class WebAuthnGrantValidator : BaseRequestValidator>>()) ); - services.AddSingleton>(serviceProvider => - new DataProtectorTokenFactory( - HCaptchaTokenable.ClearTextPrefix, - HCaptchaTokenable.DataProtectorPurpose, - serviceProvider.GetDataProtectionProvider(), - serviceProvider.GetRequiredService>>()) - ); - services.AddSingleton>(serviceProvider => new DataProtectorTokenFactory( SsoTokenable.ClearTextPrefix, @@ -401,16 +393,6 @@ public static class ServiceCollectionExtensions { services.AddSingleton(); } - - if (CoreHelpers.SettingHasValue(globalSettings.Captcha?.HCaptchaSecretKey) && - CoreHelpers.SettingHasValue(globalSettings.Captcha?.HCaptchaSiteKey)) - { - services.AddSingleton(); - } - else - { - services.AddSingleton(); - } } public static void AddOosServices(this IServiceCollection services) diff --git a/test/Core.Test/Auth/Models/Business/Tokenables/HCaptchaTokenableTests.cs b/test/Core.Test/Auth/Models/Business/Tokenables/HCaptchaTokenableTests.cs deleted file mode 100644 index 56533bab7a..0000000000 --- a/test/Core.Test/Auth/Models/Business/Tokenables/HCaptchaTokenableTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -using AutoFixture.Xunit2; -using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Entities; -using Bit.Core.Tokens; -using Bit.Test.Common.AutoFixture.Attributes; -using Xunit; - -namespace Bit.Core.Test.Auth.Models.Business.Tokenables; - -public class HCaptchaTokenableTests -{ - [Fact] - public void CanHandleNullUser() - { - var token = new HCaptchaTokenable(null); - - Assert.Equal(default, token.Id); - Assert.Equal(default, token.Email); - } - - [Fact] - public void TokenWithNullUserIsInvalid() - { - var token = new HCaptchaTokenable(null) - { - ExpirationDate = DateTime.UtcNow + TimeSpan.FromDays(1) - }; - - Assert.False(token.Valid); - } - - [Theory, BitAutoData] - public void TokenValidityCheckNullUserIdIsInvalid(User user) - { - var token = new HCaptchaTokenable(user) - { - ExpirationDate = DateTime.UtcNow + TimeSpan.FromDays(1) - }; - - Assert.False(token.TokenIsValid(null)); - } - - [Theory, AutoData] - public void CanUpdateExpirationToNonStandard(User user) - { - var token = new HCaptchaTokenable(user) - { - ExpirationDate = DateTime.MinValue - }; - - Assert.Equal(DateTime.MinValue, token.ExpirationDate, TimeSpan.FromMilliseconds(10)); - } - - [Theory, AutoData] - public void SetsDataFromUser(User user) - { - var token = new HCaptchaTokenable(user); - - Assert.Equal(user.Id, token.Id); - Assert.Equal(user.Email, token.Email); - } - - [Theory, AutoData] - public void SerializationSetsCorrectDateTime(User user) - { - var expectedDateTime = DateTime.UtcNow.AddHours(-5); - var token = new HCaptchaTokenable(user) - { - ExpirationDate = expectedDateTime - }; - - var result = Tokenable.FromToken(token.ToToken()); - - Assert.Equal(expectedDateTime, result.ExpirationDate, TimeSpan.FromMilliseconds(10)); - } - - [Theory, AutoData] - public void IsInvalidIfIdentifierIsWrong(User user) - { - var token = new HCaptchaTokenable(user) - { - Identifier = "not correct" - }; - - Assert.False(token.Valid); - } -} diff --git a/test/Core.Test/Auth/Models/Business/Tokenables/SsoTokenableTests.cs b/test/Core.Test/Auth/Models/Business/Tokenables/SsoTokenableTests.cs index 4d95a1c196..ab393203ab 100644 --- a/test/Core.Test/Auth/Models/Business/Tokenables/SsoTokenableTests.cs +++ b/test/Core.Test/Auth/Models/Business/Tokenables/SsoTokenableTests.cs @@ -67,7 +67,7 @@ public class SsoTokenableTests ExpirationDate = expectedDateTime }; - var result = Tokenable.FromToken(token.ToToken()); + var result = Tokenable.FromToken(token.ToToken()); Assert.Equal(expectedDateTime, result.ExpirationDate, TimeSpan.FromMilliseconds(10)); } diff --git a/test/Identity.Test/Controllers/AccountsControllerTests.cs b/test/Identity.Test/Controllers/AccountsControllerTests.cs index e36f7f37b6..a045490862 100644 --- a/test/Identity.Test/Controllers/AccountsControllerTests.cs +++ b/test/Identity.Test/Controllers/AccountsControllerTests.cs @@ -3,7 +3,6 @@ using System.Text; using Bit.Core; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Business.Tokenables; -using Bit.Core.Auth.Services; using Bit.Core.Auth.UserFeatures.Registration; using Bit.Core.Auth.UserFeatures.WebAuthnLogin; using Bit.Core.Context; @@ -38,7 +37,6 @@ public class AccountsControllerTests : IDisposable private readonly ILogger _logger; private readonly IUserRepository _userRepository; private readonly IRegisterUserCommand _registerUserCommand; - private readonly ICaptchaValidationService _captchaValidationService; private readonly IDataProtectorTokenFactory _assertionOptionsDataProtector; private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand; private readonly ISendVerificationEmailForRegistrationCommand _sendVerificationEmailForRegistrationCommand; @@ -54,7 +52,6 @@ public class AccountsControllerTests : IDisposable _logger = Substitute.For>(); _userRepository = Substitute.For(); _registerUserCommand = Substitute.For(); - _captchaValidationService = Substitute.For(); _assertionOptionsDataProtector = Substitute.For>(); _getWebAuthnLoginCredentialAssertionOptionsCommand = Substitute.For(); _sendVerificationEmailForRegistrationCommand = Substitute.For(); @@ -68,7 +65,6 @@ public class AccountsControllerTests : IDisposable _logger, _userRepository, _registerUserCommand, - _captchaValidationService, _assertionOptionsDataProtector, _getWebAuthnLoginCredentialAssertionOptionsCommand, _sendVerificationEmailForRegistrationCommand, diff --git a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs index 1d58b62b02..9eb17da88a 100644 --- a/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/BaseRequestValidatorTests.cs @@ -33,7 +33,6 @@ public class BaseRequestValidatorTests private readonly IDeviceValidator _deviceValidator; private readonly ITwoFactorAuthenticationValidator _twoFactorAuthenticationValidator; private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IMailService _mailService; private readonly ILogger _logger; private readonly ICurrentContext _currentContext; private readonly GlobalSettings _globalSettings; @@ -54,7 +53,6 @@ public class BaseRequestValidatorTests _deviceValidator = Substitute.For(); _twoFactorAuthenticationValidator = Substitute.For(); _organizationUserRepository = Substitute.For(); - _mailService = Substitute.For(); _logger = Substitute.For>(); _currentContext = Substitute.For(); _globalSettings = Substitute.For(); @@ -72,7 +70,6 @@ public class BaseRequestValidatorTests _deviceValidator, _twoFactorAuthenticationValidator, _organizationUserRepository, - _mailService, _logger, _currentContext, _globalSettings, @@ -84,36 +81,6 @@ public class BaseRequestValidatorTests _policyRequirementQuery); } - /* Logic path - * ValidateAsync -> _Logger.LogInformation - * |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync - * |-> SetErrorResult - */ - [Theory, BitAutoData] - public async Task ValidateAsync_IsBot_UserNotNull_ShouldBuildErrorResult_ShouldLogFailedLoginEvent( - [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, - CustomValidatorRequestContext requestContext, - GrantValidationResult grantResult) - { - // Arrange - var context = CreateContext(tokenRequest, requestContext, grantResult); - - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = true; - _sut.isValid = true; - - // Act - await _sut.ValidateAsync(context); - - var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"]; - - // Assert - await _eventService.Received(1) - .LogUserEventAsync(context.CustomValidatorRequestContext.User.Id, - EventType.User_FailedLogIn); - Assert.True(context.GrantResult.IsError); - Assert.Equal("Username or password is incorrect. Try again.", errorResponse.Message); - } - /* Logic path * ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync * |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync @@ -128,8 +95,6 @@ public class BaseRequestValidatorTests { // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; - _globalSettings.Captcha.Returns(new GlobalSettings.CaptchaSettings()); _globalSettings.SelfHosted = true; _sut.isValid = false; @@ -142,44 +107,6 @@ public class BaseRequestValidatorTests Assert.Equal("Username or password is incorrect. Try again.", errorResponse.Message); } - /* Logic path - * ValidateAsync -> UpdateFailedAuthDetailsAsync -> _mailService.SendFailedLoginAttemptsEmailAsync - * |-> BuildErrorResultAsync -> _eventService.LogUserEventAsync - * |-> SetErrorResult - */ - [Theory, BitAutoData] - public async Task ValidateAsync_ContextNotValid_MaxAttemptLogin_ShouldSendEmail( - [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, - CustomValidatorRequestContext requestContext, - GrantValidationResult grantResult) - { - // Arrange - var context = CreateContext(tokenRequest, requestContext, grantResult); - - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; - // This needs to be n-1 of the max failed login attempts - context.CustomValidatorRequestContext.User.FailedLoginCount = 2; - context.CustomValidatorRequestContext.KnownDevice = false; - - _globalSettings.Captcha.Returns( - new GlobalSettings.CaptchaSettings - { - MaximumFailedLoginAttempts = 3 - }); - _sut.isValid = false; - - // Act - await _sut.ValidateAsync(context); - - // Assert - await _mailService.Received(1) - .SendFailedLoginAttemptsEmailAsync( - Arg.Any(), Arg.Any(), Arg.Any()); - Assert.True(context.GrantResult.IsError); - var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"]; - Assert.Equal("Username or password is incorrect. Try again.", errorResponse.Message); - } - [Theory, BitAutoData] public async Task ValidateAsync_DeviceNotValidated_ShouldLogError( [AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest, @@ -189,7 +116,6 @@ public class BaseRequestValidatorTests // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); // 1 -> to pass - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; // 2 -> will result to false with no extra configuration @@ -226,7 +152,6 @@ public class BaseRequestValidatorTests // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); // 1 -> to pass - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; // 2 -> will result to false with no extra configuration @@ -263,7 +188,6 @@ public class BaseRequestValidatorTests { // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; context.ValidatedTokenRequest.GrantType = grantType; @@ -294,7 +218,6 @@ public class BaseRequestValidatorTests // Arrange _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); var context = CreateContext(tokenRequest, requestContext, grantResult); - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; context.ValidatedTokenRequest.GrantType = grantType; @@ -326,7 +249,6 @@ public class BaseRequestValidatorTests // Arrange _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); var context = CreateContext(tokenRequest, requestContext, grantResult); - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; context.ValidatedTokenRequest.GrantType = grantType; @@ -363,7 +285,6 @@ public class BaseRequestValidatorTests { // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; context.ValidatedTokenRequest.GrantType = grantType; @@ -401,7 +322,6 @@ public class BaseRequestValidatorTests { // Arrange var context = CreateContext(tokenRequest, requestContext, grantResult); - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; _sut.isValid = true; context.ValidatedTokenRequest.GrantType = grantType; @@ -439,7 +359,6 @@ public class BaseRequestValidatorTests var user = context.CustomValidatorRequestContext.User; user.Key = null; - context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false; context.ValidatedTokenRequest.ClientId = "Not Web"; _sut.isValid = true; _twoFactorAuthenticationValidator diff --git a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs index c204e380b8..4c14de2d73 100644 --- a/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs +++ b/test/Identity.Test/Wrappers/BaseRequestValidatorTestWrapper.cs @@ -54,7 +54,6 @@ IBaseRequestValidatorTestWrapper IDeviceValidator deviceValidator, ITwoFactorAuthenticationValidator twoFactorAuthenticationValidator, IOrganizationUserRepository organizationUserRepository, - IMailService mailService, ILogger logger, ICurrentContext currentContext, GlobalSettings globalSettings, @@ -71,7 +70,6 @@ IBaseRequestValidatorTestWrapper deviceValidator, twoFactorAuthenticationValidator, organizationUserRepository, - mailService, logger, currentContext, globalSettings, diff --git a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs index c1089608da..76fa0f03d1 100644 --- a/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs +++ b/test/IntegrationTestCommon/Factories/WebApplicationFactoryBase.cs @@ -1,5 +1,4 @@ using AspNetCoreRateLimit; -using Bit.Core.Auth.Services; using Bit.Core.Billing.Services; using Bit.Core.Platform.Push; using Bit.Core.Platform.Push.Internal; @@ -207,8 +206,6 @@ public abstract class WebApplicationFactoryBase : WebApplicationFactory Replace(services); - Replace(services); - // TODO: Install and use azurite in CI pipeline Replace(services); diff --git a/util/Setup/Configuration.cs b/util/Setup/Configuration.cs index 264eef05b2..3372652d03 100644 --- a/util/Setup/Configuration.cs +++ b/util/Setup/Configuration.cs @@ -31,9 +31,6 @@ public class Configuration "Learn more: https://docs.docker.com/compose/compose-file/#ports")] public string HttpsPort { get; set; } = "443"; - [Description("Configure Nginx for Captcha.")] - public bool Captcha { get; set; } = false; - [Description("Configure Nginx for SSL.")] public bool Ssl { get; set; } = true; diff --git a/util/Setup/NginxConfigBuilder.cs b/util/Setup/NginxConfigBuilder.cs index 865b8bdd69..1315ffaba7 100644 --- a/util/Setup/NginxConfigBuilder.cs +++ b/util/Setup/NginxConfigBuilder.cs @@ -73,7 +73,6 @@ public class NginxConfigBuilder public TemplateModel(Context context) { - Captcha = context.Config.Captcha; Ssl = context.Config.Ssl; EnableKeyConnector = context.Config.EnableKeyConnector; EnableScim = context.Config.EnableScim; @@ -127,7 +126,6 @@ public class NginxConfigBuilder } } - public bool Captcha { get; set; } public bool Ssl { get; set; } public bool EnableKeyConnector { get; set; } public bool EnableScim { get; set; } diff --git a/util/Setup/Templates/NginxConfig.hbs b/util/Setup/Templates/NginxConfig.hbs index 115c79c72a..f37987ca70 100644 --- a/util/Setup/Templates/NginxConfig.hbs +++ b/util/Setup/Templates/NginxConfig.hbs @@ -100,16 +100,6 @@ server { proxy_pass http://web:5000/sso-connector.html; } -{{#if Captcha}} - location = /captcha-connector.html { - proxy_pass http://web:5000/captcha-connector.html; - } - - location = /captcha-mobile-connector.html { - proxy_pass http://web:5000/captcha-mobile-connector.html; - } -{{/if}} - location /attachments/ { proxy_pass http://attachments:5000/; } From 3f95513d1142d02cdc6a0f36e7a24dd7f25c9f69 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Fri, 9 May 2025 11:39:57 -0400 Subject: [PATCH 038/114] [PM-19029][PM-19203] Addressing `UserService` tech debt around `ITwoFactorIsEnabledQuery` (#5754) * fix : split out the interface from the TwoFactorAuthenticationValidator into separate file. * fix: replacing IUserService.TwoFactorEnabled with ITwoFactorEnabledQuery * fix: combined logic for both bulk and single user look ups for TwoFactorIsEnabledQuery. * fix: return two factor provider enabled on CanGenerate() method. * tech debt: modfifying MFA providers to call the database less to validate if two factor is enabled. * tech debt: removed unused service from AuthenticatorTokenProvider * doc: added documentation to ITwoFactorProviderUsers * doc: updated comments for TwoFactorIsEnabled impl * test: fixing tests for ITwoFactorIsEnabledQuery * test: updating tests to have correct DI and removing test for automatic email of TOTP. * test: adding better test coverage --- .../Public/Controllers/MembersController.cs | 4 +- .../Auth/Controllers/AccountsController.cs | 10 +- .../Billing/Controllers/AccountsController.cs | 6 +- src/Api/Vault/Controllers/SyncController.cs | 8 +- .../OrganizationUsers/AcceptOrgUserCommand.cs | 6 +- src/Core/Auth/Enums/TwoFactorProviderType.cs | 3 +- .../AuthenticatorTokenProvider.cs | 15 +-- .../DuoUniversalTokenProvider.cs | 11 +- .../EmailTwoFactorTokenProvider.cs | 17 +-- .../TokenProviders/WebAuthnTokenProvider.cs | 19 ++- .../TokenProviders/YubicoOtpTokenProvider.cs | 8 +- src/Core/Auth/Identity/UserStore.cs | 6 +- .../Auth/Models/ITwoFactorProvidersUser.cs | 8 ++ .../Interfaces/ITwoFactorIsEnabledQuery.cs | 4 +- .../TwoFactorAuth/TwoFactorIsEnabledQuery.cs | 96 +++++++------- src/Core/Entities/User.cs | 45 ++++--- src/Core/Repositories/IUserRepository.cs | 10 ++ src/Core/Services/IUserService.cs | 8 +- .../Services/Implementations/UserService.cs | 48 +------ .../ITwoFactorAuthenticationValidator.cs | 38 ++++++ .../TwoFactorAuthenticationValidator.cs | 44 +------ .../Repositories/UserRepository.cs | 10 +- .../Repositories/UserRepository.cs | 16 ++- .../Controllers/AccountsControllerTests.cs | 4 + .../Vault/Controllers/SyncControllerTests.cs | 19 ++- .../AcceptOrgUserCommandTests.cs | 6 +- .../Auth/Identity/BaseTokenProviderTests.cs | 5 - ...DuoUniversalTwoFactorTokenProviderTests.cs | 3 + .../TwoFactorIsEnabledQueryTests.cs | 120 +++++++++++++++--- test/Core.Test/Services/UserServiceTests.cs | 6 + .../TwoFactorAuthenticationValidatorTests.cs | 28 +--- 31 files changed, 372 insertions(+), 259 deletions(-) create mode 100644 src/Identity/IdentityServer/RequestValidators/ITwoFactorAuthenticationValidator.cs diff --git a/src/Api/AdminConsole/Public/Controllers/MembersController.cs b/src/Api/AdminConsole/Public/Controllers/MembersController.cs index 92e5071801..6552684ca3 100644 --- a/src/Api/AdminConsole/Public/Controllers/MembersController.cs +++ b/src/Api/AdminConsole/Public/Controllers/MembersController.cs @@ -76,7 +76,7 @@ public class MembersController : Controller { return new NotFoundResult(); } - var response = new MemberResponseModel(orgUser, await _userService.TwoFactorIsEnabledAsync(orgUser), + var response = new MemberResponseModel(orgUser, await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUser), collections); return new JsonResult(response); } @@ -185,7 +185,7 @@ public class MembersController : Controller { var existingUserDetails = await _organizationUserRepository.GetDetailsByIdAsync(id); response = new MemberResponseModel(existingUserDetails, - await _userService.TwoFactorIsEnabledAsync(existingUserDetails), associations); + await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(existingUserDetails), associations); } else { diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index 2134a7fc4e..fdd5fbb290 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -16,6 +16,7 @@ using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; using Bit.Core.Entities; using Bit.Core.Enums; @@ -45,6 +46,7 @@ public class AccountsController : Controller private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand; private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand; private readonly IRotateUserKeyCommand _rotateUserKeyCommand; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IFeatureService _featureService; private readonly IRotationValidator, IEnumerable> _cipherValidator; @@ -68,6 +70,7 @@ public class AccountsController : Controller ISetInitialMasterPasswordCommand setInitialMasterPasswordCommand, ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand, IRotateUserKeyCommand rotateUserKeyCommand, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IFeatureService featureService, IRotationValidator, IEnumerable> cipherValidator, IRotationValidator, IEnumerable> folderValidator, @@ -87,6 +90,7 @@ public class AccountsController : Controller _setInitialMasterPasswordCommand = setInitialMasterPasswordCommand; _tdeOffboardingPasswordCommand = tdeOffboardingPasswordCommand; _rotateUserKeyCommand = rotateUserKeyCommand; + _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _featureService = featureService; _cipherValidator = cipherValidator; _folderValidator = folderValidator; @@ -389,7 +393,7 @@ public class AccountsController : Controller await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id, ProviderUserStatusType.Confirmed); - var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); + var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); @@ -423,7 +427,7 @@ public class AccountsController : Controller await _userService.SaveUserAsync(model.ToUser(user)); - var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); + var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); @@ -442,7 +446,7 @@ public class AccountsController : Controller } await _userService.SaveUserAsync(model.ToUser(user), true); - var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); + var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); diff --git a/src/Api/Billing/Controllers/AccountsController.cs b/src/Api/Billing/Controllers/AccountsController.cs index bc263691a8..49ff679bb8 100644 --- a/src/Api/Billing/Controllers/AccountsController.cs +++ b/src/Api/Billing/Controllers/AccountsController.cs @@ -3,6 +3,7 @@ using Bit.Api.Models.Request; using Bit.Api.Models.Request.Accounts; using Bit.Api.Models.Response; using Bit.Api.Utilities; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Models; using Bit.Core.Billing.Services; using Bit.Core.Context; @@ -22,7 +23,8 @@ namespace Bit.Api.Billing.Controllers; [Route("accounts")] [Authorize("Application")] public class AccountsController( - IUserService userService) : Controller + IUserService userService, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) : Controller { [HttpPost("premium")] public async Task PostPremiumAsync( @@ -56,7 +58,7 @@ public class AccountsController( model.PaymentMethodType!.Value, model.AdditionalStorageGb.GetValueOrDefault(0), license, new TaxInfo { BillingAddressCountry = model.Country, BillingAddressPostalCode = model.PostalCode }); - var userTwoFactorEnabled = await userService.TwoFactorIsEnabledAsync(user); + var userTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user); var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id); diff --git a/src/Api/Vault/Controllers/SyncController.cs b/src/Api/Vault/Controllers/SyncController.cs index 4b66c7f2bd..568c05d651 100644 --- a/src/Api/Vault/Controllers/SyncController.cs +++ b/src/Api/Vault/Controllers/SyncController.cs @@ -3,6 +3,7 @@ using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; @@ -37,6 +38,7 @@ public class SyncController : Controller private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion); private readonly IFeatureService _featureService; private readonly IApplicationCacheService _applicationCacheService; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; public SyncController( IUserService userService, @@ -51,7 +53,8 @@ public class SyncController : Controller GlobalSettings globalSettings, ICurrentContext currentContext, IFeatureService featureService, - IApplicationCacheService applicationCacheService) + IApplicationCacheService applicationCacheService, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) { _userService = userService; _folderRepository = folderRepository; @@ -66,6 +69,7 @@ public class SyncController : Controller _currentContext = currentContext; _featureService = featureService; _applicationCacheService = applicationCacheService; + _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; } [HttpGet("")] @@ -102,7 +106,7 @@ public class SyncController : Controller collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key); } - var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); + var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user); var organizationClaimingActiveUser = await _userService.GetOrganizationsClaimingUserAsync(user.Id); var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id); diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs index 756bd2ae46..f3426efddc 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; @@ -24,6 +25,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand private readonly IPolicyService _policyService; private readonly IMailService _mailService; private readonly IUserRepository _userRepository; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; public AcceptOrgUserCommand( @@ -34,6 +36,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand IPolicyService policyService, IMailService mailService, IUserRepository userRepository, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IDataProtectorTokenFactory orgUserInviteTokenDataFactory) { @@ -45,6 +48,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand _policyService = policyService; _mailService = mailService; _userRepository = userRepository; + _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; } @@ -192,7 +196,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand } // Enforce Two Factor Authentication Policy of organization user is trying to join - if (!await userService.TwoFactorIsEnabledAsync(user)) + if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user)) { var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited); diff --git a/src/Core/Auth/Enums/TwoFactorProviderType.cs b/src/Core/Auth/Enums/TwoFactorProviderType.cs index 07a52dc429..c3613785bc 100644 --- a/src/Core/Auth/Enums/TwoFactorProviderType.cs +++ b/src/Core/Auth/Enums/TwoFactorProviderType.cs @@ -6,7 +6,8 @@ public enum TwoFactorProviderType : byte Email = 1, Duo = 2, YubiKey = 3, - U2f = 4, // Deprecated + [Obsolete("Deprecated in favor of WebAuthn.")] + U2f = 4, Remember = 5, OrganizationDuo = 6, WebAuthn = 7, diff --git a/src/Core/Auth/Identity/TokenProviders/AuthenticatorTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/AuthenticatorTokenProvider.cs index 9468e4d571..5a3d9522f3 100644 --- a/src/Core/Auth/Identity/TokenProviders/AuthenticatorTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/AuthenticatorTokenProvider.cs @@ -1,6 +1,5 @@ using Bit.Core.Auth.Enums; using Bit.Core.Entities; -using Bit.Core.Services; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; @@ -12,16 +11,13 @@ public class AuthenticatorTokenProvider : IUserTwoFactorTokenProvider { private const string CacheKeyFormat = "Authenticator_TOTP_{0}_{1}"; - private readonly IServiceProvider _serviceProvider; private readonly IDistributedCache _distributedCache; private readonly DistributedCacheEntryOptions _distributedCacheEntryOptions; public AuthenticatorTokenProvider( - IServiceProvider serviceProvider, [FromKeyedServices("persistent")] IDistributedCache distributedCache) { - _serviceProvider = serviceProvider; _distributedCache = distributedCache; _distributedCacheEntryOptions = new DistributedCacheEntryOptions { @@ -29,15 +25,14 @@ public class AuthenticatorTokenProvider : IUserTwoFactorTokenProvider }; } - public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) + public Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) { - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator); - if (string.IsNullOrWhiteSpace((string)provider?.MetaData["Key"])) + var authenticatorProvider = user.GetTwoFactorProvider(TwoFactorProviderType.Authenticator); + if (string.IsNullOrWhiteSpace((string)authenticatorProvider?.MetaData["Key"])) { - return false; + return Task.FromResult(false); } - return await _serviceProvider.GetRequiredService() - .TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Authenticator, user); + return Task.FromResult(authenticatorProvider.Enabled); } public Task GenerateAsync(string purpose, UserManager manager, User user) diff --git a/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs index cbb994fa09..3f2a44915c 100644 --- a/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/DuoUniversalTokenProvider.cs @@ -17,7 +17,7 @@ public class DuoUniversalTokenProvider( { /// /// We need the IServiceProvider to resolve the . There is a complex dependency dance - /// occurring between , which extends the , and the usage + /// occurring between , which extends the , and the usage /// of the within this class. Trying to resolve the using /// the DI pipeline will not allow the server to start and it will hang and give no helpful indication as to the /// problem. @@ -29,12 +29,13 @@ public class DuoUniversalTokenProvider( public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) { var userService = _serviceProvider.GetRequiredService(); - var provider = await GetDuoTwoFactorProvider(user, userService); - if (provider == null) + var duoUniversalTokenProvider = await GetDuoTwoFactorProvider(user, userService); + if (duoUniversalTokenProvider == null) { return false; } - return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Duo, user); + + return duoUniversalTokenProvider.Enabled; } public async Task GenerateAsync(string purpose, UserManager manager, User user) @@ -58,7 +59,7 @@ public class DuoUniversalTokenProvider( } /// - /// Get the Duo Two Factor Provider for the user if they have access to Duo + /// Get the Duo Two Factor Provider for the user if they have premium access to Duo /// /// Active User /// null or Duo TwoFactorProvider diff --git a/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs index b0ad9bd480..718e44ae5f 100644 --- a/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/EmailTwoFactorTokenProvider.cs @@ -1,7 +1,6 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Entities; -using Bit.Core.Services; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; @@ -10,31 +9,25 @@ namespace Bit.Core.Auth.Identity.TokenProviders; public class EmailTwoFactorTokenProvider : EmailTokenProvider { - private readonly IServiceProvider _serviceProvider; - public EmailTwoFactorTokenProvider( - IServiceProvider serviceProvider, [FromKeyedServices("persistent")] IDistributedCache distributedCache) : base(distributedCache) { - _serviceProvider = serviceProvider; - TokenAlpha = false; TokenNumeric = true; TokenLength = 6; } - public override async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) + public override Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) { - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.Email); - if (!HasProperMetaData(provider)) + var emailTokenProvider = user.GetTwoFactorProvider(TwoFactorProviderType.Email); + if (!HasProperMetaData(emailTokenProvider)) { - return false; + return Task.FromResult(false); } - return await _serviceProvider.GetRequiredService(). - TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.Email, user); + return Task.FromResult(emailTokenProvider.Enabled); } public override Task GenerateAsync(string purpose, UserManager manager, User user) diff --git a/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs index 202ba3a38c..0bf75d0fc3 100644 --- a/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/WebAuthnTokenProvider.cs @@ -25,17 +25,16 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider _globalSettings = globalSettings; } - public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) + public Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) { - var userService = _serviceProvider.GetRequiredService(); - var webAuthnProvider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); + // null check happens in this method if (!HasProperMetaData(webAuthnProvider)) { - return false; + return Task.FromResult(false); } - return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.WebAuthn, user); + return Task.FromResult(webAuthnProvider.Enabled); } public async Task GenerateAsync(string purpose, UserManager manager, User user) @@ -81,7 +80,7 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider var provider = user.GetTwoFactorProvider(TwoFactorProviderType.WebAuthn); var keys = LoadKeys(provider); - if (!provider.MetaData.ContainsKey("login")) + if (!provider.MetaData.TryGetValue("login", out var value)) { return false; } @@ -89,7 +88,7 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider var clientResponse = JsonSerializer.Deserialize(token, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); - var jsonOptions = provider.MetaData["login"].ToString(); + var jsonOptions = value.ToString(); var options = AssertionOptions.FromJson(jsonOptions); var webAuthCred = keys.Find(k => k.Item2.Descriptor.Id.SequenceEqual(clientResponse.Id)); @@ -126,6 +125,12 @@ public class WebAuthnTokenProvider : IUserTwoFactorTokenProvider } + /// + /// Checks if the provider has proper metadata. + /// This is used to determine if the provider has been properly configured. + /// + /// + /// true if metadata is present; false if empty or null private bool HasProperMetaData(TwoFactorProvider provider) { return provider?.MetaData?.Any() ?? false; diff --git a/src/Core/Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs b/src/Core/Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs index 9794a51ae9..b33d2fc0c9 100644 --- a/src/Core/Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs +++ b/src/Core/Auth/Identity/TokenProviders/YubicoOtpTokenProvider.cs @@ -23,19 +23,21 @@ public class YubicoOtpTokenProvider : IUserTwoFactorTokenProvider public async Task CanGenerateTwoFactorTokenAsync(UserManager manager, User user) { + // Ensure the user has access to premium var userService = _serviceProvider.GetRequiredService(); if (!await userService.CanAccessPremium(user)) { return false; } - var provider = user.GetTwoFactorProvider(TwoFactorProviderType.YubiKey); - if (!provider?.MetaData.Values.Any(v => !string.IsNullOrWhiteSpace((string)v)) ?? true) + // Check if the user has a YubiKey provider configured + var yubicoProvider = user.GetTwoFactorProvider(TwoFactorProviderType.YubiKey); + if (!yubicoProvider?.MetaData.Values.Any(v => !string.IsNullOrWhiteSpace((string)v)) ?? true) { return false; } - return await userService.TwoFactorProviderIsEnabledAsync(TwoFactorProviderType.YubiKey, user); + return yubicoProvider.Enabled; } public Task GenerateAsync(string purpose, UserManager manager, User user) diff --git a/src/Core/Auth/Identity/UserStore.cs b/src/Core/Auth/Identity/UserStore.cs index 3716d75b6a..41323f05b7 100644 --- a/src/Core/Auth/Identity/UserStore.cs +++ b/src/Core/Auth/Identity/UserStore.cs @@ -1,7 +1,7 @@ -using Bit.Core.Context; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; +using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Repositories; -using Bit.Core.Services; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; @@ -167,7 +167,7 @@ public class UserStore : public async Task GetTwoFactorEnabledAsync(User user, CancellationToken cancellationToken) { - return await _serviceProvider.GetRequiredService().TwoFactorIsEnabledAsync(user); + return await _serviceProvider.GetRequiredService().TwoFactorIsEnabledAsync(user); } public Task SetSecurityStampAsync(User user, string stamp, CancellationToken cancellationToken) diff --git a/src/Core/Auth/Models/ITwoFactorProvidersUser.cs b/src/Core/Auth/Models/ITwoFactorProvidersUser.cs index 5d9ae4b362..f953e4570e 100644 --- a/src/Core/Auth/Models/ITwoFactorProvidersUser.cs +++ b/src/Core/Auth/Models/ITwoFactorProvidersUser.cs @@ -1,10 +1,18 @@ using Bit.Core.Auth.Enums; +using Bit.Core.Services; namespace Bit.Core.Auth.Models; public interface ITwoFactorProvidersUser { string TwoFactorProviders { get; } + /// + /// Get the two factor providers for the user. Currently it can be assumed providers are enabled + /// if they exists in the dictionary. When two factor providers are disabled they are removed + /// from the dictionary. + /// + /// + /// Dictionary of providers with the type enum as the key Dictionary GetTwoFactorProviders(); Guid? GetUserId(); bool GetPremium(); diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/Interfaces/ITwoFactorIsEnabledQuery.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/Interfaces/ITwoFactorIsEnabledQuery.cs index 203ef3accb..697c10690c 100644 --- a/src/Core/Auth/UserFeatures/TwoFactorAuth/Interfaces/ITwoFactorIsEnabledQuery.cs +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/Interfaces/ITwoFactorIsEnabledQuery.cs @@ -2,6 +2,7 @@ namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; + public interface ITwoFactorIsEnabledQuery { /// @@ -16,7 +17,8 @@ public interface ITwoFactorIsEnabledQuery /// The type of user in the list. Must implement . Task> TwoFactorIsEnabledAsync(IEnumerable users) where T : ITwoFactorProvidersUser; /// - /// Returns whether two factor is enabled for the user. + /// Returns whether two factor is enabled for the user. A user is able to have a TwoFactorProvider that is enabled but requires Premium. + /// If the user does not have premium then the TwoFactorProvider is considered _not_ enabled. /// /// The user to check. Task TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user); diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQuery.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQuery.cs index bda2094f24..8d4bd49e42 100644 --- a/src/Core/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQuery.cs +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQuery.cs @@ -1,17 +1,13 @@ -using Bit.Core.Auth.Models; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Repositories; namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth; -public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery +public class TwoFactorIsEnabledQuery(IUserRepository userRepository) : ITwoFactorIsEnabledQuery { - private readonly IUserRepository _userRepository; - - public TwoFactorIsEnabledQuery(IUserRepository userRepository) - { - _userRepository = userRepository; - } + private readonly IUserRepository _userRepository = userRepository; public async Task> TwoFactorIsEnabledAsync(IEnumerable userIds) { @@ -21,26 +17,15 @@ public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery return result; } - var userDetails = await _userRepository.GetManyWithCalculatedPremiumAsync(userIds.ToList()); - + var userDetails = await _userRepository.GetManyWithCalculatedPremiumAsync([.. userIds]); foreach (var userDetail in userDetails) { - var hasTwoFactor = false; - var providers = userDetail.GetTwoFactorProviders(); - if (providers != null) - { - // Get all enabled providers - var enabledProviderKeys = from provider in providers - where provider.Value?.Enabled ?? false - select provider.Key; - - // Find the first provider that is enabled and passes the premium check - hasTwoFactor = enabledProviderKeys - .Select(type => userDetail.HasPremiumAccess || !TwoFactorProvider.RequiresPremium(type)) - .FirstOrDefault(); - } - - result.Add((userDetail.Id, hasTwoFactor)); + result.Add( + (userDetail.Id, + await TwoFactorEnabledAsync(userDetail.GetTwoFactorProviders(), + () => Task.FromResult(userDetail.HasPremiumAccess)) + ) + ); } return result; @@ -83,41 +68,56 @@ public class TwoFactorIsEnabledQuery : ITwoFactorIsEnabledQuery return false; } - var providers = user.GetTwoFactorProviders(); - if (providers == null || !providers.Any()) + return await TwoFactorEnabledAsync( + user.GetTwoFactorProviders(), + async () => + { + var calcUser = await _userRepository.GetCalculatedPremiumAsync(userId.Value); + return calcUser?.HasPremiumAccess ?? false; + }); + } + + /// + /// Checks to see what kind of two-factor is enabled. + /// We use a delegate to check if the user has premium access, since there are multiple ways to + /// determine if a user has premium access. + /// + /// dictionary of two factor providers + /// function to check if the user has premium access + /// true if the user has two factor enabled; false otherwise; + private async static Task TwoFactorEnabledAsync( + Dictionary providers, + Func> hasPremiumAccessDelegate) + { + // If there are no providers, then two factor is not enabled + if (providers == null || providers.Count == 0) { return false; } // Get all enabled providers - var enabledProviderKeys = providers - .Where(provider => provider.Value?.Enabled ?? false) - .Select(provider => provider.Key); + // TODO: PM-21210: In practice we don't save disabled providers to the database, worth looking into. + var enabledProviderKeys = from provider in providers + where provider.Value?.Enabled ?? false + select provider.Key; + // If no providers are enabled then two factor is not enabled if (!enabledProviderKeys.Any()) { return false; } - // Determine if any enabled provider passes the premium check - var hasTwoFactor = enabledProviderKeys - .Select(type => user.GetPremium() || !TwoFactorProvider.RequiresPremium(type)) - .FirstOrDefault(); - - // If no enabled provider passes the check, check the repository for organization premium access - if (!hasTwoFactor) + // If there are only premium two factor options then standard two factor is not enabled + var onlyHasPremiumTwoFactor = enabledProviderKeys.All(TwoFactorProvider.RequiresPremium); + if (onlyHasPremiumTwoFactor) { - var userDetails = await _userRepository.GetManyWithCalculatedPremiumAsync(new List { userId.Value }); - var userDetail = userDetails.FirstOrDefault(); - - if (userDetail != null) - { - hasTwoFactor = enabledProviderKeys - .Select(type => userDetail.HasPremiumAccess || !TwoFactorProvider.RequiresPremium(type)) - .FirstOrDefault(); - } + // There are no Standard two factor options, check if the user has premium access + // If the user has premium access, then two factor is enabled + var premiumAccess = await hasPremiumAccessDelegate(); + return premiumAccess; } - return hasTwoFactor; + // The user has at least one non-premium two factor option + return true; } } diff --git a/src/Core/Entities/User.cs b/src/Core/Entities/User.cs index 9878c96c1c..b3a6a9592e 100644 --- a/src/Core/Entities/User.cs +++ b/src/Core/Entities/User.cs @@ -128,6 +128,10 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac public bool IsExpired() => PremiumExpirationDate.HasValue && PremiumExpirationDate.Value <= DateTime.UtcNow; + /// + /// Deserializes the User.TwoFactorProviders property from JSON to the appropriate C# dictionary. + /// + /// Dictionary of TwoFactor providers public Dictionary? GetTwoFactorProviders() { if (string.IsNullOrWhiteSpace(TwoFactorProviders)) @@ -137,19 +141,17 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac try { - if (_twoFactorProviders == null) - { - _twoFactorProviders = - JsonHelpers.LegacyDeserialize>( - TwoFactorProviders); - } + _twoFactorProviders ??= + JsonHelpers.LegacyDeserialize>( + TwoFactorProviders); - // U2F is no longer supported, and all users keys should have been migrated to WebAuthn. - // To prevent issues with accounts being prompted for unsupported U2F we remove them - if (_twoFactorProviders.ContainsKey(TwoFactorProviderType.U2f)) - { - _twoFactorProviders.Remove(TwoFactorProviderType.U2f); - } + /* + U2F is no longer supported, and all users keys should have been migrated to WebAuthn. + To prevent issues with accounts being prompted for unsupported U2F we remove them. + This will probably exist in perpetuity since there is no way to know for sure if any + given user does or doesn't have this enabled. It is a non-zero chance. + */ + _twoFactorProviders?.Remove(TwoFactorProviderType.U2f); return _twoFactorProviders; } @@ -169,6 +171,10 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac return Premium; } + /// + /// Serializes the C# object to the User.TwoFactorProviders property in JSON format. + /// + /// Dictionary of Two Factor providers public void SetTwoFactorProviders(Dictionary providers) { // When replacing with system.text remember to remove the extra serialization in WebAuthnTokenProvider. @@ -176,20 +182,21 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac _twoFactorProviders = providers; } - public void ClearTwoFactorProviders() - { - SetTwoFactorProviders(new Dictionary()); - } - + /// + /// Checks if the user has a specific TwoFactorProvider configured. If a user has a premium TwoFactor + /// configured it will still be found, even if the user's premium subscription has ended. + /// + /// TwoFactor provider being searched for + /// TwoFactorProvider if found; null otherwise. public TwoFactorProvider? GetTwoFactorProvider(TwoFactorProviderType provider) { var providers = GetTwoFactorProviders(); - if (providers == null || !providers.ContainsKey(provider)) + if (providers == null || !providers.TryGetValue(provider, out var value)) { return null; } - return providers[provider]; + return value; } public long StorageBytesRemaining() diff --git a/src/Core/Repositories/IUserRepository.cs b/src/Core/Repositories/IUserRepository.cs index 0e59b9998f..22effb4329 100644 --- a/src/Core/Repositories/IUserRepository.cs +++ b/src/Core/Repositories/IUserRepository.cs @@ -25,6 +25,16 @@ public interface IUserRepository : IRepository /// Task> GetManyWithCalculatedPremiumAsync(IEnumerable ids); /// + /// Retrieves the data for the requested user ID and includes additional property indicating + /// whether the user has premium access directly or through an organization. + /// + /// Calls the same stored procedure as GetManyWithCalculatedPremiumAsync but handles the query + /// for a single user. + /// + /// The user ID to retrieve data for. + /// User data with calculated premium access; null if nothing is found + Task GetCalculatedPremiumAsync(Guid userId); + /// /// Sets a new user key and updates all encrypted data. /// Warning: Any user key encrypted data not included will be lost. /// diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 9b12713218..228b8543d7 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -71,11 +71,13 @@ public interface IUserService Task GenerateLicenseAsync(User user, SubscriptionInfo subscriptionInfo = null, int? version = null); Task CheckPasswordAsync(User user, string password); + /// + /// Checks if the user has access to premium features, either through a personal subscription or through an organization. + /// + /// user being acted on + /// true if they can access premium; false otherwise. Task CanAccessPremium(ITwoFactorProvidersUser user); Task HasPremiumFromOrganization(ITwoFactorProvidersUser user); - [Obsolete("Use ITwoFactorIsEnabledQuery instead.")] - Task TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user); - Task TwoFactorProviderIsEnabledAsync(TwoFactorProviderType provider, ITwoFactorProvidersUser user); Task GenerateSignInTokenAsync(User user, string purpose); Task UpdatePasswordHash(User user, string newPassword, diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 95ee4544fa..23617a0fcd 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -11,6 +11,7 @@ using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; @@ -77,6 +78,7 @@ public class UserService : UserManager, IUserService, IDisposable private readonly IPremiumUserBillingService _premiumUserBillingService; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IDistributedCache _distributedCache; public UserService( @@ -115,6 +117,7 @@ public class UserService : UserManager, IUserService, IDisposable IPremiumUserBillingService premiumUserBillingService, IRemoveOrganizationUserCommand removeOrganizationUserCommand, IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IDistributedCache distributedCache) : base( store, @@ -158,6 +161,7 @@ public class UserService : UserManager, IUserService, IDisposable _premiumUserBillingService = premiumUserBillingService; _removeOrganizationUserCommand = removeOrganizationUserCommand; _revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand; + _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _distributedCache = distributedCache; } @@ -918,7 +922,7 @@ public class UserService : UserManager, IUserService, IDisposable await SaveUserAsync(user); await _eventService.LogUserEventAsync(user.Id, EventType.User_Disabled2fa); - if (!await TwoFactorIsEnabledAsync(user)) + if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user)) { await CheckPoliciesOnTwoFactorRemovalAsync(user); } @@ -1280,48 +1284,6 @@ public class UserService : UserManager, IUserService, IDisposable orgAbility.UsersGetPremium && orgAbility.Enabled); } - - public async Task TwoFactorIsEnabledAsync(ITwoFactorProvidersUser user) - { - var providers = user.GetTwoFactorProviders(); - if (providers == null) - { - return false; - } - - foreach (var p in providers) - { - if (p.Value?.Enabled ?? false) - { - if (!TwoFactorProvider.RequiresPremium(p.Key)) - { - return true; - } - if (await CanAccessPremium(user)) - { - return true; - } - } - } - return false; - } - - public async Task TwoFactorProviderIsEnabledAsync(TwoFactorProviderType provider, ITwoFactorProvidersUser user) - { - var providers = user.GetTwoFactorProviders(); - if (providers == null || !providers.ContainsKey(provider) || !providers[provider].Enabled) - { - return false; - } - - if (!TwoFactorProvider.RequiresPremium(provider)) - { - return true; - } - - return await CanAccessPremium(user); - } - public async Task GenerateSignInTokenAsync(User user, string purpose) { var token = await GenerateUserTokenAsync(user, Options.Tokens.PasswordResetTokenProvider, diff --git a/src/Identity/IdentityServer/RequestValidators/ITwoFactorAuthenticationValidator.cs b/src/Identity/IdentityServer/RequestValidators/ITwoFactorAuthenticationValidator.cs new file mode 100644 index 0000000000..cc45fcb3eb --- /dev/null +++ b/src/Identity/IdentityServer/RequestValidators/ITwoFactorAuthenticationValidator.cs @@ -0,0 +1,38 @@ + +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Enums; +using Bit.Core.Entities; +using Duende.IdentityServer.Validation; + +namespace Bit.Identity.IdentityServer.RequestValidators; + +public interface ITwoFactorAuthenticationValidator +{ + /// + /// Check if the user is required to use two-factor authentication to login. This is based on the user's + /// enabled two-factor providers, the user's organizations enabled two-factor providers, and the grant type. + /// Client credentials and webauthn grant types do not require two-factor authentication. + /// + /// the active user for the request + /// the request that contains the grant types + /// boolean + Task> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request); + /// + /// Builds the two-factor authentication result for the user based on the available two-factor providers + /// from either their user account or Organization. + /// + /// user trying to login + /// organization associated with the user; Can be null + /// Dictionary with the TwoFactorProviderType as the Key and the Provider Metadata as the Value + Task> BuildTwoFactorResultAsync(User user, Organization organization); + /// + /// Uses the built in userManager methods to verify the two-factor token for the user. If the organization uses + /// organization duo, it will use the organization duo token provider to verify the token. + /// + /// the active User + /// organization of user; can be null + /// Two Factor Provider to use to verify the token + /// secret passed from the user and consumed by the two-factor provider's verify method + /// boolean + Task VerifyTwoFactorAsync(User user, Organization organization, TwoFactorProviderType twoFactorProviderType, string token); +} diff --git a/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs index e733d4f410..80b3b6e1f4 100644 --- a/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs @@ -4,6 +4,7 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Models.Data.Organizations; @@ -16,56 +17,25 @@ using Microsoft.AspNetCore.Identity; namespace Bit.Identity.IdentityServer.RequestValidators; -public interface ITwoFactorAuthenticationValidator -{ - /// - /// Check if the user is required to use two-factor authentication to login. This is based on the user's - /// enabled two-factor providers, the user's organizations enabled two-factor providers, and the grant type. - /// Client credentials and webauthn grant types do not require two-factor authentication. - /// - /// the active user for the request - /// the request that contains the grant types - /// boolean - Task> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request); - /// - /// Builds the two-factor authentication result for the user based on the available two-factor providers - /// from either their user account or Organization. - /// - /// user trying to login - /// organization associated with the user; Can be null - /// Dictionary with the TwoFactorProviderType as the Key and the Provider Metadata as the Value - Task> BuildTwoFactorResultAsync(User user, Organization organization); - /// - /// Uses the built in userManager methods to verify the two-factor token for the user. If the organization uses - /// organization duo, it will use the organization duo token provider to verify the token. - /// - /// the active User - /// organization of user; can be null - /// Two Factor Provider to use to verify the token - /// secret passed from the user and consumed by the two-factor provider's verify method - /// boolean - Task VerifyTwoFactorAsync(User user, Organization organization, TwoFactorProviderType twoFactorProviderType, string token); -} - public class TwoFactorAuthenticationValidator( IUserService userService, UserManager userManager, IOrganizationDuoUniversalTokenProvider organizationDuoWebTokenProvider, - IFeatureService featureService, IApplicationCacheService applicationCacheService, IOrganizationUserRepository organizationUserRepository, IOrganizationRepository organizationRepository, IDataProtectorTokenFactory ssoEmail2faSessionTokeFactory, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, ICurrentContext currentContext) : ITwoFactorAuthenticationValidator { private readonly IUserService _userService = userService; private readonly UserManager _userManager = userManager; private readonly IOrganizationDuoUniversalTokenProvider _organizationDuoUniversalTokenProvider = organizationDuoWebTokenProvider; - private readonly IFeatureService _featureService = featureService; private readonly IApplicationCacheService _applicationCacheService = applicationCacheService; private readonly IOrganizationUserRepository _organizationUserRepository = organizationUserRepository; private readonly IOrganizationRepository _organizationRepository = organizationRepository; private readonly IDataProtectorTokenFactory _ssoEmail2faSessionTokeFactory = ssoEmail2faSessionTokeFactory; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; private readonly ICurrentContext _currentContext = currentContext; public async Task> RequiresTwoFactorAsync(User user, ValidatedTokenRequest request) @@ -161,7 +131,7 @@ public class TwoFactorAuthenticationValidator( // These cases we want to always return false, U2f is deprecated and OrganizationDuo // uses a different flow than the other two factor providers, it follows the same - // structure of a UserTokenProvider but has it's logic ran outside the usual token + // structure of a UserTokenProvider but has it's logic runs outside the usual token // provider flow. See IOrganizationDuoUniversalTokenProvider.cs if (type is TwoFactorProviderType.U2f or TwoFactorProviderType.OrganizationDuo) { @@ -171,12 +141,12 @@ public class TwoFactorAuthenticationValidator( // Now we are concerning the rest of the Two Factor Provider Types // The intent of this check is to make sure that the user is using a 2FA provider that - // is enabled and allowed by their premium status. The exception for Remember - // is because it is a "special" 2FA type that isn't ever explicitly + // is enabled and allowed by their premium status. + // The exception for Remember is because it is a "special" 2FA type that isn't ever explicitly // enabled by a user, so we can't check the user's 2FA providers to see if they're // enabled. We just have to check if the token is valid. if (type != TwoFactorProviderType.Remember && - !await _userService.TwoFactorProviderIsEnabledAsync(type, user)) + user.GetTwoFactorProvider(type) == null) { return false; } diff --git a/src/Infrastructure.Dapper/Repositories/UserRepository.cs b/src/Infrastructure.Dapper/Repositories/UserRepository.cs index 28478a0c41..6b11d64cda 100644 --- a/src/Infrastructure.Dapper/Repositories/UserRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/UserRepository.cs @@ -253,7 +253,6 @@ public class UserRepository : Repository, IUserRepository } } - public async Task UpdateUserKeyAndEncryptedDataV2Async( User user, IEnumerable updateDataActions) @@ -289,7 +288,6 @@ public class UserRepository : Repository, IUserRepository UnprotectData(user); } - public async Task> GetManyAsync(IEnumerable ids) { using (var connection = new SqlConnection(ReadOnlyConnectionString)) @@ -318,6 +316,14 @@ public class UserRepository : Repository, IUserRepository } } + public async Task GetCalculatedPremiumAsync(Guid userId) + { + var result = await GetManyWithCalculatedPremiumAsync([userId]); + + UnprotectData(result); + return result.SingleOrDefault(); + } + private async Task ProtectDataAndSaveAsync(User user, Func saveTask) { if (user == null) diff --git a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs index 127646ed59..bd70e27e78 100644 --- a/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/UserRepository.cs @@ -1,10 +1,10 @@ using AutoMapper; using Bit.Core.KeyManagement.UserKey; +using Bit.Core.Models.Data; using Bit.Core.Repositories; using Bit.Infrastructure.EntityFramework.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using DataModel = Bit.Core.Models.Data; #nullable enable @@ -38,13 +38,13 @@ public class UserRepository : Repository, IUserR } } - public async Task GetKdfInformationByEmailAsync(string email) + public async Task GetKdfInformationByEmailAsync(string email) { using (var scope = ServiceScopeFactory.CreateScope()) { var dbContext = GetDatabaseContext(scope); return await GetDbSet(dbContext).Where(e => e.Email == email) - .Select(e => new DataModel.UserKdfInformation + .Select(e => new UserKdfInformation { Kdf = e.Kdf, KdfIterations = e.KdfIterations, @@ -251,13 +251,13 @@ public class UserRepository : Repository, IUserR } } - public async Task> GetManyWithCalculatedPremiumAsync(IEnumerable ids) + public async Task> GetManyWithCalculatedPremiumAsync(IEnumerable ids) { using (var scope = ServiceScopeFactory.CreateScope()) { var dbContext = GetDatabaseContext(scope); var users = dbContext.Users.Where(x => ids.Contains(x.Id)); - return await users.Select(e => new DataModel.UserWithCalculatedPremium(e) + return await users.Select(e => new UserWithCalculatedPremium(e) { HasPremiumAccess = e.Premium || dbContext.OrganizationUsers .Any(ou => ou.UserId == e.Id && @@ -269,6 +269,12 @@ public class UserRepository : Repository, IUserR } } + public async Task GetCalculatedPremiumAsync(Guid id) + { + var result = await GetManyWithCalculatedPremiumAsync([id]); + return result.FirstOrDefault(); + } + public override async Task DeleteAsync(Core.Entities.User user) { using (var scope = ServiceScopeFactory.CreateScope()) diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs index bd22fd9346..c617f6c9a9 100644 --- a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs @@ -14,6 +14,7 @@ using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces; using Bit.Core.Entities; using Bit.Core.Exceptions; @@ -40,6 +41,7 @@ public class AccountsControllerTests : IDisposable private readonly IPolicyService _policyService; private readonly ISetInitialMasterPasswordCommand _setInitialMasterPasswordCommand; private readonly IRotateUserKeyCommand _rotateUserKeyCommand; + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly ITdeOffboardingPasswordCommand _tdeOffboardingPasswordCommand; private readonly IFeatureService _featureService; @@ -64,6 +66,7 @@ public class AccountsControllerTests : IDisposable _policyService = Substitute.For(); _setInitialMasterPasswordCommand = Substitute.For(); _rotateUserKeyCommand = Substitute.For(); + _twoFactorIsEnabledQuery = Substitute.For(); _tdeOffboardingPasswordCommand = Substitute.For(); _featureService = Substitute.For(); _cipherValidator = @@ -87,6 +90,7 @@ public class AccountsControllerTests : IDisposable _setInitialMasterPasswordCommand, _tdeOffboardingPasswordCommand, _rotateUserKeyCommand, + _twoFactorIsEnabledQuery, _featureService, _cipherValidator, _folderValidator, diff --git a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs index 03c05ef0f4..ebbfc2a2ba 100644 --- a/test/Api.Test/Vault/Controllers/SyncControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/SyncControllerTests.cs @@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Models; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -64,6 +65,7 @@ public class SyncControllerTests { // Get dependencies var userService = sutProvider.GetDependency(); + var twoFactorIsEnabledQuery = sutProvider.GetDependency(); var organizationUserRepository = sutProvider.GetDependency(); var providerUserRepository = sutProvider.GetDependency(); var folderRepository = sutProvider.GetDependency(); @@ -119,7 +121,7 @@ public class SyncControllerTests collectionRepository.GetManyByUserIdAsync(user.Id).Returns(collections); collectionCipherRepository.GetManyByUserIdAsync(user.Id).Returns(new List()); // Back to standard test setup - userService.TwoFactorIsEnabledAsync(user).Returns(false); + twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false); userService.HasPremiumFromOrganization(user).Returns(false); // Execute GET @@ -129,7 +131,7 @@ public class SyncControllerTests // Asserts // Assert that methods are called var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled); - await this.AssertMethodsCalledAsync(userService, organizationUserRepository, providerUserRepository, folderRepository, + await this.AssertMethodsCalledAsync(userService, twoFactorIsEnabledQuery, organizationUserRepository, providerUserRepository, folderRepository, cipherRepository, sendRepository, collectionRepository, collectionCipherRepository, hasEnabledOrgs); Assert.IsType(result); @@ -155,6 +157,7 @@ public class SyncControllerTests { // Get dependencies var userService = sutProvider.GetDependency(); + var twoFactorIsEnabledQuery = sutProvider.GetDependency(); var organizationUserRepository = sutProvider.GetDependency(); var providerUserRepository = sutProvider.GetDependency(); var folderRepository = sutProvider.GetDependency(); @@ -205,7 +208,7 @@ public class SyncControllerTests policyRepository.GetManyByUserIdAsync(user.Id).Returns(policies); - userService.TwoFactorIsEnabledAsync(user).Returns(false); + twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false); userService.HasPremiumFromOrganization(user).Returns(false); // Execute GET @@ -216,7 +219,7 @@ public class SyncControllerTests // Assert that methods are called var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled); - await this.AssertMethodsCalledAsync(userService, organizationUserRepository, providerUserRepository, folderRepository, + await this.AssertMethodsCalledAsync(userService, twoFactorIsEnabledQuery, organizationUserRepository, providerUserRepository, folderRepository, cipherRepository, sendRepository, collectionRepository, collectionCipherRepository, hasEnabledOrgs); Assert.IsType(result); @@ -244,6 +247,7 @@ public class SyncControllerTests { // Get dependencies var userService = sutProvider.GetDependency(); + var twoFactorIsEnabledQuery = sutProvider.GetDependency(); var organizationUserRepository = sutProvider.GetDependency(); var providerUserRepository = sutProvider.GetDependency(); var folderRepository = sutProvider.GetDependency(); @@ -283,7 +287,7 @@ public class SyncControllerTests collectionRepository.GetManyByUserIdAsync(user.Id).Returns(collections); collectionCipherRepository.GetManyByUserIdAsync(user.Id).Returns(new List()); // Back to standard test setup - userService.TwoFactorIsEnabledAsync(user).Returns(false); + twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false); userService.HasPremiumFromOrganization(user).Returns(false); // Execute GET @@ -293,7 +297,7 @@ public class SyncControllerTests // Assert that methods are called var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled); - await this.AssertMethodsCalledAsync(userService, organizationUserRepository, providerUserRepository, folderRepository, + await this.AssertMethodsCalledAsync(userService, twoFactorIsEnabledQuery, organizationUserRepository, providerUserRepository, folderRepository, cipherRepository, sendRepository, collectionRepository, collectionCipherRepository, hasEnabledOrgs); Assert.IsType(result); @@ -315,6 +319,7 @@ public class SyncControllerTests private async Task AssertMethodsCalledAsync(IUserService userService, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IOrganizationUserRepository organizationUserRepository, IProviderUserRepository providerUserRepository, IFolderRepository folderRepository, ICipherRepository cipherRepository, ISendRepository sendRepository, @@ -356,7 +361,7 @@ public class SyncControllerTests .GetManyByUserIdAsync(default); } - await userService.ReceivedWithAnyArgs(1) + await twoFactorIsEnabledQuery.ReceivedWithAnyArgs(1) .TwoFactorIsEnabledAsync(default(ITwoFactorProvidersUser)); await userService.ReceivedWithAnyArgs(1) .HasPremiumFromOrganization(default); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs index 2dda23481a..baf844acae 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs @@ -2,6 +2,7 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; @@ -28,6 +29,7 @@ namespace Bit.Core.Test.OrganizationFeatures.OrganizationUsers; public class AcceptOrgUserCommandTests { private readonly IUserService _userService = Substitute.For(); + private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery = Substitute.For(); private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory = Substitute.For(); private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory(); @@ -165,7 +167,7 @@ public class AcceptOrgUserCommandTests SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); // User doesn't have 2FA enabled - _userService.TwoFactorIsEnabledAsync(user).Returns(false); + _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false); // Organization they are trying to join requires 2FA var twoFactorPolicy = new OrganizationUserPolicyDetails { OrganizationId = orgUser.OrganizationId }; @@ -646,7 +648,7 @@ public class AcceptOrgUserCommandTests .Returns(false); // User doesn't have 2FA enabled - _userService.TwoFactorIsEnabledAsync(user).Returns(false); + _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false); // Org does not require 2FA sutProvider.GetDependency().GetPoliciesApplicableToUserAsync(user.Id, diff --git a/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs b/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs index da2d4a282a..ff09e1f141 100644 --- a/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs +++ b/test/Core.Test/Auth/Identity/BaseTokenProviderTests.cs @@ -44,9 +44,6 @@ public abstract class BaseTokenProviderTests protected virtual void SetupUserService(IUserService userService, User user) { - userService - .TwoFactorProviderIsEnabledAsync(TwoFactorProviderType, user) - .Returns(true); userService .CanAccessPremium(user) .Returns(true); @@ -85,8 +82,6 @@ public abstract class BaseTokenProviderTests var userManager = SubstituteUserManager(); MockDatabase(user, metaData); - AdditionalSetup(sutProvider, user); - var response = await sutProvider.Sut.CanGenerateTwoFactorTokenAsync(userManager, user); Assert.Equal(expectedResponse, response); } diff --git a/test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs b/test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs index 85c687119b..5715403974 100644 --- a/test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs +++ b/test/Core.Test/Auth/Identity/DuoUniversalTwoFactorTokenProviderTests.cs @@ -83,6 +83,7 @@ public class DuoUniversalTwoFactorTokenProviderTests : BaseTokenProviderTests sutProvider) { // Arrange + AdditionalSetup(sutProvider, user); user.Premium = true; user.PremiumExpirationDate = DateTime.UtcNow.AddDays(1); @@ -100,6 +101,8 @@ public class DuoUniversalTwoFactorTokenProviderTests : BaseTokenProviderTests sutProvider) { // Arrange + AdditionalSetup(sutProvider, user); + user.Premium = false; sutProvider.GetDependency() diff --git a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQueryTests.cs b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQueryTests.cs index 8011c52ead..adeac45d06 100644 --- a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQueryTests.cs +++ b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/TwoFactorIsEnabledQueryTests.cs @@ -5,6 +5,7 @@ using Bit.Core.Entities; using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; +using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -53,6 +54,39 @@ public class TwoFactorIsEnabledQueryTests } } + [Theory, BitAutoData] + public async Task TwoFactorIsEnabledQuery_DatabaseReturnsEmpty_ResultEmpty( + SutProvider sutProvider, + List usersWithCalculatedPremium) + { + // Arrange + var userIds = usersWithCalculatedPremium.Select(u => u.Id).ToList(); + + sutProvider.GetDependency() + .GetManyWithCalculatedPremiumAsync(Arg.Any>()) + .Returns([]); + + // Act + var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(userIds); + + // Assert + Assert.Empty(result); + } + + [Theory] + [BitAutoData((IEnumerable)null)] + [BitAutoData([])] + public async Task TwoFactorIsEnabledQuery_UserIdsNullorEmpty_ResultEmpty( + IEnumerable userIds, + SutProvider sutProvider) + { + // Act + var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(userIds); + + // Assert + Assert.Empty(result); + } + [Theory] [BitAutoData] public async Task TwoFactorIsEnabledQuery_WithNoTwoFactorEnabled_ReturnsAllTwoFactorDisabled( @@ -122,8 +156,11 @@ public class TwoFactorIsEnabledQueryTests } [Theory] - [BitAutoData] - public async Task TwoFactorIsEnabledQuery_WithNullTwoFactorProviders_ReturnsAllTwoFactorDisabled( + [BitAutoData("")] + [BitAutoData("{}")] + [BitAutoData((string)null)] + public async Task TwoFactorIsEnabledQuery_WithNullOrEmptyTwoFactorProviders_ReturnsAllTwoFactorDisabled( + string twoFactorProviders, SutProvider sutProvider, List usersWithCalculatedPremium) { @@ -132,7 +169,7 @@ public class TwoFactorIsEnabledQueryTests foreach (var user in usersWithCalculatedPremium) { - user.TwoFactorProviders = null; // No two-factor providers configured + user.TwoFactorProviders = twoFactorProviders; // No two-factor providers configured } sutProvider.GetDependency() @@ -176,6 +213,24 @@ public class TwoFactorIsEnabledQueryTests .GetManyWithCalculatedPremiumAsync(default); } + [Theory] + [BitAutoData] + public async Task TwoFactorIsEnabledQuery_UserIdNull_ReturnsFalse( + SutProvider sutProvider) + { + // Arrange + var user = new TestTwoFactorProviderUser + { + Id = null + }; + + // Act + var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user); + + // Assert + Assert.False(result); + } + [Theory] [BitAutoData(TwoFactorProviderType.Authenticator)] [BitAutoData(TwoFactorProviderType.Email)] @@ -193,10 +248,8 @@ public class TwoFactorIsEnabledQueryTests { freeProviderType, new TwoFactorProvider { Enabled = true } } }; - user.Premium = false; user.SetTwoFactorProviders(twoFactorProviders); - // Act var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user); @@ -205,7 +258,7 @@ public class TwoFactorIsEnabledQueryTests await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() - .GetManyWithCalculatedPremiumAsync(default); + .GetCalculatedPremiumAsync(default); } [Theory] @@ -230,7 +283,7 @@ public class TwoFactorIsEnabledQueryTests await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() - .GetManyWithCalculatedPremiumAsync(default); + .GetCalculatedPremiumAsync(default); } [Theory] @@ -252,14 +305,18 @@ public class TwoFactorIsEnabledQueryTests user.SetTwoFactorProviders(twoFactorProviders); sutProvider.GetDependency() - .GetManyWithCalculatedPremiumAsync(Arg.Is>(i => i.Contains(user.Id))) - .Returns(new List { user }); + .GetCalculatedPremiumAsync(user.Id) + .Returns(user); // Act var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user); // Assert Assert.False(result); + + await sutProvider.GetDependency() + .ReceivedWithAnyArgs(1) + .GetCalculatedPremiumAsync(default); } [Theory] @@ -268,7 +325,7 @@ public class TwoFactorIsEnabledQueryTests public async Task TwoFactorIsEnabledQuery_WithProviderTypeRequiringPremium_WithUserPremium_ReturnsTrue( TwoFactorProviderType premiumProviderType, SutProvider sutProvider, - User user) + UserWithCalculatedPremium user) { // Arrange var twoFactorProviders = new Dictionary @@ -276,9 +333,14 @@ public class TwoFactorIsEnabledQueryTests { premiumProviderType, new TwoFactorProvider { Enabled = true } } }; - user.Premium = true; + user.Premium = false; + user.HasPremiumAccess = true; user.SetTwoFactorProviders(twoFactorProviders); + sutProvider.GetDependency() + .GetCalculatedPremiumAsync(user.Id) + .Returns(user); + // Act var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user); @@ -286,8 +348,8 @@ public class TwoFactorIsEnabledQueryTests Assert.True(result); await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .GetManyWithCalculatedPremiumAsync(default); + .ReceivedWithAnyArgs(1) + .GetCalculatedPremiumAsync(default); } [Theory] @@ -309,14 +371,18 @@ public class TwoFactorIsEnabledQueryTests user.SetTwoFactorProviders(twoFactorProviders); sutProvider.GetDependency() - .GetManyWithCalculatedPremiumAsync(Arg.Is>(i => i.Contains(user.Id))) - .Returns(new List { user }); + .GetCalculatedPremiumAsync(user.Id) + .Returns(user); // Act var result = await sutProvider.Sut.TwoFactorIsEnabledAsync(user); // Assert Assert.True(result); + + await sutProvider.GetDependency() + .ReceivedWithAnyArgs(1) + .GetCalculatedPremiumAsync(default); } [Theory] @@ -333,5 +399,29 @@ public class TwoFactorIsEnabledQueryTests // Assert Assert.False(result); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .GetCalculatedPremiumAsync(default); + } + + private class TestTwoFactorProviderUser : ITwoFactorProvidersUser + { + public Guid? Id { get; set; } + public string TwoFactorProviders { get; set; } + public bool Premium { get; set; } + public Dictionary GetTwoFactorProviders() + { + return JsonHelpers.LegacyDeserialize>(TwoFactorProviders); + } + + public Guid? GetUserId() + { + return Id; + } + + public bool GetPremium() + { + return Premium; + } } } diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index 02ff24d9bf..d9bb2beaca 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -9,6 +9,7 @@ using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Services; using Bit.Core.Context; using Bit.Core.Entities; @@ -324,6 +325,7 @@ public class UserServiceTests sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), + sutProvider.GetDependency(), sutProvider.GetDependency() ); @@ -476,6 +478,9 @@ public class UserServiceTests sutProvider.GetDependency() .GetByIdAsync(organization.Id) .Returns(organization); + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(user) + .Returns(true); var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary { [TwoFactorProviderType.Remember] = new() { Enabled = true } @@ -911,6 +916,7 @@ public class UserServiceTests sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), + sutProvider.GetDependency(), sutProvider.GetDependency() ); } diff --git a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs index fb4d7c321a..1f075a0147 100644 --- a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs @@ -2,6 +2,7 @@ using Bit.Core.Auth.Enums; using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Models.Data.Organizations; @@ -27,11 +28,11 @@ public class TwoFactorAuthenticationValidatorTests private readonly IUserService _userService; private readonly UserManagerTestWrapper _userManager; private readonly IOrganizationDuoUniversalTokenProvider _organizationDuoUniversalTokenProvider; - private readonly IFeatureService _featureService; private readonly IApplicationCacheService _applicationCacheService; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationRepository _organizationRepository; private readonly IDataProtectorTokenFactory _ssoEmail2faSessionTokenable; + private readonly ITwoFactorIsEnabledQuery _twoFactorenabledQuery; private readonly ICurrentContext _currentContext; private readonly TwoFactorAuthenticationValidator _sut; @@ -40,22 +41,22 @@ public class TwoFactorAuthenticationValidatorTests _userService = Substitute.For(); _userManager = SubstituteUserManager(); _organizationDuoUniversalTokenProvider = Substitute.For(); - _featureService = Substitute.For(); _applicationCacheService = Substitute.For(); _organizationUserRepository = Substitute.For(); _organizationRepository = Substitute.For(); _ssoEmail2faSessionTokenable = Substitute.For>(); + _twoFactorenabledQuery = Substitute.For(); _currentContext = Substitute.For(); _sut = new TwoFactorAuthenticationValidator( _userService, _userManager, _organizationDuoUniversalTokenProvider, - _featureService, _applicationCacheService, _organizationUserRepository, _organizationRepository, _ssoEmail2faSessionTokenable, + _twoFactorenabledQuery, _currentContext); } @@ -263,9 +264,6 @@ public class TwoFactorAuthenticationValidatorTests _userManager.SUPPORTS_TWO_FACTOR = true; _userManager.TWO_FACTOR_PROVIDERS = [providerType.ToString()]; - _userService.TwoFactorProviderIsEnabledAsync(Arg.Any(), user) - .Returns(true); - // Act var result = await _sut.BuildTwoFactorResultAsync(user, null); @@ -322,9 +320,6 @@ public class TwoFactorAuthenticationValidatorTests string token) { // Arrange - _userService.TwoFactorProviderIsEnabledAsync( - TwoFactorProviderType.Email, user).Returns(true); - _userManager.TWO_FACTOR_PROVIDERS = ["email"]; // Act @@ -342,10 +337,8 @@ public class TwoFactorAuthenticationValidatorTests string token) { // Arrange - _userService.TwoFactorProviderIsEnabledAsync( - TwoFactorProviderType.Email, user).Returns(false); - _userManager.TWO_FACTOR_PROVIDERS = ["email"]; + user.TwoFactorProviders = ""; // Act var result = await _sut.VerifyTwoFactorAsync( @@ -362,9 +355,6 @@ public class TwoFactorAuthenticationValidatorTests string token) { // Arrange - _userService.TwoFactorProviderIsEnabledAsync( - TwoFactorProviderType.OrganizationDuo, user).Returns(false); - _userManager.TWO_FACTOR_PROVIDERS = ["OrganizationDuo"]; // Act @@ -387,11 +377,9 @@ public class TwoFactorAuthenticationValidatorTests string token) { // Arrange - _userService.TwoFactorProviderIsEnabledAsync( - providerType, user).Returns(true); - _userManager.TWO_FACTOR_ENABLED = true; _userManager.TWO_FACTOR_TOKEN_VERIFIED = true; + user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType); // Act var result = await _sut.VerifyTwoFactorAsync(user, null, providerType, token); @@ -412,11 +400,9 @@ public class TwoFactorAuthenticationValidatorTests string token) { // Arrange - _userService.TwoFactorProviderIsEnabledAsync( - providerType, user).Returns(true); - _userManager.TWO_FACTOR_ENABLED = true; _userManager.TWO_FACTOR_TOKEN_VERIFIED = false; + user.TwoFactorProviders = GetTwoFactorIndividualProviderJson(providerType); // Act var result = await _sut.VerifyTwoFactorAsync(user, null, providerType, token); From ead5bbdd2a513e4627ad205577c2f83205b3d731 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Fri, 9 May 2025 12:13:01 -0400 Subject: [PATCH 039/114] [PM-21281] Email TOTP sent twice when user only has Email MFA enabled (#5782) * fix: addressed bug where email token is sent twice, * test: updating tests to have correct DI and removing test for automatic email of TOTP. --- .../Tokenables/SsoEmail2faSessionTokenable.cs | 7 ++++--- .../TwoFactorAuthenticationValidator.cs | 11 ++++------- .../TwoFactorAuthenticationValidatorTests.cs | 8 +++----- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/Core/Auth/Models/Business/Tokenables/SsoEmail2faSessionTokenable.cs b/src/Core/Auth/Models/Business/Tokenables/SsoEmail2faSessionTokenable.cs index 24a74bde07..30687a6a4a 100644 --- a/src/Core/Auth/Models/Business/Tokenables/SsoEmail2faSessionTokenable.cs +++ b/src/Core/Auth/Models/Business/Tokenables/SsoEmail2faSessionTokenable.cs @@ -4,9 +4,10 @@ using Bit.Core.Tokens; namespace Bit.Core.Auth.Models.Business.Tokenables; -// This token just provides a verifiable authN mechanism for the API service -// TwoFactorController.cs SendEmailLogin anonymous endpoint so it cannot be -// used maliciously. +/// +/// This token provides a verifiable authN mechanism for the TwoFactorController.SendEmailLoginAsync +/// anonymous endpoint so it cannot used maliciously. +/// public class SsoEmail2faSessionTokenable : ExpiringTokenable { // Just over 2 min expiration (client expires session after 2 min) diff --git a/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs index 80b3b6e1f4..000f98c006 100644 --- a/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/TwoFactorAuthenticationValidator.cs @@ -91,7 +91,10 @@ public class TwoFactorAuthenticationValidator( { "TwoFactorProviders2", providers }, }; - // If we have email as a 2FA provider, we might need an SsoEmail2fa Session Token + // If we have an Email 2FA provider we need this session token so SSO users + // can re-request an email TOTP. The TwoFactorController.SendEmailLoginAsync + // endpoint requires a way to authenticate the user before sending another email with + // a TOTP, this token acts as the authentication mechanism. if (enabledProviders.Any(p => p.Key == TwoFactorProviderType.Email)) { twoFactorResultDict.Add("SsoEmail2faSessionToken", @@ -100,12 +103,6 @@ public class TwoFactorAuthenticationValidator( twoFactorResultDict.Add("Email", user.Email); } - if (enabledProviders.Count == 1 && enabledProviders.First().Key == TwoFactorProviderType.Email) - { - // Send email now if this is their only 2FA method - await _userService.SendTwoFactorEmailAsync(user); - } - return twoFactorResultDict; } diff --git a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs index 1f075a0147..53e9a00c9f 100644 --- a/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs +++ b/test/Identity.Test/IdentityServer/TwoFactorAuthenticationValidatorTests.cs @@ -252,9 +252,9 @@ public class TwoFactorAuthenticationValidatorTests [Theory] [BitAutoData(TwoFactorProviderType.Email)] - public async void BuildTwoFactorResultAsync_IndividualEmailProvider_SendsEmail_SetsSsoToken_ReturnsNotNull( - TwoFactorProviderType providerType, - User user) + public async void BuildTwoFactorResultAsync_SetsSsoToken_ReturnsNotNull( + TwoFactorProviderType providerType, + User user) { // Arrange var providerTypeInt = (int)providerType; @@ -276,8 +276,6 @@ public class TwoFactorAuthenticationValidatorTests Assert.True(providers.ContainsKey(providerTypeInt.ToString())); Assert.True(result.ContainsKey("SsoEmail2faSessionToken")); Assert.True(result.ContainsKey("Email")); - - await _userService.Received(1).SendTwoFactorEmailAsync(Arg.Any()); } [Theory] From 0075a15485daf3845c4dab42b7e3729850b131ad Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 9 May 2025 13:43:50 -0400 Subject: [PATCH 040/114] [PM-18064] Resolve billing warnings (#5797) * Resolve Billing warnings * Remove exclusions * Format --- src/Api/Api.csproj | 2 -- .../Controllers/OrganizationBillingController.cs | 2 ++ .../Billing/Services/IProviderBillingService.cs | 2 +- .../Implementations/PaymentHistoryService.cs | 4 +--- src/Core/Core.csproj | 2 -- .../Billing/Services/PaymentHistoryServiceTests.cs | 13 ++++--------- 6 files changed, 8 insertions(+), 17 deletions(-) diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj index 6505fdab5b..c490e90150 100644 --- a/src/Api/Api.csproj +++ b/src/Api/Api.csproj @@ -4,8 +4,6 @@ false bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml true - - $(WarningsNotAsErrors);CS8604 diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index 2f0a4ef48b..b82c627ee0 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -1,4 +1,5 @@ #nullable enable +using System.Diagnostics; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; @@ -292,6 +293,7 @@ public class OrganizationBillingController( sale.SubscriptionSetup.SkipTrial = true; await organizationBillingService.Finalize(sale); var org = await organizationRepository.GetByIdAsync(organizationId); + Debug.Assert(org is not null, "This organization has already been found via this same ID, this should be fine."); if (organizationSignup.PaymentMethodType != null) { var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken); diff --git a/src/Core/Billing/Services/IProviderBillingService.cs b/src/Core/Billing/Services/IProviderBillingService.cs index 6ed8910dd8..0171a7e1c3 100644 --- a/src/Core/Billing/Services/IProviderBillingService.cs +++ b/src/Core/Billing/Services/IProviderBillingService.cs @@ -59,7 +59,7 @@ public interface IProviderBillingService int seatAdjustment); /// - /// Determines whether the provided will result in a purchase for the 's . + /// Determines whether the provided will result in a purchase for the 's . /// Seat adjustments that result in purchases include: /// /// The going from below the seat minimum to above the seat minimum for the provided diff --git a/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs b/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs index 6e984f946e..5a8cf16f5a 100644 --- a/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs +++ b/src/Core/Billing/Services/Implementations/PaymentHistoryService.cs @@ -5,14 +5,12 @@ using Bit.Core.Entities; using Bit.Core.Models.BitStripe; using Bit.Core.Repositories; using Bit.Core.Services; -using Microsoft.Extensions.Logging; namespace Bit.Core.Billing.Services.Implementations; public class PaymentHistoryService( IStripeAdapter stripeAdapter, - ITransactionRepository transactionRepository, - ILogger logger) : IPaymentHistoryService + ITransactionRepository transactionRepository) : IPaymentHistoryService { public async Task> GetInvoiceHistoryAsync( ISubscriber subscriber, diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 4411a3de9b..898f0550b0 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -3,8 +3,6 @@ false bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml - - $(WarningsNotAsErrors);CS1574;CS9113 diff --git a/test/Core.Test/Billing/Services/PaymentHistoryServiceTests.cs b/test/Core.Test/Billing/Services/PaymentHistoryServiceTests.cs index c9278e4488..06a408c5a8 100644 --- a/test/Core.Test/Billing/Services/PaymentHistoryServiceTests.cs +++ b/test/Core.Test/Billing/Services/PaymentHistoryServiceTests.cs @@ -4,7 +4,6 @@ using Bit.Core.Entities; using Bit.Core.Models.BitStripe; using Bit.Core.Repositories; using Bit.Core.Services; -using Microsoft.Extensions.Logging; using NSubstitute; using Stripe; using Xunit; @@ -22,8 +21,7 @@ public class PaymentHistoryServiceTests var stripeAdapter = Substitute.For(); stripeAdapter.InvoiceListAsync(Arg.Any()).Returns(invoices); var transactionRepository = Substitute.For(); - var logger = Substitute.For>(); - var paymentHistoryService = new PaymentHistoryService(stripeAdapter, transactionRepository, logger); + var paymentHistoryService = new PaymentHistoryService(stripeAdapter, transactionRepository); // Act var result = await paymentHistoryService.GetInvoiceHistoryAsync(subscriber); @@ -40,8 +38,7 @@ public class PaymentHistoryServiceTests // Arrange var paymentHistoryService = new PaymentHistoryService( Substitute.For(), - Substitute.For(), - Substitute.For>()); + Substitute.For()); // Act var result = await paymentHistoryService.GetInvoiceHistoryAsync(null); @@ -59,8 +56,7 @@ public class PaymentHistoryServiceTests var transactionRepository = Substitute.For(); transactionRepository.GetManyByOrganizationIdAsync(subscriber.Id, Arg.Any(), Arg.Any()).Returns(transactions); var stripeAdapter = Substitute.For(); - var logger = Substitute.For>(); - var paymentHistoryService = new PaymentHistoryService(stripeAdapter, transactionRepository, logger); + var paymentHistoryService = new PaymentHistoryService(stripeAdapter, transactionRepository); // Act var result = await paymentHistoryService.GetTransactionHistoryAsync(subscriber); @@ -77,8 +73,7 @@ public class PaymentHistoryServiceTests // Arrange var paymentHistoryService = new PaymentHistoryService( Substitute.For(), - Substitute.For(), - Substitute.For>()); + Substitute.For()); // Act var result = await paymentHistoryService.GetTransactionHistoryAsync(null); From 15b498184f394e80ea1cbf8408941e89758e1919 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 9 May 2025 16:03:09 -0400 Subject: [PATCH 041/114] Resolve platform warnings (#5798) * Installation Repository tests * Formatting * Remove extra LastActivityDate property * Remove exclusion --- src/Core/Core.csproj | 4 ++ .../Infrastructure.EntityFramework.csproj | 5 -- .../Installations/Models/Installation.cs | 12 +---- .../InstallationRepositoryTests.cs | 46 +++++++++++++++++++ 4 files changed, 51 insertions(+), 16 deletions(-) create mode 100644 test/Infrastructure.IntegrationTest/Platform/Installations/InstallationRepositoryTests.cs diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 898f0550b0..6397e0b8ea 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -75,4 +75,8 @@ + + + + diff --git a/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj b/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj index 639d88524b..9814eef2aa 100644 --- a/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj +++ b/src/Infrastructure.EntityFramework/Infrastructure.EntityFramework.csproj @@ -1,10 +1,5 @@ - - - $(WarningsNotAsErrors);CS0108 - - diff --git a/src/Infrastructure.EntityFramework/Platform/Installations/Models/Installation.cs b/src/Infrastructure.EntityFramework/Platform/Installations/Models/Installation.cs index 96b60a39ed..601ae993b3 100644 --- a/src/Infrastructure.EntityFramework/Platform/Installations/Models/Installation.cs +++ b/src/Infrastructure.EntityFramework/Platform/Installations/Models/Installation.cs @@ -3,22 +3,12 @@ using C = Bit.Core.Platform.Installations; namespace Bit.Infrastructure.EntityFramework.Platform; -public class Installation : C.Installation -{ - // Shadow property - to be introduced by https://bitwarden.atlassian.net/browse/PM-11129 - // This isn't a value or entity used by self hosted servers, but it's - // being added for synchronicity between database provider options. - public DateTime? LastActivityDate { get; set; } -} +public class Installation : C.Installation; public class InstallationMapperProfile : Profile { public InstallationMapperProfile() { - CreateMap() - // Shadow property - to be introduced by https://bitwarden.atlassian.net/browse/PM-11129 - .ForMember(i => i.LastActivityDate, opt => opt.Ignore()) - .ReverseMap(); CreateMap().ReverseMap(); } } diff --git a/test/Infrastructure.IntegrationTest/Platform/Installations/InstallationRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Platform/Installations/InstallationRepositoryTests.cs new file mode 100644 index 0000000000..2d212d4e39 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/Platform/Installations/InstallationRepositoryTests.cs @@ -0,0 +1,46 @@ +using Bit.Core.Platform.Installations; +using Bit.Infrastructure.IntegrationTest.Comparers; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.Platform.Installations; + +public class InstallationRepositoryTests +{ + [DatabaseTheory, DatabaseData] + public async Task GetByIdAsync_Works(IInstallationRepository installationRepository) + { + var installation = await installationRepository.CreateAsync(new Installation + { + Email = "test@email.com", + Key = "installation_key", + Enabled = true, + }); + + var retrievedInstallation = await installationRepository.GetByIdAsync(installation.Id); + + Assert.NotNull(retrievedInstallation); + Assert.Equal("installation_key", retrievedInstallation.Key); + } + + [DatabaseTheory, DatabaseData] + public async Task UpdateAsync_Works(IInstallationRepository installationRepository) + { + var installation = await installationRepository.CreateAsync(new Installation + { + Email = "test@email.com", + Key = "installation_key", + Enabled = true, + }); + + var now = DateTime.UtcNow; + + installation.LastActivityDate = now; + + await installationRepository.ReplaceAsync(installation); + + var retrievedInstallation = await installationRepository.GetByIdAsync(installation.Id); + + Assert.NotNull(retrievedInstallation.LastActivityDate); + Assert.Equal(now, retrievedInstallation.LastActivityDate.Value, LaxDateTimeComparer.Default); + } +} From 3a848d5747b59b4ffb78a82264dc527745c99705 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 12 May 2025 09:12:37 -0400 Subject: [PATCH 042/114] Move `Microsoft.Build.Sql` version to global.json (#5810) --- global.json | 3 ++- src/Sql/Sql.sqlproj | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/global.json b/global.json index 0c1d58f410..d04c13bbb5 100644 --- a/global.json +++ b/global.json @@ -4,6 +4,7 @@ "rollForward": "latestFeature" }, "msbuild-sdks": { - "Microsoft.Build.Traversal": "4.1.0" + "Microsoft.Build.Traversal": "4.1.0", + "Microsoft.Build.Sql": "0.1.9-preview" } } diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index 65524fca45..849fd3bdfd 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -1,6 +1,6 @@  - + Sql {58554e52-fdec-4832-aff9-302b01e08dca} From e4359f071c89df5be7ceb31cb489d35bb199c8f9 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Mon, 12 May 2025 18:21:07 +0100 Subject: [PATCH 043/114] [PM-21097] Fix: Prevent admin-added sponsored families from appearing in individual vault settings (#5767) * Changes to resolve sponsorship showing in individual vault * Resolve the failing unit test Signed-off-by: Cy Okeke * Resolve the failing test * Resolve the failing test * Resolve the failing test * fix make IsAdminInitiated nullable Signed-off-by: Cy Okeke * Add the isAdminInitiated property Signed-off-by: Cy Okeke * Resolve the database error Signed-off-by: Cy Okeke * Resolve the failing unit test Signed-off-by: Cy Okeke * Resolve the scan error Signed-off-by: Cy Okeke * Resolve the database issue * resolve the database build error * Resolve the database build error * Resolve the synchronization issue --------- Signed-off-by: Cy Okeke --- .../ProfileOrganizationResponseModel.cs | 4 +- .../OrganizationSponsorshipsController.cs | 7 +- .../OrganizationUserOrganizationDetails.cs | 1 + .../OrganizationSponsorshipResponseModel.cs | 5 +- ...izationUserOrganizationDetailsViewQuery.cs | 6 +- ...ip_ReadBySponsoringOrganizationUserId.sql} | 9 +- ...rganizationUserOrganizationDetailsView.sql | 3 +- ...OrganizationSponsorshipsControllerTests.cs | 6 + .../OrganizationUserRepositoryTests.cs | 24 ++ ...-05_00_AddIsAdminInitiated_RefreshView.sql | 221 ++++++++++++++++++ ...hip_ReadBySponsoringOrganizationUserId.sql | 20 ++ 11 files changed, 294 insertions(+), 12 deletions(-) rename src/Sql/dbo/Stored Procedures/{OrganizationSponsorship_ReadBySponsoringOrganiationUserId.sql => OrganizationSponsorship_ReadBySponsoringOrganizationUserId.sql} (61%) create mode 100644 util/Migrator/DbScripts/2025-05-05_00_AddIsAdminInitiated_RefreshView.sql create mode 100644 util/Migrator/DbScripts/2025-05-05_01_AddIsAdminInitiated_OrganizationSponsorship_ReadBySponsoringOrganizationUserId.sql diff --git a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs index c74599a70e..5c900b73e0 100644 --- a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs @@ -58,7 +58,8 @@ public class ProfileOrganizationResponseModel : ResponseModel ProviderName = organization.ProviderName; ProviderType = organization.ProviderType; FamilySponsorshipFriendlyName = organization.FamilySponsorshipFriendlyName; - FamilySponsorshipAvailable = FamilySponsorshipFriendlyName == null && + IsAdminInitiated = organization.IsAdminInitiated ?? false; + FamilySponsorshipAvailable = (FamilySponsorshipFriendlyName == null || IsAdminInitiated) && StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise) .UsersCanSponsor(organization); ProductTierType = organization.PlanType.GetProductTier(); @@ -157,4 +158,5 @@ public class ProfileOrganizationResponseModel : ResponseModel public bool UserIsClaimedByOrganization { get; set; } public bool UseRiskInsights { get; set; } public bool UseAdminSponsoredFamilies { get; set; } + public bool IsAdminInitiated { get; set; } } diff --git a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs index b007c05730..c4dc5fae75 100644 --- a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs @@ -271,8 +271,11 @@ public class OrganizationSponsorshipsController : Controller } var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId); - return new ListResponseModel(sponsorships.Select(s => - new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s)))); + return new ListResponseModel( + sponsorships + .Where(s => s.IsAdminInitiated) + .Select(s => new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s))) + ); } diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs index 0771457d0a..a804dc0f6a 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs @@ -60,4 +60,5 @@ public class OrganizationUserOrganizationDetails public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool UseRiskInsights { get; set; } public bool UseAdminSponsoredFamilies { get; set; } + public bool? IsAdminInitiated { get; set; } } diff --git a/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipResponseModel.cs b/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipResponseModel.cs index 58c1b2cffb..e082d98de6 100644 --- a/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipResponseModel.cs +++ b/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipResponseModel.cs @@ -14,6 +14,7 @@ public class OrganizationSponsorshipResponseModel public bool ToDelete { get; set; } public bool CloudSponsorshipRemoved { get; set; } + public bool IsAdminInitiated { get; set; } public OrganizationSponsorshipResponseModel() { } @@ -27,6 +28,7 @@ public class OrganizationSponsorshipResponseModel ValidUntil = sponsorshipData.ValidUntil; ToDelete = sponsorshipData.ToDelete; CloudSponsorshipRemoved = sponsorshipData.CloudSponsorshipRemoved; + IsAdminInitiated = sponsorshipData.IsAdminInitiated; } public OrganizationSponsorshipData ToOrganizationSponsorship() @@ -40,7 +42,8 @@ public class OrganizationSponsorshipResponseModel LastSyncDate = LastSyncDate, ValidUntil = ValidUntil, ToDelete = ToDelete, - CloudSponsorshipRemoved = CloudSponsorshipRemoved + CloudSponsorshipRemoved = CloudSponsorshipRemoved, + IsAdminInitiated = IsAdminInitiated, }; } diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs index 69f40bebb4..793abff8a2 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs @@ -7,8 +7,7 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery Run(DatabaseContext dbContext) { var query = from ou in dbContext.OrganizationUsers - join o in dbContext.Organizations on ou.OrganizationId equals o.Id into outerOrganization - from o in outerOrganization.DefaultIfEmpty() + join o in dbContext.Organizations on ou.OrganizationId equals o.Id join su in dbContext.SsoUsers on new { ou.UserId, OrganizationId = (Guid?)ou.OrganizationId } equals new { UserId = (Guid?)su.UserId, su.OrganizationId } into su_g from su in su_g.DefaultIfEmpty() join po in dbContext.ProviderOrganizations on o.Id equals po.OrganizationId into po_g @@ -68,10 +67,11 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery() .GetManyBySponsoringOrganizationAsync(sponsoringOrg.Id).Returns(sponsorships); + // Set IsAdminInitiated to true for all test sponsorships + foreach (var sponsorship in sponsorships) + { + sponsorship.IsAdminInitiated = true; + } + // Act var result = await sutProvider.Sut.GetSponsoredOrganizations(sponsoringOrg.Id); diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs index 637e970f8f..fd759e4777 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs @@ -316,6 +316,29 @@ public class OrganizationUserRepositoryTests BillingEmail = user1.Email, // TODO: EF does not enforce this being NOT NULl Plan = "Test", // TODO: EF does not enforce this being NOT NULl PrivateKey = "privatekey", + UsePolicies = false, + UseSso = false, + UseKeyConnector = false, + UseScim = false, + UseGroups = false, + UseDirectory = false, + UseEvents = false, + UseTotp = false, + Use2fa = false, + UseApi = false, + UseResetPassword = false, + UseSecretsManager = false, + SelfHost = false, + UsersGetPremium = false, + UseCustomPermissions = false, + Enabled = true, + UsePasswordManager = false, + LimitCollectionCreation = false, + LimitCollectionDeletion = false, + LimitItemDeletion = false, + AllowAdminAccessToAllCollectionItems = false, + UseRiskInsights = false, + UseAdminSponsoredFamilies = false }); var organizationDomain = new OrganizationDomain @@ -335,6 +358,7 @@ public class OrganizationUserRepositoryTests UserId = user1.Id, Status = OrganizationUserStatusType.Confirmed, ResetPasswordKey = "resetpasswordkey1", + AccessSecretsManager = false }); await organizationUserRepository.CreateAsync(new OrganizationUser diff --git a/util/Migrator/DbScripts/2025-05-05_00_AddIsAdminInitiated_RefreshView.sql b/util/Migrator/DbScripts/2025-05-05_00_AddIsAdminInitiated_RefreshView.sql new file mode 100644 index 0000000000..8fd465025c --- /dev/null +++ b/util/Migrator/DbScripts/2025-05-05_00_AddIsAdminInitiated_RefreshView.sql @@ -0,0 +1,221 @@ +CREATE OR ALTER VIEW [dbo].[OrganizationUserOrganizationDetailsView] +AS +SELECT + OU.[UserId], + OU.[OrganizationId], + OU.[Id] OrganizationUserId, + O.[Name], + O.[Enabled], + O.[PlanType], + O.[UsePolicies], + O.[UseSso], + O.[UseKeyConnector], + O.[UseScim], + O.[UseGroups], + O.[UseDirectory], + O.[UseEvents], + O.[UseTotp], + O.[Use2fa], + O.[UseApi], + O.[UseResetPassword], + O.[SelfHost], + O.[UsersGetPremium], + O.[UseCustomPermissions], + O.[UseSecretsManager], + 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, + P.[Type] ProviderType, + SS.[Data] SsoConfig, + OS.[FriendlyName] FamilySponsorshipFriendlyName, + OS.[LastSyncDate] FamilySponsorshipLastSyncDate, + OS.[ToDelete] FamilySponsorshipToDelete, + OS.[ValidUntil] FamilySponsorshipValidUntil, + OU.[AccessSecretsManager], + O.[UsePasswordManager], + O.[SmSeats], + O.[SmServiceAccounts], + O.[LimitCollectionCreation], + O.[LimitCollectionDeletion], + O.[AllowAdminAccessToAllCollectionItems], + O.[UseRiskInsights], + O.[UseAdminSponsoredFamilies], + O.[LimitItemDeletion], + OS.[IsAdminInitiated] +FROM + [dbo].[OrganizationUser] OU + LEFT JOIN + [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] + LEFT JOIN + [dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId] + LEFT JOIN + [dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id] + LEFT JOIN + [dbo].[Provider] P ON P.[Id] = PO.[ProviderId] + LEFT JOIN + [dbo].[SsoConfig] SS ON SS.[OrganizationId] = OU.[OrganizationId] + LEFT JOIN + [dbo].[OrganizationSponsorship] OS ON OS.[SponsoringOrganizationUserID] = OU.[Id] +GO + +--Manually refresh [dbo].[OrganizationUserOrganizationDetailsView] +IF OBJECT_ID('[dbo].[OrganizationUserOrganizationDetailsView]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationUserOrganizationDetailsView]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationView]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationView]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorshipView]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorshipView]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationUserOrganizationDetailsView]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationUserOrganizationDetailsView]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_OrganizationUserDeleted]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_OrganizationUserDeleted]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_CreateMany]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_CreateMany]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_OrganizationUsersDeleted]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_OrganizationUsersDeleted]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_DeleteExpired]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_DeleteExpired]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_Update]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_Update]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_DeleteById]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_DeleteById]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_Create]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_Create]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_OrganizationDeleted]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_OrganizationDeleted]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_UpdateMany]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_UpdateMany]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_DeleteByIds]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_DeleteByIds]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadLatestBySponsoringOrganizationId]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_ReadLatestBySponsoringOrganizationId]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadByOfferedToEmail]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_ReadByOfferedToEmail]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadBySponsoredOrganizationId]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_ReadBySponsoredOrganizationId]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationId]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationId]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadById]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_ReadById]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationUserId]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationUserId]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationUserOrganizationDetails_ReadByUserIdStatus]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationUserOrganizationDetails_ReadByUserIdStatus]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationUserOrganizationDetails_ReadByUserIdStatusOrganizationId]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationUserOrganizationDetails_ReadByUserIdStatusOrganizationId]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationUser_DeleteById]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationUser_DeleteById]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationUser_DeleteByIds]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationUser_DeleteByIds]'; +END +GO + +IF OBJECT_ID('[dbo].[Organization_DeleteById]') IS NOT NULL +BEGIN +EXECUTE sp_refreshsqlmodule N'[dbo].[Organization_DeleteById]'; +END +GO diff --git a/util/Migrator/DbScripts/2025-05-05_01_AddIsAdminInitiated_OrganizationSponsorship_ReadBySponsoringOrganizationUserId.sql b/util/Migrator/DbScripts/2025-05-05_01_AddIsAdminInitiated_OrganizationSponsorship_ReadBySponsoringOrganizationUserId.sql new file mode 100644 index 0000000000..bb3bdee9b9 --- /dev/null +++ b/util/Migrator/DbScripts/2025-05-05_01_AddIsAdminInitiated_OrganizationSponsorship_ReadBySponsoringOrganizationUserId.sql @@ -0,0 +1,20 @@ +SET ANSI_NULLS ON +GO +SET QUOTED_IDENTIFIER ON +GO +ALTER PROCEDURE [dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationUserId] + @SponsoringOrganizationUserId UNIQUEIDENTIFIER, + @IsAdminInitiated BIT = 0 +AS +BEGIN + SET NOCOUNT ON; + + SELECT + * + FROM + [dbo].[OrganizationSponsorshipView] + WHERE + [SponsoringOrganizationUserId] = @SponsoringOrganizationUserId + and [IsAdminInitiated] = @IsAdminInitiated +END +GO From 952967b8b3ed5850814e0c2ae7d1f253ed6debd5 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Mon, 12 May 2025 14:57:19 -0400 Subject: [PATCH 044/114] Fix SQL casing warnings (#5809) * Turn of casing validation for identifiers * Try fixing in reference files * Remove commented out xml prop --- .../OrganizationDomainSsoDetails_ReadByEmail.sql | 8 ++++---- ...tionSponsorship_ReadBySponsoringOrganizationUserId.sql | 4 ++-- .../VerfiedOrganaizationDomainSsoDetails_ReadByEmail.sql | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Sql/dbo/Stored Procedures/OrganizationDomainSsoDetails_ReadByEmail.sql b/src/Sql/dbo/Stored Procedures/OrganizationDomainSsoDetails_ReadByEmail.sql index 5cc47213d6..262d4bfd8d 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationDomainSsoDetails_ReadByEmail.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationDomainSsoDetails_ReadByEmail.sql @@ -3,9 +3,9 @@ CREATE PROCEDURE [dbo].[OrganizationDomainSsoDetails_ReadByEmail] AS BEGIN SET NOCOUNT ON - + DECLARE @Domain NVARCHAR(256) - + SELECT @Domain = SUBSTRING(@Email, CHARINDEX( '@', @Email) + 1, LEN(@Email)) SELECT @@ -19,8 +19,8 @@ BEGIN [dbo].[OrganizationView] O INNER JOIN [dbo].[OrganizationDomainView] OD ON O.Id = OD.OrganizationId - LEFT JOIN [dbo].[Ssoconfig] S + LEFT JOIN [dbo].[SsoConfig] S ON O.Id = S.OrganizationId WHERE OD.DomainName = @Domain AND O.Enabled = 1 -END \ No newline at end of file +END diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoringOrganizationUserId.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoringOrganizationUserId.sql index fe248e7ccd..520a902601 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoringOrganizationUserId.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoringOrganizationUserId.sql @@ -10,6 +10,6 @@ BEGIN FROM [dbo].[OrganizationSponsorshipView] WHERE - [SponsoringOrganizationUserId] = @SponsoringOrganizationUserId + [SponsoringOrganizationUserID] = @SponsoringOrganizationUserId and [IsAdminInitiated] = @IsAdminInitiated -END \ No newline at end of file +END diff --git a/src/Sql/dbo/Stored Procedures/VerfiedOrganaizationDomainSsoDetails_ReadByEmail.sql b/src/Sql/dbo/Stored Procedures/VerfiedOrganaizationDomainSsoDetails_ReadByEmail.sql index a32b42f6c1..2b1a594bfc 100644 --- a/src/Sql/dbo/Stored Procedures/VerfiedOrganaizationDomainSsoDetails_ReadByEmail.sql +++ b/src/Sql/dbo/Stored Procedures/VerfiedOrganaizationDomainSsoDetails_ReadByEmail.sql @@ -15,7 +15,7 @@ BEGIN OD.DomainName FROM [dbo].[OrganizationView] O INNER JOIN [dbo].[OrganizationDomainView] OD ON O.Id = OD.OrganizationId - LEFT JOIN [dbo].[Ssoconfig] S ON O.Id = S.OrganizationId + LEFT JOIN [dbo].[SsoConfig] S ON O.Id = S.OrganizationId WHERE OD.DomainName = @Domain AND O.Enabled = 1 AND OD.VerifiedDate IS NOT NULL From a1b22e66e5a8d0cabd49e59c25b0993f391b2dd8 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Tue, 13 May 2025 07:17:54 +1000 Subject: [PATCH 045/114] [PM-14613] Remove account deprovisioning feature flag (#5676) * Remove flag * Remove old tests * Remove old xmldoc referencing the flag * Remove old emails --- src/Admin/Controllers/UsersController.cs | 13 +- .../OrganizationUsersController.cs | 7 - .../Controllers/OrganizationsController.cs | 3 +- .../Controllers/PoliciesController.cs | 3 +- .../ProfileOrganizationResponseModel.cs | 8 +- .../Auth/Controllers/AccountsController.cs | 5 +- .../Vault/Controllers/CiphersController.cs | 5 +- .../VerifyOrganizationDomainCommand.cs | 8 +- .../RemoveOrganizationUserCommand.cs | 4 +- .../SingleOrgPolicyValidator.cs | 52 +--- .../TwoFactorAuthenticationPolicyValidator.cs | 53 +--- .../OrganizationDomainService.cs | 12 +- .../OrganizationDomainUnverified.html.hbs | 27 -- .../OrganizationDomainUnverified.text.hbs | 10 - ...tionUserRemovedForPolicySingleOrg.html.hbs | 9 - ...tionUserRemovedForPolicySingleOrg.text.hbs | 5 - ...zationUserRemovedForPolicyTwoStep.html.hbs | 15 -- ...zationUserRemovedForPolicyTwoStep.text.hbs | 7 - src/Core/Services/IMailService.cs | 3 - src/Core/Services/IUserService.cs | 8 - .../Implementations/HandlebarsMailService.cs | 41 --- .../Services/Implementations/UserService.cs | 27 +- .../NoopImplementations/NoopMailService.cs | 15 -- .../OrganizationUsersControllerTests.cs | 13 +- .../OrganizationsControllerTests.cs | 3 - .../Controllers/AccountsControllerTests.cs | 22 +- .../VerifyOrganizationDomainCommandTests.cs | 70 +---- .../RemoveOrganizationUserCommandTests.cs | 176 +----------- .../SingleOrgPolicyValidatorTests.cs | 160 ----------- ...actorAuthenticationPolicyValidatorTests.cs | 252 +----------------- test/Core.Test/Services/UserServiceTests.cs | 133 +-------- 31 files changed, 49 insertions(+), 1120 deletions(-) delete mode 100644 src/Core/MailTemplates/Handlebars/OrganizationDomainUnverified.html.hbs delete mode 100644 src/Core/MailTemplates/Handlebars/OrganizationDomainUnverified.text.hbs delete mode 100644 src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicySingleOrg.html.hbs delete mode 100644 src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicySingleOrg.text.hbs delete mode 100644 src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicyTwoStep.html.hbs delete mode 100644 src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicyTwoStep.text.hbs diff --git a/src/Admin/Controllers/UsersController.cs b/src/Admin/Controllers/UsersController.cs index cecd7a2142..b85a91719c 100644 --- a/src/Admin/Controllers/UsersController.cs +++ b/src/Admin/Controllers/UsersController.cs @@ -4,7 +4,6 @@ using Bit.Admin.Enums; using Bit.Admin.Models; using Bit.Admin.Services; using Bit.Admin.Utilities; -using Bit.Core; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -89,7 +88,7 @@ public class UsersController : Controller var ciphers = await _cipherRepository.GetManyByUserIdAsync(id); var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); - var verifiedDomain = await AccountDeprovisioningEnabled(user.Id); + var verifiedDomain = await _userService.IsClaimedByAnyOrganizationAsync(user.Id); return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, verifiedDomain)); } @@ -106,7 +105,7 @@ public class UsersController : Controller var billingInfo = await _paymentService.GetBillingAsync(user); var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user); var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); - var verifiedDomain = await AccountDeprovisioningEnabled(user.Id); + var verifiedDomain = await _userService.IsClaimedByAnyOrganizationAsync(user.Id); var deviceVerificationRequired = await _userService.ActiveNewDeviceVerificationException(user.Id); return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain, deviceVerificationRequired)); @@ -178,12 +177,4 @@ public class UsersController : Controller await _userService.ToggleNewDeviceVerificationException(user.Id); return RedirectToAction("Edit", new { id }); } - - // TODO: Feature flag to be removed in PM-14207 - private async Task AccountDeprovisioningEnabled(Guid userId) - { - return _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - ? await _userService.IsClaimedByAnyOrganizationAsync(userId) - : null; - } } diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs index 536914b56f..6b23edf347 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs @@ -616,7 +616,6 @@ public class OrganizationUsersController : Controller new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage))); } - [RequireFeature(FeatureFlagKeys.AccountDeprovisioning)] [HttpDelete("{id}/delete-account")] [HttpPost("{id}/delete-account")] public async Task DeleteAccount(Guid orgId, Guid id) @@ -635,7 +634,6 @@ public class OrganizationUsersController : Controller await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id); } - [RequireFeature(FeatureFlagKeys.AccountDeprovisioning)] [HttpDelete("delete-account")] [HttpPost("delete-account")] public async Task> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model) @@ -760,11 +758,6 @@ public class OrganizationUsersController : Controller private async Task> GetClaimedByOrganizationStatusAsync(Guid orgId, IEnumerable userIds) { - if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) - { - return userIds.ToDictionary(kvp => kvp, kvp => false); - } - var usersOrganizationClaimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgId, userIds); return usersOrganizationClaimedStatus; } diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index c856c8ab91..f402c927e0 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -279,8 +279,7 @@ public class OrganizationsController : Controller throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving."); } - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - && (await _userService.GetOrganizationsClaimingUserAsync(user.Id)).Any(x => x.Id == id)) + if ((await _userService.GetOrganizationsClaimingUserAsync(user.Id)).Any(x => x.Id == id)) { throw new BadRequestException("Claimed user account cannot leave claiming organization. Contact your organization administrator for additional details."); } diff --git a/src/Api/AdminConsole/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Controllers/PoliciesController.cs index 7de6f6e730..86a1609ee6 100644 --- a/src/Api/AdminConsole/Controllers/PoliciesController.cs +++ b/src/Api/AdminConsole/Controllers/PoliciesController.cs @@ -2,7 +2,6 @@ using Bit.Api.AdminConsole.Models.Response.Helpers; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Response; -using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; @@ -79,7 +78,7 @@ public class PoliciesController : Controller return new PolicyDetailResponseModel(new Policy { Type = (PolicyType)type }); } - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && policy.Type is PolicyType.SingleOrg) + if (policy.Type is PolicyType.SingleOrg) { return await policy.GetSingleOrgPolicyDetailResponseAsync(_organizationHasVerifiedDomainsQuery); } diff --git a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs index 5c900b73e0..259ce3e795 100644 --- a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs @@ -136,7 +136,6 @@ public class ProfileOrganizationResponseModel : ResponseModel public bool AllowAdminAccessToAllCollectionItems { get; set; } /// /// Obsolete. - /// /// See /// [Obsolete("Please use UserIsClaimedByOrganization instead. This property will be removed in a future version.")] @@ -146,15 +145,12 @@ public class ProfileOrganizationResponseModel : ResponseModel set => UserIsClaimedByOrganization = value; } /// - /// Indicates if the organization claims the user. + /// Indicates if the user is claimed by the organization. /// /// - /// An organization claims a user if the user's email domain is verified by the organization and the user is a member of it. + /// A user is claimed by an organization if the user's email domain is verified by the organization and the user is a member. /// The organization must be enabled and able to have verified domains. /// - /// - /// False if the Account Deprovisioning feature flag is disabled. - /// public bool UserIsClaimedByOrganization { get; set; } public bool UseRiskInsights { get; set; } public bool UseAdminSponsoredFamilies { get; set; } diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs index fdd5fbb290..2499b269f5 100644 --- a/src/Api/Auth/Controllers/AccountsController.cs +++ b/src/Api/Auth/Controllers/AccountsController.cs @@ -518,9 +518,8 @@ public class AccountsController : Controller } else { - // If Account Deprovisioning is enabled, we need to check if the user is claimed by any organization. - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - && await _userService.IsClaimedByAnyOrganizationAsync(user.Id)) + // Check if the user is claimed by any organization. + if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id)) { throw new BadRequestException("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details."); } diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 03b83e3de2..02dace894d 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -1086,9 +1086,8 @@ public class CiphersController : Controller throw new BadRequestException(ModelState); } - // If Account Deprovisioning is enabled, we need to check if the user is claimed by any organization. - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - && await _userService.IsClaimedByAnyOrganizationAsync(user.Id)) + // Check if the user is claimed by any organization. + if (await _userService.IsClaimedByAnyOrganizationAsync(user.Id)) { throw new BadRequestException("Cannot purge accounts owned by an organization. Contact your organization administrator for additional details."); } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs index ec635282f7..43a3120ffd 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommand.cs @@ -20,7 +20,6 @@ public class VerifyOrganizationDomainCommand( IDnsResolverService dnsResolverService, IEventService eventService, IGlobalSettings globalSettings, - IFeatureService featureService, ICurrentContext currentContext, ISavePolicyCommand savePolicyCommand, IMailService mailService, @@ -125,11 +124,8 @@ public class VerifyOrganizationDomainCommand( private async Task DomainVerificationSideEffectsAsync(OrganizationDomain domain, IActingUser actingUser) { - if (featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) - { - await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser); - await SendVerifiedDomainUserEmailAsync(domain); - } + await EnableSingleOrganizationPolicyAsync(domain.OrganizationId, actingUser); + await SendVerifiedDomainUserEmailAsync(domain); } private async Task EnableSingleOrganizationPolicyAsync(Guid organizationId, IActingUser actingUser) => diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs index 4de2cd0ea5..00d3ebb533 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommand.cs @@ -159,7 +159,7 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand throw new BadRequestException(RemoveAdminByCustomUserErrorMessage); } - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && deletingUserId.HasValue && eventSystemUser == null) + if (deletingUserId.HasValue && eventSystemUser == null) { var claimedStatus = await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(orgUser.OrganizationId, new[] { orgUser.Id }); if (claimedStatus.TryGetValue(orgUser.Id, out var isClaimed) && isClaimed) @@ -214,7 +214,7 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId); } - var claimedStatus = _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) && deletingUserId.HasValue && eventSystemUser == null + var claimedStatus = deletingUserId.HasValue && eventSystemUser == null ? await _getOrganizationUsersClaimedStatusQuery.GetUsersOrganizationClaimedStatusAsync(organizationId, filteredUsers.Select(u => u.Id)) : filteredUsers.ToDictionary(u => u.Id, u => false); var result = new List<(OrganizationUser OrganizationUser, string ErrorMessage)>(); diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs index a37deef3eb..49467eaae4 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidator.cs @@ -61,16 +61,9 @@ public class SingleOrgPolicyValidator : IPolicyValidator { if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true }) { - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) - { - var currentUser = _currentContext.UserId ?? Guid.Empty; - var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId); - await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider)); - } - else - { - await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId); - } + var currentUser = _currentContext.UserId ?? Guid.Empty; + var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId); + await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider)); } } @@ -116,42 +109,6 @@ public class SingleOrgPolicyValidator : IPolicyValidator _mailService.SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), x.Email))); } - private async Task RemoveNonCompliantUsersAsync(Guid organizationId) - { - // Remove non-compliant users - var savingUserId = _currentContext.UserId; - // Note: must get OrganizationUserUserDetails so that Email is always populated from the User object - var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId); - var org = await _organizationRepository.GetByIdAsync(organizationId); - if (org == null) - { - throw new NotFoundException(OrganizationNotFoundErrorMessage); - } - - var removableOrgUsers = orgUsers.Where(ou => - ou.Status != OrganizationUserStatusType.Invited && - ou.Status != OrganizationUserStatusType.Revoked && - ou.Type != OrganizationUserType.Owner && - ou.Type != OrganizationUserType.Admin && - ou.UserId != savingUserId - ).ToList(); - - var userOrgs = await _organizationUserRepository.GetManyByManyUsersAsync( - removableOrgUsers.Select(ou => ou.UserId!.Value)); - foreach (var orgUser in removableOrgUsers) - { - if (userOrgs.Any(ou => ou.UserId == orgUser.UserId - && ou.OrganizationId != org.Id - && ou.Status != OrganizationUserStatusType.Invited)) - { - await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id, savingUserId); - - await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync( - org.DisplayName(), orgUser.Email); - } - } - } - public async Task ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) { if (policyUpdate is not { Enabled: true }) @@ -165,8 +122,7 @@ public class SingleOrgPolicyValidator : IPolicyValidator return validateDecryptionErrorMessage; } - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - && await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId)) + if (await _organizationHasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policyUpdate.OrganizationId)) { return ClaimedDomainSingleOrganizationRequiredErrorMessage; } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs index c757a65913..13cc935eb9 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidator.cs @@ -23,8 +23,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator private readonly IOrganizationRepository _organizationRepository; private readonly ICurrentContext _currentContext; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; - private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; - private readonly IFeatureService _featureService; private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand; public const string NonCompliantMembersWillLoseAccessMessage = "Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page."; @@ -38,8 +36,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator IOrganizationRepository organizationRepository, ICurrentContext currentContext, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, - IRemoveOrganizationUserCommand removeOrganizationUserCommand, - IFeatureService featureService, IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand) { _organizationUserRepository = organizationUserRepository; @@ -47,8 +43,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator _organizationRepository = organizationRepository; _currentContext = currentContext; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; - _removeOrganizationUserCommand = removeOrganizationUserCommand; - _featureService = featureService; _revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand; } @@ -56,16 +50,9 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator { if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true }) { - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) - { - var currentUser = _currentContext.UserId ?? Guid.Empty; - var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId); - await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider)); - } - else - { - await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId); - } + var currentUser = _currentContext.UserId ?? Guid.Empty; + var isOwnerOrProvider = await _currentContext.OrganizationOwner(policyUpdate.OrganizationId); + await RevokeNonCompliantUsersAsync(policyUpdate.OrganizationId, policyUpdate.PerformedBy ?? new StandardUser(currentUser, isOwnerOrProvider)); } } @@ -121,40 +108,6 @@ public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), x.Email))); } - private async Task RemoveNonCompliantUsersAsync(Guid organizationId) - { - var org = await _organizationRepository.GetByIdAsync(organizationId); - var savingUserId = _currentContext.UserId; - - var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId); - var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers); - var removableOrgUsers = orgUsers.Where(ou => - ou.Status != OrganizationUserStatusType.Invited && ou.Status != OrganizationUserStatusType.Revoked && - ou.Type != OrganizationUserType.Owner && ou.Type != OrganizationUserType.Admin && - ou.UserId != savingUserId); - - // Reorder by HasMasterPassword to prioritize checking users without a master if they have 2FA enabled - foreach (var orgUser in removableOrgUsers.OrderBy(ou => ou.HasMasterPassword)) - { - var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == orgUser.Id) - .twoFactorIsEnabled; - if (!userTwoFactorEnabled) - { - if (!orgUser.HasMasterPassword) - { - throw new BadRequestException( - "Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page."); - } - - await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id, - savingUserId); - - await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync( - org!.DisplayName(), orgUser.Email); - } - } - } - private static bool MembersWithNoMasterPasswordWillLoseAccess( IEnumerable orgUserDetails, IEnumerable<(OrganizationUserUserDetails user, bool isTwoFactorEnabled)> organizationUsersTwoFactorEnabled) => diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs index 9b99cf71f0..854e486b42 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationDomainService.cs @@ -93,16 +93,8 @@ public class OrganizationDomainService : IOrganizationDomainService //Send email to administrators if (adminEmails.Count > 0) { - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) - { - await _mailService.SendUnclaimedOrganizationDomainEmailAsync(adminEmails, - domain.OrganizationId.ToString(), domain.DomainName); - } - else - { - await _mailService.SendUnverifiedOrganizationDomainEmailAsync(adminEmails, - domain.OrganizationId.ToString(), domain.DomainName); - } + await _mailService.SendUnclaimedOrganizationDomainEmailAsync(adminEmails, + domain.OrganizationId.ToString(), domain.DomainName); } _logger.LogInformation(Constants.BypassFiltersEventId, "Expired domain: {domainName}", domain.DomainName); diff --git a/src/Core/MailTemplates/Handlebars/OrganizationDomainUnverified.html.hbs b/src/Core/MailTemplates/Handlebars/OrganizationDomainUnverified.html.hbs deleted file mode 100644 index 11b482acda..0000000000 --- a/src/Core/MailTemplates/Handlebars/OrganizationDomainUnverified.html.hbs +++ /dev/null @@ -1,27 +0,0 @@ -{{#>FullHtmlLayout}} - - - - - - - - - - - - - -
- The domain {{DomainName}} in your Bitwarden organization could not be verified. -
- Check the corresponding record in your domain host. Then reverify this domain in Bitwarden to use it for your organization. -
- The domain will be removed from your organization in 7 days if it is not verified. -
- - Manage Domains - -
-
-{{/FullHtmlLayout}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/OrganizationDomainUnverified.text.hbs b/src/Core/MailTemplates/Handlebars/OrganizationDomainUnverified.text.hbs deleted file mode 100644 index f056bf26c3..0000000000 --- a/src/Core/MailTemplates/Handlebars/OrganizationDomainUnverified.text.hbs +++ /dev/null @@ -1,10 +0,0 @@ -{{#>BasicTextLayout}} -The domain {{DomainName}} in your Bitwarden organization could not be verified. - -Check the corresponding record in your domain host. Then reverify this domain in Bitwarden to use it for your organization. - -The domain will be removed from your organization in 7 days if it is not verified. - -{{Url}} - -{{/BasicTextLayout}} \ No newline at end of file diff --git a/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicySingleOrg.html.hbs b/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicySingleOrg.html.hbs deleted file mode 100644 index bd2e4eb946..0000000000 --- a/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicySingleOrg.html.hbs +++ /dev/null @@ -1,9 +0,0 @@ -{{#>FullHtmlLayout}} - - - - -
- Your user account has been removed from the {{OrganizationName}} organization because you are a part of another organization. The {{OrganizationName}} organization has enabled a policy that prevents users from being a part of multiple organizations. Before you can re-join this organization you need to leave all other organizations or join with a different account. -
-{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicySingleOrg.text.hbs b/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicySingleOrg.text.hbs deleted file mode 100644 index 44ef628a90..0000000000 --- a/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicySingleOrg.text.hbs +++ /dev/null @@ -1,5 +0,0 @@ -{{#>BasicTextLayout}} -Your user account has been removed from the {{OrganizationName}} organization because you are a part of another -organization. The {{OrganizationName}} has enabled a policy that prevents users from being a part of multiple organizations. Before you can re-join this organization you need to leave all other organizations, or join with a -new account. -{{/BasicTextLayout}} diff --git a/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicyTwoStep.html.hbs b/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicyTwoStep.html.hbs deleted file mode 100644 index e82dfcef27..0000000000 --- a/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicyTwoStep.html.hbs +++ /dev/null @@ -1,15 +0,0 @@ -{{#>FullHtmlLayout}} - - - - - - - -
- Your user account has been removed from the {{OrganizationName}} organization because you do not have two-step login configured. Before you can re-join this organization you need to set up two-step login on your user account. -
- Learn how to enable two-step login on your user account at - https://help.bitwarden.com/article/setup-two-step-login/ -
-{{/FullHtmlLayout}} diff --git a/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicyTwoStep.text.hbs b/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicyTwoStep.text.hbs deleted file mode 100644 index a79afb588a..0000000000 --- a/src/Core/MailTemplates/Handlebars/OrganizationUserRemovedForPolicyTwoStep.text.hbs +++ /dev/null @@ -1,7 +0,0 @@ -{{#>BasicTextLayout}} -Your user account has been removed from the {{OrganizationName}} organization because you do not have two-step login -configured. Before you can re-join this organization you need to set up two-step login on your user account. - -Learn how to enable two-step login on your user account at - -{{/BasicTextLayout}} \ No newline at end of file diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 7de75a5143..aa1c0c8c25 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -40,7 +40,6 @@ public interface IMailService Task SendOrganizationAutoscaledEmailAsync(Organization organization, int initialSeatCount, IEnumerable ownerEmails); Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier, IEnumerable adminEmails, bool hasAccessSecretsManager = false); Task SendOrganizationConfirmedEmailAsync(string organizationName, string email, bool hasAccessSecretsManager = false); - Task SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(string organizationName, string email); Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email); Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email); Task SendPasswordlessSignInAsync(string returnUrl, string token, string email); @@ -61,7 +60,6 @@ public interface IMailService Task SendLicenseExpiredAsync(IEnumerable emails, string? organizationName = null); Task SendNewDeviceLoggedInEmail(string email, string deviceType, DateTime timestamp, string ip); Task SendRecoverTwoFactorEmail(string email, DateTime timestamp, string ip); - Task SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(string organizationName, string email); Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token); Task SendEmergencyAccessAcceptedEmailAsync(string granteeEmail, string email); Task SendEmergencyAccessConfirmedEmailAsync(string grantorName, string email); @@ -88,7 +86,6 @@ public interface IMailService Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail); Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, DateTime expirationDate); Task SendOTPEmailAsync(string email, string token); - Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName); Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName); Task SendSecretsManagerMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable ownerEmails); Task SendSecretsManagerMaxServiceAccountLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable ownerEmails); diff --git a/src/Core/Services/IUserService.cs b/src/Core/Services/IUserService.cs index 228b8543d7..e63b4e3b87 100644 --- a/src/Core/Services/IUserService.cs +++ b/src/Core/Services/IUserService.cs @@ -133,16 +133,11 @@ public interface IUserService /// verified domains of that organization, and the user is a member of it. /// The organization must be enabled and able to have verified domains. /// - /// - /// False if the Account Deprovisioning feature flag is disabled. - /// Task IsClaimedByAnyOrganizationAsync(Guid userId); /// /// Verify whether the new email domain meets the requirements for managed users. /// - /// - /// /// /// IdentityResult /// @@ -151,9 +146,6 @@ public interface IUserService /// /// Gets the organizations that manage the user. /// - /// - /// An empty collection if the Account Deprovisioning feature flag is disabled. - /// /// Task> GetOrganizationsClaimingUserAsync(Guid userId); } diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 315e180721..20f6e3a0ab 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -301,20 +301,6 @@ public class HandlebarsMailService : IMailService await EnqueueMailAsync(messageModels); } - public async Task SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(string organizationName, string email) - { - var message = CreateDefaultMessage($"You have been removed from {organizationName}", email); - var model = new OrganizationUserRemovedForPolicyTwoStepViewModel - { - OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false), - WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, - SiteName = _globalSettings.SiteName - }; - await AddMessageContentAsync(message, "OrganizationUserRemovedForPolicyTwoStep", model); - message.Category = "OrganizationUserRemovedForPolicyTwoStep"; - await _mailDeliveryService.SendEmailAsync(message); - } - public async Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email) { var message = CreateDefaultMessage($"You have been revoked from {organizationName}", email); @@ -532,20 +518,6 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } - public async Task SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(string organizationName, string email) - { - var message = CreateDefaultMessage($"You have been removed from {organizationName}", email); - var model = new OrganizationUserRemovedForPolicySingleOrgViewModel - { - OrganizationName = CoreHelpers.SanitizeForEmail(organizationName, false), - WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, - SiteName = _globalSettings.SiteName - }; - await AddMessageContentAsync(message, "OrganizationUserRemovedForPolicySingleOrg", model); - message.Category = "OrganizationUserRemovedForPolicySingleOrg"; - await _mailDeliveryService.SendEmailAsync(message); - } - public async Task SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(string organizationName, string email) { var message = CreateDefaultMessage($"You have been revoked from {organizationName}", email); @@ -1137,19 +1109,6 @@ public class HandlebarsMailService : IMailService await _mailDeliveryService.SendEmailAsync(message); } - public async Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName) - { - var message = CreateDefaultMessage("Domain not verified", adminEmails); - var model = new OrganizationDomainUnverifiedViewModel - { - Url = $"{_globalSettings.BaseServiceUri.VaultWithHash}/organizations/{organizationId}/settings/domain-verification", - DomainName = domainName - }; - await AddMessageContentAsync(message, "OrganizationDomainUnverified", model); - message.Category = "UnverifiedOrganizationDomain"; - await _mailDeliveryService.SendEmailAsync(message); - } - public async Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName) { var message = CreateDefaultMessage("Domain not claimed", adminEmails); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 23617a0fcd..71661493ec 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -1336,11 +1336,6 @@ public class UserService : UserManager, IUserService, IDisposable public async Task> GetOrganizationsClaimingUserAsync(Guid userId) { - if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) - { - return Enumerable.Empty(); - } - // Get all organizations that have verified the user's email domain. var organizationsWithVerifiedUserEmailDomain = await _organizationRepository.GetByVerifiedUserEmailDomainAsync(userId); @@ -1405,22 +1400,12 @@ public class UserService : UserManager, IUserService, IDisposable var removeOrgUserTasks = twoFactorPolicies.Select(async p => { var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId); - if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)) - { - await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync( - new RevokeOrganizationUsersRequest( - p.OrganizationId, - [new OrganizationUserUserDetails { Id = p.OrganizationUserId, OrganizationId = p.OrganizationId }], - new SystemUser(EventSystemUser.TwoFactorDisabled))); - await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email); - } - else - { - await _removeOrganizationUserCommand.RemoveUserAsync(p.OrganizationId, user.Id); - await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync( - organization.DisplayName(), user.Email); - } - + await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync( + new RevokeOrganizationUsersRequest( + p.OrganizationId, + [new OrganizationUserUserDetails { Id = p.OrganizationUserId, OrganizationId = p.OrganizationId }], + new SystemUser(EventSystemUser.TwoFactorDisabled))); + await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email); }).ToArray(); await Task.WhenAll(removeOrgUserTasks); diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index 83bc3ba7cf..26858911a8 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -80,11 +80,6 @@ public class NoopMailService : IMailService return Task.FromResult(0); } - public Task SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(string organizationName, string email) - { - return Task.FromResult(0); - } - public Task SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(string organizationName, string email) => Task.CompletedTask; @@ -155,11 +150,6 @@ public class NoopMailService : IMailService return Task.FromResult(0); } - public Task SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(string organizationName, string email) - { - return Task.FromResult(0); - } - public Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token) { return Task.FromResult(0); @@ -268,11 +258,6 @@ public class NoopMailService : IMailService return Task.FromResult(0); } - public Task SendUnverifiedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName) - { - return Task.FromResult(0); - } - public Task SendUnclaimedOrganizationDomainEmailAsync(IEnumerable adminEmails, string organizationId, string domainName) { return Task.FromResult(0); diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs index 107b9cdfb1..de54a44bca 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationUsersControllerTests.cs @@ -238,20 +238,13 @@ public class OrganizationUsersControllerTests await Assert.ThrowsAsync(() => sutProvider.Sut.Invite(organizationAbility.Id, model)); } - [Theory] - [BitAutoData(true)] - [BitAutoData(false)] + [Theory, BitAutoData] public async Task Get_ReturnsUser( - bool accountDeprovisioningEnabled, OrganizationUserUserDetails organizationUser, ICollection collections, SutProvider sutProvider) { organizationUser.Permissions = null; - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(accountDeprovisioningEnabled); - sutProvider.GetDependency() .ManageUsers(organizationUser.OrganizationId) .Returns(true); @@ -267,8 +260,8 @@ public class OrganizationUsersControllerTests var response = await sutProvider.Sut.Get(organizationUser.Id, false); Assert.Equal(organizationUser.Id, response.Id); - Assert.Equal(accountDeprovisioningEnabled, response.ManagedByOrganization); - Assert.Equal(accountDeprovisioningEnabled, response.ClaimedByOrganization); + Assert.True(response.ManagedByOrganization); + Assert.True(response.ClaimedByOrganization); } [Theory] diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index 867f8f8ec6..7d0a57ea45 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -140,7 +140,6 @@ public class OrganizationsControllerTests : IDisposable _currentContext.OrganizationUser(orgId).Returns(true); _ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig); _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(user); - _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); _userService.GetOrganizationsClaimingUserAsync(user.Id).Returns(new List { null }); var exception = await Assert.ThrowsAsync(() => _sut.Leave(orgId)); @@ -170,7 +169,6 @@ public class OrganizationsControllerTests : IDisposable _currentContext.OrganizationUser(orgId).Returns(true); _ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig); _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(user); - _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); _userService.GetOrganizationsClaimingUserAsync(user.Id).Returns(new List { { foundOrg } }); var exception = await Assert.ThrowsAsync(() => _sut.Leave(orgId)); @@ -205,7 +203,6 @@ public class OrganizationsControllerTests : IDisposable _currentContext.OrganizationUser(orgId).Returns(true); _ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig); _userService.GetUserByPrincipalAsync(Arg.Any()).Returns(user); - _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); _userService.GetOrganizationsClaimingUserAsync(user.Id).Returns(new List()); await _sut.Leave(orgId); diff --git a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs index c617f6c9a9..581a7e8f04 100644 --- a/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs +++ b/test/Api.Test/Auth/Controllers/AccountsControllerTests.cs @@ -7,7 +7,6 @@ using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Api.KeyManagement.Validators; using Bit.Api.Tools.Models.Request; using Bit.Api.Vault.Models.Request; -using Bit.Core; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Entities; @@ -193,21 +192,6 @@ public class AccountsControllerTests : IDisposable await _userService.Received(1).ChangeEmailAsync(user, default, default, default, default, default); } - [Fact] - public async Task PostEmail_WithAccountDeprovisioningEnabled_WhenUserIsNotManagedByAnOrganization_ShouldChangeUserEmail() - { - var user = GenerateExampleUser(); - ConfigureUserServiceToReturnValidPrincipalFor(user); - _userService.ChangeEmailAsync(user, default, default, default, default, default) - .Returns(Task.FromResult(IdentityResult.Success)); - _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); - _userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(false); - - await _sut.PostEmail(new EmailRequestModel()); - - await _userService.Received(1).ChangeEmailAsync(user, default, default, default, default, default); - } - [Fact] public async Task PostEmail_WhenNotAuthorized_ShouldThrownUnauthorizedAccessException() { @@ -537,12 +521,11 @@ public class AccountsControllerTests : IDisposable } [Fact] - public async Task Delete_WhenAccountDeprovisioningIsEnabled_WithUserManagedByAnOrganization_ThrowsBadRequestException() + public async Task Delete_WithUserManagedByAnOrganization_ThrowsBadRequestException() { var user = GenerateExampleUser(); ConfigureUserServiceToReturnValidPrincipalFor(user); ConfigureUserServiceToAcceptPasswordFor(user); - _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); _userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(true); var result = await Assert.ThrowsAsync(() => _sut.Delete(new SecretVerificationRequestModel())); @@ -551,12 +534,11 @@ public class AccountsControllerTests : IDisposable } [Fact] - public async Task Delete_WhenAccountDeprovisioningIsEnabled_WithUserNotManagedByAnOrganization_ShouldSucceed() + public async Task Delete_WithUserNotManagedByAnOrganization_ShouldSucceed() { var user = GenerateExampleUser(); ConfigureUserServiceToReturnValidPrincipalFor(user); ConfigureUserServiceToAcceptPasswordFor(user); - _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); _userService.IsClaimedByAnyOrganizationAsync(user.Id).Returns(false); _userService.DeleteAsync(user).Returns(IdentityResult.Success); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs index daa560f3bc..b0774927e3 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationDomains/VerifyOrganizationDomainCommandTests.cs @@ -166,7 +166,7 @@ public class VerifyOrganizationDomainCommandTests } [Theory, BitAutoData] - public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsVerified_ThenSingleOrgPolicyShouldBeEnabled( + public async Task UserVerifyOrganizationDomainAsync_WhenDomainIsVerified_ThenSingleOrgPolicyShouldBeEnabled( OrganizationDomain domain, Guid userId, SutProvider sutProvider) { sutProvider.GetDependency() @@ -177,10 +177,6 @@ public class VerifyOrganizationDomainCommandTests .ResolveAsync(domain.DomainName, domain.Txt) .Returns(true); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - sutProvider.GetDependency() .UserId.Returns(userId); @@ -196,33 +192,7 @@ public class VerifyOrganizationDomainCommandTests } [Theory, BitAutoData] - public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningDisabled_WhenDomainIsVerified_ThenSingleOrgPolicyShouldBeNotBeEnabled( - OrganizationDomain domain, SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetClaimedDomainsByDomainNameAsync(domain.DomainName) - .Returns([]); - - sutProvider.GetDependency() - .ResolveAsync(domain.DomainName, domain.Txt) - .Returns(true); - - sutProvider.GetDependency() - .UserId.Returns(Guid.NewGuid()); - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(false); - - _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); - - await sutProvider.GetDependency() - .DidNotReceive() - .SaveAsync(Arg.Any()); - } - - [Theory, BitAutoData] - public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsNotVerified_ThenSingleOrgPolicyShouldNotBeEnabled( + public async Task UserVerifyOrganizationDomainAsync_WhenDomainIsNotVerified_ThenSingleOrgPolicyShouldNotBeEnabled( OrganizationDomain domain, SutProvider sutProvider) { sutProvider.GetDependency() @@ -236,10 +206,6 @@ public class VerifyOrganizationDomainCommandTests sutProvider.GetDependency() .UserId.Returns(Guid.NewGuid()); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); await sutProvider.GetDependency() @@ -248,33 +214,7 @@ public class VerifyOrganizationDomainCommandTests } [Theory, BitAutoData] - public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningDisabled_WhenDomainIsNotVerified_ThenSingleOrgPolicyShouldBeNotBeEnabled( - OrganizationDomain domain, SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetClaimedDomainsByDomainNameAsync(domain.DomainName) - .Returns([]); - - sutProvider.GetDependency() - .ResolveAsync(domain.DomainName, domain.Txt) - .Returns(false); - - sutProvider.GetDependency() - .UserId.Returns(Guid.NewGuid()); - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - - _ = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(domain); - - await sutProvider.GetDependency() - .DidNotReceive() - .SaveAsync(Arg.Any()); - } - - [Theory, BitAutoData] - public async Task UserVerifyOrganizationDomainAsync_GivenOrganizationDomainWithAccountDeprovisioningEnabled_WhenDomainIsVerified_ThenEmailShouldBeSentToUsersWhoBelongToTheDomain( + public async Task UserVerifyOrganizationDomainAsync_WhenDomainIsVerified_ThenEmailShouldBeSentToUsersWhoBelongToTheDomain( ICollection organizationUsers, OrganizationDomain domain, Organization organization, @@ -306,10 +246,6 @@ public class VerifyOrganizationDomainCommandTests sutProvider.GetDependency() .UserId.Returns(Guid.NewGuid()); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - sutProvider.GetDependency() .GetManyDetailsByOrganizationAsync(domain.OrganizationId) .Returns(mockedUsers); diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs index 3578706e47..c105c7a9ee 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RemoveOrganizationUserCommandTests.cs @@ -40,43 +40,6 @@ public class RemoveOrganizationUserCommandTests // Act await sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId); - // Assert - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .GetUsersOrganizationClaimedStatusAsync(default, default); - await sutProvider.GetDependency() - .Received(1) - .DeleteAsync(organizationUser); - await sutProvider.GetDependency() - .Received(1) - .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed); - } - - [Theory, BitAutoData] - public async Task RemoveUser_WithDeletingUserId_WithAccountDeprovisioningEnabled_Success( - [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, - [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser deletingUser, - SutProvider sutProvider) - { - // Arrange - organizationUser.OrganizationId = deletingUser.OrganizationId; - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - sutProvider.GetDependency() - .GetByIdAsync(organizationUser.Id) - .Returns(organizationUser); - sutProvider.GetDependency() - .GetByIdAsync(deletingUser.Id) - .Returns(deletingUser); - sutProvider.GetDependency() - .OrganizationOwner(deletingUser.OrganizationId) - .Returns(true); - - // Act - await sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId); - // Assert await sutProvider.GetDependency() .Received(1) @@ -235,15 +198,12 @@ public class RemoveOrganizationUserCommandTests } [Theory, BitAutoData] - public async Task RemoveUserAsync_WithDeletingUserId_WithAccountDeprovisioningEnabled_WhenUserIsManaged_ThrowsException( + public async Task RemoveUserAsync_WithDeletingUserId_WhenUserIsManaged_ThrowsException( [OrganizationUser(status: OrganizationUserStatusType.Confirmed)] OrganizationUser orgUser, Guid deletingUserId, SutProvider sutProvider) { // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); sutProvider.GetDependency() .GetByIdAsync(orgUser.Id) .Returns(orgUser); @@ -285,34 +245,6 @@ public class RemoveOrganizationUserCommandTests .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser); } - [Theory, BitAutoData] - public async Task RemoveUser_WithEventSystemUser_WithAccountDeprovisioningEnabled_Success( - [OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser, - EventSystemUser eventSystemUser, SutProvider sutProvider) - { - // Arrange - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - sutProvider.GetDependency() - .GetByIdAsync(organizationUser.Id) - .Returns(organizationUser); - - // Act - await sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, eventSystemUser); - - // Assert - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .GetUsersOrganizationClaimedStatusAsync(default, default); - await sutProvider.GetDependency() - .Received(1) - .DeleteAsync(organizationUser); - await sutProvider.GetDependency() - .Received(1) - .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser); - } - [Theory] [BitAutoData] public async Task RemoveUser_WithEventSystemUser_NotFound_ThrowsException( @@ -474,7 +406,6 @@ public class RemoveOrganizationUserCommandTests var sutProvider = SutProviderFactory(); var eventDate = sutProvider.GetDependency().GetUtcNow().UtcDateTime; orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId; - var organizationUsers = new[] { orgUser1, orgUser2 }; var organizationUserIds = organizationUsers.Select(u => u.Id); @@ -499,60 +430,6 @@ public class RemoveOrganizationUserCommandTests // Act var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); - // Assert - Assert.Equal(2, result.Count()); - Assert.All(result, r => Assert.Empty(r.ErrorMessage)); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .GetUsersOrganizationClaimedStatusAsync(default, default); - await sutProvider.GetDependency() - .Received(1) - .DeleteManyAsync(Arg.Is>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id))); - await sutProvider.GetDependency() - .Received(1) - .LogOrganizationUserEventsAsync( - Arg.Is>(i => - i.First().OrganizationUser.Id == orgUser1.Id - && i.Last().OrganizationUser.Id == orgUser2.Id - && i.All(u => u.DateTime == eventDate))); - } - - [Theory, BitAutoData] - public async Task RemoveUsers_WithDeletingUserId_WithAccountDeprovisioningEnabled_Success( - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser deletingUser, - [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, OrganizationUser orgUser2) - { - // Arrange - var sutProvider = SutProviderFactory(); - var eventDate = sutProvider.GetDependency().GetUtcNow().UtcDateTime; - orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId; - var organizationUsers = new[] { orgUser1, orgUser2 }; - var organizationUserIds = organizationUsers.Select(u => u.Id); - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - sutProvider.GetDependency() - .GetManyAsync(default) - .ReturnsForAnyArgs(organizationUsers); - sutProvider.GetDependency() - .GetByIdAsync(deletingUser.Id) - .Returns(deletingUser); - sutProvider.GetDependency() - .HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any>()) - .Returns(true); - sutProvider.GetDependency() - .OrganizationOwner(deletingUser.OrganizationId) - .Returns(true); - sutProvider.GetDependency() - .GetUsersOrganizationClaimedStatusAsync( - deletingUser.OrganizationId, - Arg.Is>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id))) - .Returns(new Dictionary { { orgUser1.Id, false }, { orgUser2.Id, false } }); - - // Act - var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId); - // Assert Assert.Equal(2, result.Count()); Assert.All(result, r => Assert.Empty(r.ErrorMessage)); @@ -638,7 +515,7 @@ public class RemoveOrganizationUserCommandTests } [Theory, BitAutoData] - public async Task RemoveUsers_WithDeletingUserId_RemovingClaimedUser_WithAccountDeprovisioningEnabled_ThrowsException( + public async Task RemoveUsers_WithDeletingUserId_RemovingClaimedUser_ThrowsException( [OrganizationUser(status: OrganizationUserStatusType.Confirmed, OrganizationUserType.User)] OrganizationUser orgUser, OrganizationUser deletingUser, SutProvider sutProvider) @@ -646,10 +523,6 @@ public class RemoveOrganizationUserCommandTests // Arrange orgUser.OrganizationId = deletingUser.OrganizationId; - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - sutProvider.GetDependency() .GetManyAsync(Arg.Is>(i => i.Contains(orgUser.Id))) .Returns(new[] { orgUser }); @@ -739,51 +612,6 @@ public class RemoveOrganizationUserCommandTests && u.DateTime == eventDate))); } - [Theory, BitAutoData] - public async Task RemoveUsers_WithEventSystemUser_WithAccountDeprovisioningEnabled_Success( - EventSystemUser eventSystemUser, - [OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, - OrganizationUser orgUser2) - { - // Arrange - var sutProvider = SutProviderFactory(); - var eventDate = sutProvider.GetDependency().GetUtcNow().UtcDateTime; - orgUser1.OrganizationId = orgUser2.OrganizationId; - var organizationUsers = new[] { orgUser1, orgUser2 }; - var organizationUserIds = organizationUsers.Select(u => u.Id); - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - sutProvider.GetDependency() - .GetManyAsync(default) - .ReturnsForAnyArgs(organizationUsers); - sutProvider.GetDependency() - .HasConfirmedOwnersExceptAsync(orgUser1.OrganizationId, Arg.Any>()) - .Returns(true); - - // Act - var result = await sutProvider.Sut.RemoveUsersAsync(orgUser1.OrganizationId, organizationUserIds, eventSystemUser); - - // Assert - Assert.Equal(2, result.Count()); - Assert.All(result, r => Assert.Empty(r.ErrorMessage)); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .GetUsersOrganizationClaimedStatusAsync(default, default); - await sutProvider.GetDependency() - .Received(1) - .DeleteManyAsync(Arg.Is>(i => i.Contains(orgUser1.Id) && i.Contains(orgUser2.Id))); - await sutProvider.GetDependency() - .Received(1) - .LogOrganizationUserEventsAsync( - Arg.Is>( - i => i.First().OrganizationUser.Id == orgUser1.Id - && i.Last().OrganizationUser.Id == orgUser2.Id - && i.All(u => u.EventSystemUser == eventSystemUser - && u.DateTime == eventDate))); - } - [Theory, BitAutoData] public async Task RemoveUsers_WithEventSystemUser_WithMismatchingOrganizationId_ThrowsException( EventSystemUser eventSystemUser, diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs index 6048ed54d5..e982a67e46 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/SingleOrgPolicyValidatorTests.cs @@ -122,9 +122,6 @@ public class SingleOrgPolicyValidatorTests sutProvider.GetDependency().UserId.Returns(savingUserId); sutProvider.GetDependency().GetByIdAsync(policyUpdate.OrganizationId).Returns(organization); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - sutProvider.GetDependency() .RevokeNonCompliantOrganizationUsersAsync(Arg.Any()) .Returns(new CommandResult()); @@ -148,161 +145,4 @@ public class SingleOrgPolicyValidatorTests .Received(1) .SendOrganizationUserRevokedForPolicySingleOrgEmailAsync(organization.DisplayName(), nonCompliantUser.Email); } - - [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_RemovesNonCompliantUsers( - [PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate, - [Policy(PolicyType.SingleOrg, false)] Policy policy, - Guid savingUserId, - Guid nonCompliantUserId, - Organization organization, SutProvider sutProvider) - { - policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; - - var compliantUser1 = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - OrganizationId = organization.Id, - Type = OrganizationUserType.User, - Status = OrganizationUserStatusType.Confirmed, - UserId = new Guid(), - Email = "user1@example.com" - }; - - var compliantUser2 = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - OrganizationId = organization.Id, - Type = OrganizationUserType.User, - Status = OrganizationUserStatusType.Confirmed, - UserId = new Guid(), - Email = "user2@example.com" - }; - - var nonCompliantUser = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - OrganizationId = organization.Id, - Type = OrganizationUserType.User, - Status = OrganizationUserStatusType.Confirmed, - UserId = nonCompliantUserId, - Email = "user3@example.com" - }; - - sutProvider.GetDependency() - .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) - .Returns([compliantUser1, compliantUser2, nonCompliantUser]); - - var otherOrganizationUser = new OrganizationUser - { - Id = Guid.NewGuid(), - OrganizationId = new Guid(), - UserId = nonCompliantUserId, - Status = OrganizationUserStatusType.Confirmed - }; - - sutProvider.GetDependency() - .GetManyByManyUsersAsync(Arg.Is>(ids => ids.Contains(nonCompliantUserId))) - .Returns([otherOrganizationUser]); - - sutProvider.GetDependency().UserId.Returns(savingUserId); - sutProvider.GetDependency().GetByIdAsync(policyUpdate.OrganizationId).Returns(organization); - - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(false); - - sutProvider.GetDependency() - .RevokeNonCompliantOrganizationUsersAsync(Arg.Any()) - .Returns(new CommandResult()); - - await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy); - - await sutProvider.GetDependency() - .DidNotReceive() - .RemoveUserAsync(policyUpdate.OrganizationId, compliantUser1.Id, savingUserId); - await sutProvider.GetDependency() - .DidNotReceive() - .RemoveUserAsync(policyUpdate.OrganizationId, compliantUser2.Id, savingUserId); - await sutProvider.GetDependency() - .Received(1) - .RemoveUserAsync(policyUpdate.OrganizationId, nonCompliantUser.Id, savingUserId); - await sutProvider.GetDependency() - .DidNotReceive() - .SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser1.Email); - await sutProvider.GetDependency() - .DidNotReceive() - .SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(organization.DisplayName(), compliantUser2.Email); - await sutProvider.GetDependency() - .Received(1) - .SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(organization.DisplayName(), nonCompliantUser.Email); - } - - [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_WhenAccountDeprovisioningIsEnabled_ThenUsersAreRevoked( - [PolicyUpdate(PolicyType.SingleOrg)] PolicyUpdate policyUpdate, - [Policy(PolicyType.SingleOrg, false)] Policy policy, - Guid savingUserId, - Guid nonCompliantUserId, - Organization organization, SutProvider sutProvider) - { - policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; - - var compliantUser1 = new OrganizationUserUserDetails - { - OrganizationId = organization.Id, - Type = OrganizationUserType.User, - Status = OrganizationUserStatusType.Confirmed, - UserId = new Guid(), - Email = "user1@example.com" - }; - - var compliantUser2 = new OrganizationUserUserDetails - { - OrganizationId = organization.Id, - Type = OrganizationUserType.User, - Status = OrganizationUserStatusType.Confirmed, - UserId = new Guid(), - Email = "user2@example.com" - }; - - var nonCompliantUser = new OrganizationUserUserDetails - { - OrganizationId = organization.Id, - Type = OrganizationUserType.User, - Status = OrganizationUserStatusType.Confirmed, - UserId = nonCompliantUserId, - Email = "user3@example.com" - }; - - sutProvider.GetDependency() - .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) - .Returns([compliantUser1, compliantUser2, nonCompliantUser]); - - var otherOrganizationUser = new OrganizationUser - { - OrganizationId = new Guid(), - UserId = nonCompliantUserId, - Status = OrganizationUserStatusType.Confirmed - }; - - sutProvider.GetDependency() - .GetManyByManyUsersAsync(Arg.Is>(ids => ids.Contains(nonCompliantUserId))) - .Returns([otherOrganizationUser]); - - sutProvider.GetDependency().UserId.Returns(savingUserId); - sutProvider.GetDependency().GetByIdAsync(policyUpdate.OrganizationId) - .Returns(organization); - - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true); - - sutProvider.GetDependency() - .RevokeNonCompliantOrganizationUsersAsync(Arg.Any()) - .Returns(new CommandResult()); - - await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy); - - await sutProvider.GetDependency() - .Received() - .RevokeNonCompliantOrganizationUsersAsync(Arg.Any()); - } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs index e368f77699..6a97f6bc1e 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyValidators/TwoFactorAuthenticationPolicyValidatorTests.cs @@ -6,7 +6,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models; using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators; using Bit.Core.AdminConsole.Utilities.Commands; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; -using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Data.Organizations.OrganizationUsers; @@ -24,7 +23,7 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyValidat public class TwoFactorAuthenticationPolicyValidatorTests { [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_RemovesNonCompliantUsers( + public async Task OnSaveSideEffectsAsync_GivenNonCompliantUsersWithoutMasterPassword_Throws( Organization organization, [PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate, [Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy, @@ -33,249 +32,6 @@ public class TwoFactorAuthenticationPolicyValidatorTests policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - var orgUserDetailUserInvited = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Invited, - Type = OrganizationUserType.User, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "user1@test.com", - Name = "TEST", - UserId = Guid.NewGuid(), - HasMasterPassword = false - }; - var orgUserDetailUserAcceptedWith2FA = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Accepted, - Type = OrganizationUserType.User, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "user2@test.com", - Name = "TEST", - UserId = Guid.NewGuid(), - HasMasterPassword = true - }; - var orgUserDetailUserAcceptedWithout2FA = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Accepted, - Type = OrganizationUserType.User, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "user3@test.com", - Name = "TEST", - UserId = Guid.NewGuid(), - HasMasterPassword = true - }; - var orgUserDetailAdmin = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Confirmed, - Type = OrganizationUserType.Admin, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "admin@test.com", - Name = "ADMIN", - UserId = Guid.NewGuid(), - HasMasterPassword = false - }; - - sutProvider.GetDependency() - .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) - .Returns(new List - { - orgUserDetailUserInvited, - orgUserDetailUserAcceptedWith2FA, - orgUserDetailUserAcceptedWithout2FA, - orgUserDetailAdmin - }); - - sutProvider.GetDependency() - .TwoFactorIsEnabledAsync(Arg.Any>()) - .Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>() - { - (orgUserDetailUserInvited, false), - (orgUserDetailUserAcceptedWith2FA, true), - (orgUserDetailUserAcceptedWithout2FA, false), - (orgUserDetailAdmin, false), - }); - - var savingUserId = Guid.NewGuid(); - sutProvider.GetDependency().UserId.Returns(savingUserId); - - await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy); - - var removeOrganizationUserCommand = sutProvider.GetDependency(); - - await removeOrganizationUserCommand.Received() - .RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWithout2FA.Id, savingUserId); - await sutProvider.GetDependency().Received() - .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWithout2FA.Email); - - await removeOrganizationUserCommand.DidNotReceive() - .RemoveUserAsync(policy.OrganizationId, orgUserDetailUserInvited.Id, savingUserId); - await sutProvider.GetDependency().DidNotReceive() - .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserInvited.Email); - await removeOrganizationUserCommand.DidNotReceive() - .RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWith2FA.Id, savingUserId); - await sutProvider.GetDependency().DidNotReceive() - .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWith2FA.Email); - await removeOrganizationUserCommand.DidNotReceive() - .RemoveUserAsync(policy.OrganizationId, orgUserDetailAdmin.Id, savingUserId); - await sutProvider.GetDependency().DidNotReceive() - .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailAdmin.Email); - } - - [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_UsersToBeRemovedDontHaveMasterPasswords_Throws( - Organization organization, - [PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate, - [Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy, - SutProvider sutProvider) - { - policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; - - var orgUserDetailUserWith2FAAndMP = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Confirmed, - Type = OrganizationUserType.User, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "user1@test.com", - Name = "TEST", - UserId = Guid.NewGuid(), - HasMasterPassword = true - }; - var orgUserDetailUserWith2FANoMP = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Confirmed, - Type = OrganizationUserType.User, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "user2@test.com", - Name = "TEST", - UserId = Guid.NewGuid(), - HasMasterPassword = false - }; - var orgUserDetailUserWithout2FA = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Confirmed, - Type = OrganizationUserType.User, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "user3@test.com", - Name = "TEST", - UserId = Guid.NewGuid(), - HasMasterPassword = false - }; - var orgUserDetailAdmin = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Confirmed, - Type = OrganizationUserType.Admin, - // Needs to be different from what is passed in as the savingUserId to Sut.SaveAsync - Email = "admin@test.com", - Name = "ADMIN", - UserId = Guid.NewGuid(), - HasMasterPassword = false - }; - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(false); - - sutProvider.GetDependency() - .GetManyDetailsByOrganizationAsync(policy.OrganizationId) - .Returns(new List - { - orgUserDetailUserWith2FAAndMP, - orgUserDetailUserWith2FANoMP, - orgUserDetailUserWithout2FA, - orgUserDetailAdmin - }); - - sutProvider.GetDependency() - .TwoFactorIsEnabledAsync(Arg.Is>(ids => - ids.Contains(orgUserDetailUserWith2FANoMP.UserId.Value) - && ids.Contains(orgUserDetailUserWithout2FA.UserId.Value) - && ids.Contains(orgUserDetailAdmin.UserId.Value))) - .Returns(new List<(Guid userId, bool hasTwoFactor)>() - { - (orgUserDetailUserWith2FANoMP.UserId.Value, true), - (orgUserDetailUserWithout2FA.UserId.Value, false), - (orgUserDetailAdmin.UserId.Value, false), - }); - - var badRequestException = await Assert.ThrowsAsync( - () => sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy)); - - Assert.Equal(TwoFactorAuthenticationPolicyValidator.NonCompliantMembersWillLoseAccessMessage, badRequestException.Message); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .RemoveUserAsync(organizationId: default, organizationUserId: default, deletingUserId: default); - } - - [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_GivenUpdateTo2faPolicy_WhenAccountProvisioningIsDisabled_ThenRevokeUserCommandShouldNotBeCalled( - Organization organization, - [PolicyUpdate(PolicyType.TwoFactorAuthentication)] - PolicyUpdate policyUpdate, - [Policy(PolicyType.TwoFactorAuthentication, false)] - Policy policy, - SutProvider sutProvider) - { - policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; - sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(false); - - var orgUserDetailUserAcceptedWithout2Fa = new OrganizationUserUserDetails - { - Id = Guid.NewGuid(), - Status = OrganizationUserStatusType.Accepted, - Type = OrganizationUserType.User, - Email = "user3@test.com", - Name = "TEST", - UserId = Guid.NewGuid(), - HasMasterPassword = true - }; - - sutProvider.GetDependency() - .GetManyDetailsByOrganizationAsync(policyUpdate.OrganizationId) - .Returns(new List - { - orgUserDetailUserAcceptedWithout2Fa - }); - - - sutProvider.GetDependency() - .TwoFactorIsEnabledAsync(Arg.Any>()) - .Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>() - { - (orgUserDetailUserAcceptedWithout2Fa, false), - }); - - await sutProvider.Sut.OnSaveSideEffectsAsync(policyUpdate, policy); - - await sutProvider.GetDependency() - .DidNotReceive() - .RevokeNonCompliantOrganizationUsersAsync(Arg.Any()); - } - - [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_GivenUpdateTo2faPolicy_WhenAccountProvisioningIsEnabledAndUserDoesNotHaveMasterPassword_ThenNonCompliantMembersErrorMessageWillReturn( - Organization organization, - [PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate, - [Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy, - SutProvider sutProvider) - { - policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; - sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - var orgUserDetailUserWithout2Fa = new OrganizationUserUserDetails { Id = Guid.NewGuid(), @@ -304,7 +60,7 @@ public class TwoFactorAuthenticationPolicyValidatorTests } [Theory, BitAutoData] - public async Task OnSaveSideEffectsAsync_WhenAccountProvisioningIsEnabledAndUserHasMasterPassword_ThenUserWillBeRevoked( + public async Task OnSaveSideEffectsAsync_RevokesNonCompliantUsers( Organization organization, [PolicyUpdate(PolicyType.TwoFactorAuthentication)] PolicyUpdate policyUpdate, [Policy(PolicyType.TwoFactorAuthentication, false)] Policy policy, @@ -313,10 +69,6 @@ public class TwoFactorAuthenticationPolicyValidatorTests policy.OrganizationId = organization.Id = policyUpdate.OrganizationId; sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - var orgUserDetailUserWithout2Fa = new OrganizationUserUserDetails { Id = Guid.NewGuid(), diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index d9bb2beaca..0458c7cdd9 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -343,28 +343,12 @@ public class UserServiceTests } [Theory, BitAutoData] - public async Task IsClaimedByAnyOrganizationAsync_WithAccountDeprovisioningDisabled_ReturnsFalse( - SutProvider sutProvider, Guid userId) - { - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(false); - - var result = await sutProvider.Sut.IsClaimedByAnyOrganizationAsync(userId); - Assert.False(result); - } - - [Theory, BitAutoData] - public async Task IsClaimedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithManagingEnabledOrganization_ReturnsTrue( + public async Task IsClaimedByAnyOrganizationAsync_WithManagingEnabledOrganization_ReturnsTrue( SutProvider sutProvider, Guid userId, Organization organization) { organization.Enabled = true; organization.UseSso = true; - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - sutProvider.GetDependency() .GetByVerifiedUserEmailDomainAsync(userId) .Returns(new[] { organization }); @@ -374,16 +358,12 @@ public class UserServiceTests } [Theory, BitAutoData] - public async Task IsClaimedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithManagingDisabledOrganization_ReturnsFalse( + public async Task IsClaimedByAnyOrganizationAsync_WithManagingDisabledOrganization_ReturnsFalse( SutProvider sutProvider, Guid userId, Organization organization) { organization.Enabled = false; organization.UseSso = true; - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - sutProvider.GetDependency() .GetByVerifiedUserEmailDomainAsync(userId) .Returns(new[] { organization }); @@ -393,16 +373,12 @@ public class UserServiceTests } [Theory, BitAutoData] - public async Task IsClaimedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithOrganizationUseSsoFalse_ReturnsFalse( + public async Task IsClaimedByAnyOrganizationAsync_WithOrganizationUseSsoFalse_ReturnsFalse( SutProvider sutProvider, Guid userId, Organization organization) { organization.Enabled = true; organization.UseSso = false; - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); - sutProvider.GetDependency() .GetByVerifiedUserEmailDomainAsync(userId) .Returns(new[] { organization }); @@ -412,100 +388,7 @@ public class UserServiceTests } [Theory, BitAutoData] - public async Task DisableTwoFactorProviderAsync_WhenOrganizationHas2FAPolicyEnabled_DisablingAllProviders_RemovesUserFromOrganizationAndSendsEmail( - SutProvider sutProvider, User user, Organization organization) - { - // Arrange - user.SetTwoFactorProviders(new Dictionary - { - [TwoFactorProviderType.Email] = new() { Enabled = true } - }); - sutProvider.GetDependency() - .GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication) - .Returns( - [ - new OrganizationUserPolicyDetails - { - OrganizationId = organization.Id, - PolicyType = PolicyType.TwoFactorAuthentication, - PolicyEnabled = true - } - ]); - sutProvider.GetDependency() - .GetByIdAsync(organization.Id) - .Returns(organization); - var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary(), JsonHelpers.LegacyEnumKeyResolver); - - // Act - await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email); - - // Assert - await sutProvider.GetDependency() - .Received(1) - .ReplaceAsync(Arg.Is(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders)); - await sutProvider.GetDependency() - .Received(1) - .LogUserEventAsync(user.Id, EventType.User_Disabled2fa); - await sutProvider.GetDependency() - .Received(1) - .RemoveUserAsync(organization.Id, user.Id); - await sutProvider.GetDependency() - .Received(1) - .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), user.Email); - } - - [Theory, BitAutoData] - public async Task DisableTwoFactorProviderAsync_WhenOrganizationHas2FAPolicyEnabled_UserHasOneProviderEnabled_DoesNotRemoveUserFromOrganization( - SutProvider sutProvider, User user, Organization organization) - { - // Arrange - user.SetTwoFactorProviders(new Dictionary - { - [TwoFactorProviderType.Email] = new() { Enabled = true }, - [TwoFactorProviderType.Remember] = new() { Enabled = true } - }); - sutProvider.GetDependency() - .GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication) - .Returns( - [ - new OrganizationUserPolicyDetails - { - OrganizationId = organization.Id, - PolicyType = PolicyType.TwoFactorAuthentication, - PolicyEnabled = true - } - ]); - sutProvider.GetDependency() - .GetByIdAsync(organization.Id) - .Returns(organization); - sutProvider.GetDependency() - .TwoFactorIsEnabledAsync(user) - .Returns(true); - var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary - { - [TwoFactorProviderType.Remember] = new() { Enabled = true } - }, JsonHelpers.LegacyEnumKeyResolver); - - // Act - await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email); - - // Assert - await sutProvider.GetDependency() - .Received(1) - .ReplaceAsync(Arg.Is(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders)); - await sutProvider.GetDependency() - .Received(1) - .LogUserEventAsync(user.Id, EventType.User_Disabled2fa); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .RemoveUserAsync(default, default); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(default, default); - } - - [Theory, BitAutoData] - public async Task DisableTwoFactorProviderAsync_WithAccountDeprovisioningEnabled_WhenOrganizationHas2FAPolicyEnabled_DisablingAllProviders_RevokesUserAndSendsEmail( + public async Task DisableTwoFactorProviderAsync_WhenOrganizationHas2FAPolicyEnabled_DisablingAllProviders_RevokesUserAndSendsEmail( SutProvider sutProvider, User user, Organization organization1, Guid organizationUserId1, Organization organization2, Guid organizationUserId2) @@ -518,9 +401,6 @@ public class UserServiceTests organization1.Enabled = organization2.Enabled = true; organization1.UseSso = organization2.UseSso = true; - sutProvider.GetDependency() - .IsEnabled(FeatureFlagKeys.AccountDeprovisioning) - .Returns(true); sutProvider.GetDependency() .GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication) .Returns( @@ -583,7 +463,7 @@ public class UserServiceTests } [Theory, BitAutoData] - public async Task DisableTwoFactorProviderAsync_WithAccountDeprovisioningEnabled_UserHasOneProviderEnabled_DoesNotRemoveUserFromOrganization( + public async Task DisableTwoFactorProviderAsync_UserHasOneProviderEnabled_DoesNotRevokeUserFromOrganization( SutProvider sutProvider, User user, Organization organization) { // Arrange @@ -606,6 +486,9 @@ public class UserServiceTests sutProvider.GetDependency() .GetByIdAsync(organization.Id) .Returns(organization); + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(user) + .Returns(true); var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary { [TwoFactorProviderType.Remember] = new() { Enabled = true } From 082bfa3c6a7e6c327e641efbdd1512ccb52eb635 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 13 May 2025 08:51:36 -0400 Subject: [PATCH 046/114] [PM-21257] Revert MaxProjects license changes, limit MaxProjectsQuery to cloud-only for 2-person organizations (#5776) * Revert "Add SmMaxProjects to OrganizationLicense (#5678)" This reverts commit 7fe022e26fce3c3f032757e832df50e9478e6658. * Use PricingClient in MaxProjectsQuery and limit to cloud-only (free 2-person) --- .../Queries/Projects/MaxProjectsQuery.cs | 54 +----- .../Queries/Projects/MaxProjectsQueryTests.cs | 180 +++--------------- src/Core/Billing/Licenses/LicenseConstants.cs | 1 - .../Billing/Licenses/Models/LicenseContext.cs | 1 - .../OrganizationLicenseClaimsFactory.cs | 5 - .../Cloud/CloudGetOrganizationLicenseQuery.cs | 12 +- src/Core/Services/ILicensingService.cs | 3 +- .../Implementations/LicensingService.cs | 3 +- .../NoopLicensingService.cs | 2 +- .../CloudGetOrganizationLicenseQueryTests.cs | 6 +- 10 files changed, 38 insertions(+), 229 deletions(-) diff --git a/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/Projects/MaxProjectsQuery.cs b/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/Projects/MaxProjectsQuery.cs index 394e8aa9bc..7e8857e5d7 100644 --- a/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/Projects/MaxProjectsQuery.cs +++ b/bitwarden_license/src/Commercial.Core/SecretsManager/Queries/Projects/MaxProjectsQuery.cs @@ -1,13 +1,9 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Licenses; -using Bit.Core.Billing.Licenses.Extensions; +using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.SecretsManager.Queries.Projects.Interfaces; using Bit.Core.SecretsManager.Repositories; -using Bit.Core.Services; using Bit.Core.Settings; namespace Bit.Commercial.Core.SecretsManager.Queries.Projects; @@ -17,72 +13,42 @@ public class MaxProjectsQuery : IMaxProjectsQuery private readonly IOrganizationRepository _organizationRepository; private readonly IProjectRepository _projectRepository; private readonly IGlobalSettings _globalSettings; - private readonly ILicensingService _licensingService; private readonly IPricingClient _pricingClient; public MaxProjectsQuery( IOrganizationRepository organizationRepository, IProjectRepository projectRepository, IGlobalSettings globalSettings, - ILicensingService licensingService, IPricingClient pricingClient) { _organizationRepository = organizationRepository; _projectRepository = projectRepository; _globalSettings = globalSettings; - _licensingService = licensingService; _pricingClient = pricingClient; } public async Task<(short? max, bool? overMax)> GetByOrgIdAsync(Guid organizationId, int projectsToAdd) { + // "MaxProjects" only applies to free 2-person organizations, which can't be self-hosted. + if (_globalSettings.SelfHosted) + { + return (null, null); + } + var org = await _organizationRepository.GetByIdAsync(organizationId); if (org == null) { throw new NotFoundException(); } - var (planType, maxProjects) = await GetPlanTypeAndMaxProjectsAsync(org); + var plan = await _pricingClient.GetPlan(org.PlanType); - if (planType != PlanType.Free) + if (plan is not { SecretsManager: not null, Type: PlanType.Free }) { return (null, null); } var projects = await _projectRepository.GetProjectCountByOrganizationIdAsync(organizationId); - return ((short? max, bool? overMax))(projects + projectsToAdd > maxProjects ? (maxProjects, true) : (maxProjects, false)); - } - - private async Task<(PlanType planType, int maxProjects)> GetPlanTypeAndMaxProjectsAsync(Organization organization) - { - if (_globalSettings.SelfHosted) - { - var license = await _licensingService.ReadOrganizationLicenseAsync(organization); - - if (license == null) - { - throw new BadRequestException("License not found."); - } - - var claimsPrincipal = _licensingService.GetClaimsPrincipalFromLicense(license); - var maxProjects = claimsPrincipal.GetValue(OrganizationLicenseConstants.SmMaxProjects); - - if (!maxProjects.HasValue) - { - throw new BadRequestException("License does not contain a value for max Secrets Manager projects"); - } - - var planType = claimsPrincipal.GetValue(OrganizationLicenseConstants.PlanType); - return (planType, maxProjects.Value); - } - - var plan = await _pricingClient.GetPlan(organization.PlanType); - - if (plan is { SupportsSecretsManager: true }) - { - return (plan.Type, plan.SecretsManager.MaxProjects); - } - - throw new BadRequestException("Existing plan not found."); + return ((short? max, bool? overMax))(projects + projectsToAdd > plan.SecretsManager.MaxProjects ? (plan.SecretsManager.MaxProjects, true) : (plan.SecretsManager.MaxProjects, false)); } } diff --git a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/Projects/MaxProjectsQueryTests.cs b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/Projects/MaxProjectsQueryTests.cs index 158463fcfa..16ae8f7f2c 100644 --- a/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/Projects/MaxProjectsQueryTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/SecretsManager/Queries/Projects/MaxProjectsQueryTests.cs @@ -1,14 +1,10 @@ -using System.Security.Claims; -using Bit.Commercial.Core.SecretsManager.Queries.Projects; +using Bit.Commercial.Core.SecretsManager.Queries.Projects; using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Licenses; using Bit.Core.Billing.Pricing; using Bit.Core.Exceptions; -using Bit.Core.Models.Business; using Bit.Core.Repositories; using Bit.Core.SecretsManager.Repositories; -using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; @@ -22,11 +18,26 @@ namespace Bit.Commercial.Core.Test.SecretsManager.Queries.Projects; [SutProviderCustomize] public class MaxProjectsQueryTests { + [Theory] + [BitAutoData] + public async Task GetByOrgIdAsync_SelfHosted_ReturnsNulls(SutProvider sutProvider, + Guid organizationId) + { + sutProvider.GetDependency().SelfHosted.Returns(true); + + var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1); + + Assert.Null(max); + Assert.Null(overMax); + } + [Theory] [BitAutoData] public async Task GetByOrgIdAsync_OrganizationIsNull_ThrowsNotFound(SutProvider sutProvider, Guid organizationId) { + sutProvider.GetDependency().SelfHosted.Returns(false); + sutProvider.GetDependency().GetByIdAsync(default).ReturnsNull(); await Assert.ThrowsAsync(async () => await sutProvider.Sut.GetByOrgIdAsync(organizationId, 1)); @@ -35,54 +46,6 @@ public class MaxProjectsQueryTests .GetProjectCountByOrganizationIdAsync(organizationId); } - [Theory] - [BitAutoData(PlanType.FamiliesAnnually2019)] - [BitAutoData(PlanType.Custom)] - [BitAutoData(PlanType.FamiliesAnnually)] - public async Task GetByOrgIdAsync_Cloud_SmPlanIsNull_ThrowsBadRequest(PlanType planType, - SutProvider sutProvider, Organization organization) - { - organization.PlanType = planType; - sutProvider.GetDependency() - .GetByIdAsync(organization.Id) - .Returns(organization); - - sutProvider.GetDependency().SelfHosted.Returns(false); - var plan = StaticStore.GetPlan(planType); - sutProvider.GetDependency().GetPlan(organization.PlanType).Returns(plan); - - await Assert.ThrowsAsync( - async () => await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1)); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .GetProjectCountByOrganizationIdAsync(organization.Id); - } - - [Theory] - [BitAutoData] - public async Task GetByOrgIdAsync_SelfHosted_NoMaxProjectsClaim_ThrowsBadRequest( - SutProvider sutProvider, Organization organization) - { - sutProvider.GetDependency() - .GetByIdAsync(organization.Id) - .Returns(organization); - - sutProvider.GetDependency().SelfHosted.Returns(true); - - var license = new OrganizationLicense(); - var claimsPrincipal = new ClaimsPrincipal(); - sutProvider.GetDependency().ReadOrganizationLicenseAsync(organization).Returns(license); - sutProvider.GetDependency().GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal); - - await Assert.ThrowsAsync( - async () => await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1)); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .GetProjectCountByOrganizationIdAsync(organization.Id); - } - [Theory] [BitAutoData(PlanType.TeamsMonthly2019)] [BitAutoData(PlanType.TeamsMonthly2020)] @@ -97,57 +60,16 @@ public class MaxProjectsQueryTests [BitAutoData(PlanType.EnterpriseAnnually2019)] [BitAutoData(PlanType.EnterpriseAnnually2020)] [BitAutoData(PlanType.EnterpriseAnnually)] - public async Task GetByOrgIdAsync_Cloud_SmNoneFreePlans_ReturnsNull(PlanType planType, + public async Task GetByOrgIdAsync_SmNoneFreePlans_ReturnsNull(PlanType planType, SutProvider sutProvider, Organization organization) { - organization.PlanType = planType; - sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - sutProvider.GetDependency().SelfHosted.Returns(false); - var plan = StaticStore.GetPlan(planType); - sutProvider.GetDependency().GetPlan(organization.PlanType).Returns(plan); - var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1); - - Assert.Null(limit); - Assert.Null(overLimit); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .GetProjectCountByOrganizationIdAsync(organization.Id); - } - - [Theory] - [BitAutoData(PlanType.TeamsMonthly2019)] - [BitAutoData(PlanType.TeamsMonthly2020)] - [BitAutoData(PlanType.TeamsMonthly)] - [BitAutoData(PlanType.TeamsAnnually2019)] - [BitAutoData(PlanType.TeamsAnnually2020)] - [BitAutoData(PlanType.TeamsAnnually)] - [BitAutoData(PlanType.TeamsStarter)] - [BitAutoData(PlanType.EnterpriseMonthly2019)] - [BitAutoData(PlanType.EnterpriseMonthly2020)] - [BitAutoData(PlanType.EnterpriseMonthly)] - [BitAutoData(PlanType.EnterpriseAnnually2019)] - [BitAutoData(PlanType.EnterpriseAnnually2020)] - [BitAutoData(PlanType.EnterpriseAnnually)] - public async Task GetByOrgIdAsync_SelfHosted_SmNoneFreePlans_ReturnsNull(PlanType planType, - SutProvider sutProvider, Organization organization) - { organization.PlanType = planType; sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - sutProvider.GetDependency().SelfHosted.Returns(true); - var license = new OrganizationLicense(); - var plan = StaticStore.GetPlan(planType); - var claims = new List - { - new (nameof(OrganizationLicenseConstants.PlanType), organization.PlanType.ToString()), - new (nameof(OrganizationLicenseConstants.SmMaxProjects), plan.SecretsManager.MaxProjects.ToString()) - }; - var identity = new ClaimsIdentity(claims, "TestAuthenticationType"); - var claimsPrincipal = new ClaimsPrincipal(identity); - sutProvider.GetDependency().ReadOrganizationLicenseAsync(organization).Returns(license); - sutProvider.GetDependency().GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal); + sutProvider.GetDependency().GetPlan(organization.PlanType) + .Returns(StaticStore.GetPlan(organization.PlanType)); var (limit, overLimit) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, 1); @@ -183,7 +105,7 @@ public class MaxProjectsQueryTests [BitAutoData(PlanType.Free, 3, 4, true)] [BitAutoData(PlanType.Free, 4, 4, true)] [BitAutoData(PlanType.Free, 40, 4, true)] - public async Task GetByOrgIdAsync_Cloud_SmFreePlan__Success(PlanType planType, int projects, int projectsToAdd, bool expectedOverMax, + public async Task GetByOrgIdAsync_SmFreePlan__Success(PlanType planType, int projects, int projectsToAdd, bool expectedOverMax, SutProvider sutProvider, Organization organization) { organization.PlanType = planType; @@ -191,66 +113,8 @@ public class MaxProjectsQueryTests sutProvider.GetDependency().GetProjectCountByOrganizationIdAsync(organization.Id) .Returns(projects); - sutProvider.GetDependency().SelfHosted.Returns(false); - var plan = StaticStore.GetPlan(planType); - sutProvider.GetDependency().GetPlan(organization.PlanType).Returns(plan); - - var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd); - - Assert.NotNull(max); - Assert.NotNull(overMax); - Assert.Equal(3, max.Value); - Assert.Equal(expectedOverMax, overMax); - - await sutProvider.GetDependency().Received(1) - .GetProjectCountByOrganizationIdAsync(organization.Id); - } - - [Theory] - [BitAutoData(PlanType.Free, 0, 1, false)] - [BitAutoData(PlanType.Free, 1, 1, false)] - [BitAutoData(PlanType.Free, 2, 1, false)] - [BitAutoData(PlanType.Free, 3, 1, true)] - [BitAutoData(PlanType.Free, 4, 1, true)] - [BitAutoData(PlanType.Free, 40, 1, true)] - [BitAutoData(PlanType.Free, 0, 2, false)] - [BitAutoData(PlanType.Free, 1, 2, false)] - [BitAutoData(PlanType.Free, 2, 2, true)] - [BitAutoData(PlanType.Free, 3, 2, true)] - [BitAutoData(PlanType.Free, 4, 2, true)] - [BitAutoData(PlanType.Free, 40, 2, true)] - [BitAutoData(PlanType.Free, 0, 3, false)] - [BitAutoData(PlanType.Free, 1, 3, true)] - [BitAutoData(PlanType.Free, 2, 3, true)] - [BitAutoData(PlanType.Free, 3, 3, true)] - [BitAutoData(PlanType.Free, 4, 3, true)] - [BitAutoData(PlanType.Free, 40, 3, true)] - [BitAutoData(PlanType.Free, 0, 4, true)] - [BitAutoData(PlanType.Free, 1, 4, true)] - [BitAutoData(PlanType.Free, 2, 4, true)] - [BitAutoData(PlanType.Free, 3, 4, true)] - [BitAutoData(PlanType.Free, 4, 4, true)] - [BitAutoData(PlanType.Free, 40, 4, true)] - public async Task GetByOrgIdAsync_SelfHosted_SmFreePlan__Success(PlanType planType, int projects, int projectsToAdd, bool expectedOverMax, - SutProvider sutProvider, Organization organization) - { - organization.PlanType = planType; - sutProvider.GetDependency().GetByIdAsync(organization.Id).Returns(organization); - sutProvider.GetDependency().GetProjectCountByOrganizationIdAsync(organization.Id) - .Returns(projects); - sutProvider.GetDependency().SelfHosted.Returns(true); - - var license = new OrganizationLicense(); - var plan = StaticStore.GetPlan(planType); - var claims = new List - { - new (nameof(OrganizationLicenseConstants.PlanType), organization.PlanType.ToString()), - new (nameof(OrganizationLicenseConstants.SmMaxProjects), plan.SecretsManager.MaxProjects.ToString()) - }; - var identity = new ClaimsIdentity(claims, "TestAuthenticationType"); - var claimsPrincipal = new ClaimsPrincipal(identity); - sutProvider.GetDependency().ReadOrganizationLicenseAsync(organization).Returns(license); - sutProvider.GetDependency().GetClaimsPrincipalFromLicense(license).Returns(claimsPrincipal); + sutProvider.GetDependency().GetPlan(organization.PlanType) + .Returns(StaticStore.GetPlan(organization.PlanType)); var (max, overMax) = await sutProvider.Sut.GetByOrgIdAsync(organization.Id, projectsToAdd); diff --git a/src/Core/Billing/Licenses/LicenseConstants.cs b/src/Core/Billing/Licenses/LicenseConstants.cs index 8ef896d6f9..513578f43e 100644 --- a/src/Core/Billing/Licenses/LicenseConstants.cs +++ b/src/Core/Billing/Licenses/LicenseConstants.cs @@ -34,7 +34,6 @@ public static class OrganizationLicenseConstants public const string UseSecretsManager = nameof(UseSecretsManager); public const string SmSeats = nameof(SmSeats); public const string SmServiceAccounts = nameof(SmServiceAccounts); - public const string SmMaxProjects = nameof(SmMaxProjects); public const string LimitCollectionCreationDeletion = nameof(LimitCollectionCreationDeletion); public const string AllowAdminAccessToAllCollectionItems = nameof(AllowAdminAccessToAllCollectionItems); public const string UseRiskInsights = nameof(UseRiskInsights); diff --git a/src/Core/Billing/Licenses/Models/LicenseContext.cs b/src/Core/Billing/Licenses/Models/LicenseContext.cs index 01eb3ac80c..8dcc24e939 100644 --- a/src/Core/Billing/Licenses/Models/LicenseContext.cs +++ b/src/Core/Billing/Licenses/Models/LicenseContext.cs @@ -7,5 +7,4 @@ public class LicenseContext { public Guid? InstallationId { get; init; } public required SubscriptionInfo SubscriptionInfo { get; init; } - public int? SmMaxProjects { get; set; } } diff --git a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs index 7406ac16d9..6819d3cc0b 100644 --- a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs +++ b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs @@ -112,11 +112,6 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory GetLicenseAsync(Organization organization, Guid installationId, @@ -46,11 +42,7 @@ public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuer var subscriptionInfo = await GetSubscriptionAsync(organization); var license = new OrganizationLicense(organization, subscriptionInfo, installationId, _licensingService, version); - var plan = await _pricingClient.GetPlan(organization.PlanType); - int? smMaxProjects = plan?.SupportsSecretsManager ?? false - ? plan.SecretsManager.MaxProjects - : null; - license.Token = await _licensingService.CreateOrganizationTokenAsync(organization, installationId, subscriptionInfo, smMaxProjects); + license.Token = await _licensingService.CreateOrganizationTokenAsync(organization, installationId, subscriptionInfo); return license; } diff --git a/src/Core/Services/ILicensingService.cs b/src/Core/Services/ILicensingService.cs index 9c497ed538..2115e43085 100644 --- a/src/Core/Services/ILicensingService.cs +++ b/src/Core/Services/ILicensingService.cs @@ -21,8 +21,7 @@ public interface ILicensingService Task CreateOrganizationTokenAsync( Organization organization, Guid installationId, - SubscriptionInfo subscriptionInfo, - int? smMaxProjects); + SubscriptionInfo subscriptionInfo); Task CreateUserTokenAsync(User user, SubscriptionInfo subscriptionInfo); } diff --git a/src/Core/Services/Implementations/LicensingService.cs b/src/Core/Services/Implementations/LicensingService.cs index e3509bc964..dd603b4b63 100644 --- a/src/Core/Services/Implementations/LicensingService.cs +++ b/src/Core/Services/Implementations/LicensingService.cs @@ -339,13 +339,12 @@ public class LicensingService : ILicensingService } } - public async Task CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo, int? smMaxProjects) + public async Task CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo) { var licenseContext = new LicenseContext { InstallationId = installationId, SubscriptionInfo = subscriptionInfo, - SmMaxProjects = smMaxProjects }; var claims = await _organizationLicenseClaimsFactory.GenerateClaims(organization, licenseContext); diff --git a/src/Core/Services/NoopImplementations/NoopLicensingService.cs b/src/Core/Services/NoopImplementations/NoopLicensingService.cs index de5e954d44..b181e61138 100644 --- a/src/Core/Services/NoopImplementations/NoopLicensingService.cs +++ b/src/Core/Services/NoopImplementations/NoopLicensingService.cs @@ -62,7 +62,7 @@ public class NoopLicensingService : ILicensingService return null; } - public Task CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo, int? smMaxProjects) + public Task CreateOrganizationTokenAsync(Organization organization, Guid installationId, SubscriptionInfo subscriptionInfo) { return Task.FromResult(null); } diff --git a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs index 7af9044c80..cc8ab956ca 100644 --- a/test/Core.Test/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs +++ b/test/Core.Test/OrganizationFeatures/OrganizationLicenses/CloudGetOrganizationLicenseQueryTests.cs @@ -1,7 +1,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Billing.Pricing; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; @@ -9,7 +8,6 @@ using Bit.Core.OrganizationFeatures.OrganizationLicenses; using Bit.Core.Platform.Installations; using Bit.Core.Services; using Bit.Core.Test.AutoFixture; -using Bit.Core.Utilities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -78,10 +76,8 @@ public class CloudGetOrganizationLicenseQueryTests sutProvider.GetDependency().GetByIdAsync(installationId).Returns(installation); sutProvider.GetDependency().GetSubscriptionAsync(organization).Returns(subInfo); sutProvider.GetDependency().SignLicense(Arg.Any()).Returns(licenseSignature); - var plan = StaticStore.GetPlan(organization.PlanType); - sutProvider.GetDependency().GetPlan(organization.PlanType).Returns(plan); sutProvider.GetDependency() - .CreateOrganizationTokenAsync(organization, installationId, subInfo, plan.SecretsManager.MaxProjects) + .CreateOrganizationTokenAsync(organization, installationId, subInfo) .Returns(token); var result = await sutProvider.Sut.GetLicenseAsync(organization, installationId); From 53f7d9655e84318f42a2b03ac5f5cb490d639d37 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 13 May 2025 09:28:31 -0400 Subject: [PATCH 047/114] [PM-20087] [PM-21104] Preview tax amount for organization trial initiation (#5787) * [NO LOGIC] [PM-21104] Organize Core.Billing tax code * Add PreviewTaxAmountCommand and expose through TaxController * Add PreviewTaxAmountCommandTests * Run dotnet format --- .../RemoveOrganizationFromProviderCommand.cs | 3 +- .../Billing/ProviderBillingService.cs | 4 +- ...oveOrganizationFromProviderCommandTests.cs | 1 + .../Billing/ProviderBillingServiceTests.cs | 1 + .../Billing/TaxServiceTests.cs | 2 +- .../Controllers/AccountsBillingController.cs | 2 +- .../Billing/Controllers/InvoicesController.cs | 2 +- .../OrganizationBillingController.cs | 1 + .../Controllers/ProviderBillingController.cs | 1 + .../Billing/Controllers/StripeController.cs | 2 +- src/Api/Billing/Controllers/TaxController.cs | 36 ++ ...axAmountForOrganizationTrialRequestBody.cs | 27 ++ .../Requests/TaxInformationRequestBody.cs | 2 +- .../Models/Responses/PaymentMethodResponse.cs | 1 + .../Responses/ProviderSubscriptionResponse.cs | 1 + .../Responses/TaxInformationResponse.cs | 2 +- .../Implementations/UpcomingInvoiceHandler.cs | 2 +- .../Extensions/ServiceCollectionExtensions.cs | 5 +- .../Billing/Models/BillingCommandResult.cs | 36 ++ src/Core/Billing/Models/PaymentMethod.cs | 4 +- .../Billing/Models/Sales/CustomerSetup.cs | 4 +- .../Billing/Models/Sales/OrganizationSale.cs | 1 + .../Billing/Models/Sales/PremiumUserSale.cs | 3 +- .../Services/IOrganizationBillingService.cs | 1 + .../Services/IPremiumUserBillingService.cs | 1 + .../Services/IProviderBillingService.cs | 1 + .../Billing/Services/ISubscriberService.cs | 1 + .../OrganizationBillingService.cs | 2 + .../PremiumUserBillingService.cs | 4 +- .../Implementations/SubscriberService.cs | 2 + .../Tax/Commands/PreviewTaxAmountCommand.cs | 147 ++++++++ .../Billing/{ => Tax}/Models/TaxIdType.cs | 2 +- .../{ => Tax}/Models/TaxInformation.cs | 2 +- .../PreviewIndividualInvoiceRequestModel.cs | 2 +- .../PreviewOrganizationInvoiceRequestModel.cs | 2 +- .../Requests/TaxInformationRequestModel.cs | 2 +- .../Responses/PreviewInvoiceResponseModel.cs | 2 +- .../Services/IAutomaticTaxFactory.cs | 2 +- .../Services/IAutomaticTaxStrategy.cs | 2 +- .../Billing/{ => Tax}/Services/ITaxService.cs | 2 +- .../Implementations}/AutomaticTaxFactory.cs | 2 +- .../BusinessUseAutomaticTaxStrategy.cs | 2 +- .../PersonalUseAutomaticTaxStrategy.cs | 2 +- .../Services/Implementations}/TaxService.cs | 4 +- src/Core/Billing/Utilities.cs | 1 + src/Core/Services/IPaymentService.cs | 5 +- .../Implementations/StripePaymentService.cs | 9 +- .../Services/Implementations/UserService.cs | 1 + .../ProviderBillingControllerTests.cs | 2 +- .../Services/SubscriberServiceTests.cs | 5 +- .../Commands/PreviewTaxAmountCommandTests.cs | 346 ++++++++++++++++++ .../Services}/AutomaticTaxFactoryTests.cs | 4 +- .../BusinessUseAutomaticTaxStrategyTests.cs | 4 +- .../Services}/FakeAutomaticTaxStrategy.cs | 4 +- .../PersonalUseAutomaticTaxStrategyTests.cs | 4 +- .../Services/StripePaymentServiceTests.cs | 7 +- 56 files changed, 672 insertions(+), 50 deletions(-) create mode 100644 src/Api/Billing/Controllers/TaxController.cs create mode 100644 src/Api/Billing/Models/Requests/PreviewTaxAmountForOrganizationTrialRequestBody.cs create mode 100644 src/Core/Billing/Models/BillingCommandResult.cs create mode 100644 src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs rename src/Core/Billing/{ => Tax}/Models/TaxIdType.cs (92%) rename src/Core/Billing/{ => Tax}/Models/TaxInformation.cs (93%) rename src/Core/Billing/{Models/Api/Requests/Accounts => Tax/Requests}/PreviewIndividualInvoiceRequestModel.cs (87%) rename src/Core/Billing/{Models/Api/Requests/Organizations => Tax/Requests}/PreviewOrganizationInvoiceRequestModel.cs (93%) rename src/Core/Billing/{Models/Api => Tax}/Requests/TaxInformationRequestModel.cs (84%) rename src/Core/Billing/{Models/Api => Tax}/Responses/PreviewInvoiceResponseModel.cs (74%) rename src/Core/Billing/{ => Tax}/Services/IAutomaticTaxFactory.cs (88%) rename src/Core/Billing/{ => Tax}/Services/IAutomaticTaxStrategy.cs (96%) rename src/Core/Billing/{ => Tax}/Services/ITaxService.cs (94%) rename src/Core/Billing/{Services/Implementations/AutomaticTax => Tax/Services/Implementations}/AutomaticTaxFactory.cs (96%) rename src/Core/Billing/{Services/Implementations/AutomaticTax => Tax/Services/Implementations}/BusinessUseAutomaticTaxStrategy.cs (97%) rename src/Core/Billing/{Services/Implementations/AutomaticTax => Tax/Services/Implementations}/PersonalUseAutomaticTaxStrategy.cs (96%) rename src/Core/Billing/{Services => Tax/Services/Implementations}/TaxService.cs (99%) create mode 100644 test/Core.Test/Billing/Tax/Commands/PreviewTaxAmountCommandTests.cs rename test/Core.Test/Billing/{Services/Implementations => Tax/Services}/AutomaticTaxFactoryTests.cs (96%) rename test/Core.Test/Billing/{Services/Implementations/AutomaticTax => Tax/Services}/BusinessUseAutomaticTaxStrategyTests.cs (99%) rename test/Core.Test/Billing/{Stubs => Tax/Services}/FakeAutomaticTaxStrategy.cs (92%) rename test/Core.Test/Billing/{Services/Implementations/AutomaticTax => Tax/Services}/PersonalUseAutomaticTaxStrategyTests.cs (98%) diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs index 9a62be8dd5..22a2e93642 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs @@ -8,7 +8,8 @@ using Bit.Core.Billing.Constants; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; -using Bit.Core.Billing.Services.Implementations.AutomaticTax; +using Bit.Core.Billing.Tax.Services; +using Bit.Core.Billing.Tax.Services.Implementations; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs index f049d6c8df..99831aa3f1 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs @@ -16,7 +16,9 @@ using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Contracts; -using Bit.Core.Billing.Services.Implementations.AutomaticTax; +using Bit.Core.Billing.Tax.Models; +using Bit.Core.Billing.Tax.Services; +using Bit.Core.Billing.Tax.Services.Implementations; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; diff --git a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs index 48eda094e8..b450bf5d7f 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs @@ -8,6 +8,7 @@ using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; +using Bit.Core.Billing.Tax.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs index 1862692087..2199bc4bfe 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs @@ -17,6 +17,7 @@ using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Contracts; +using Bit.Core.Billing.Tax.Services; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/TaxServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/TaxServiceTests.cs index 3995fb9de6..0a20b34818 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/TaxServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/TaxServiceTests.cs @@ -1,4 +1,4 @@ -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Tax.Services.Implementations; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; diff --git a/src/Api/Billing/Controllers/AccountsBillingController.cs b/src/Api/Billing/Controllers/AccountsBillingController.cs index fcb89226e7..7abcf8c357 100644 --- a/src/Api/Billing/Controllers/AccountsBillingController.cs +++ b/src/Api/Billing/Controllers/AccountsBillingController.cs @@ -1,7 +1,7 @@ #nullable enable using Bit.Api.Billing.Models.Responses; -using Bit.Core.Billing.Models.Api.Requests.Accounts; using Bit.Core.Billing.Services; +using Bit.Core.Billing.Tax.Requests; using Bit.Core.Services; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; diff --git a/src/Api/Billing/Controllers/InvoicesController.cs b/src/Api/Billing/Controllers/InvoicesController.cs index 686d9b9643..5a1d732f42 100644 --- a/src/Api/Billing/Controllers/InvoicesController.cs +++ b/src/Api/Billing/Controllers/InvoicesController.cs @@ -1,5 +1,5 @@ using Bit.Core.AdminConsole.Entities; -using Bit.Core.Billing.Models.Api.Requests.Organizations; +using Bit.Core.Billing.Tax.Requests; using Bit.Core.Context; using Bit.Core.Repositories; using Bit.Core.Services; diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index b82c627ee0..3ebae433d8 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -9,6 +9,7 @@ using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Context; using Bit.Core.Repositories; using Bit.Core.Services; diff --git a/src/Api/Billing/Controllers/ProviderBillingController.cs b/src/Api/Billing/Controllers/ProviderBillingController.cs index bb1fd7bb25..78e361e8b3 100644 --- a/src/Api/Billing/Controllers/ProviderBillingController.cs +++ b/src/Api/Billing/Controllers/ProviderBillingController.cs @@ -6,6 +6,7 @@ using Bit.Core.Billing.Models; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Context; using Bit.Core.Models.BitStripe; using Bit.Core.Services; diff --git a/src/Api/Billing/Controllers/StripeController.cs b/src/Api/Billing/Controllers/StripeController.cs index f5e8253bfa..15fccd16f4 100644 --- a/src/Api/Billing/Controllers/StripeController.cs +++ b/src/Api/Billing/Controllers/StripeController.cs @@ -1,4 +1,4 @@ -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Tax.Services; using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.HttpResults; diff --git a/src/Api/Billing/Controllers/TaxController.cs b/src/Api/Billing/Controllers/TaxController.cs new file mode 100644 index 0000000000..7b8b9d960f --- /dev/null +++ b/src/Api/Billing/Controllers/TaxController.cs @@ -0,0 +1,36 @@ +using Bit.Api.Billing.Models.Requests; +using Bit.Core.Billing.Tax.Commands; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.Billing.Controllers; + +[Authorize("Application")] +[Route("tax")] +public class TaxController( + IPreviewTaxAmountCommand previewTaxAmountCommand) : BaseBillingController +{ + [HttpPost("preview-amount/organization-trial")] + public async Task PreviewTaxAmountForOrganizationTrialAsync( + [FromBody] PreviewTaxAmountForOrganizationTrialRequestBody requestBody) + { + var parameters = new OrganizationTrialParameters + { + PlanType = requestBody.PlanType, + ProductType = requestBody.ProductType, + TaxInformation = new OrganizationTrialParameters.TaxInformationDTO + { + Country = requestBody.TaxInformation.Country, + PostalCode = requestBody.TaxInformation.PostalCode, + TaxId = requestBody.TaxInformation.TaxId + } + }; + + var result = await previewTaxAmountCommand.Run(parameters); + + return result.Match( + taxAmount => TypedResults.Ok(new { TaxAmount = taxAmount }), + badRequest => Error.BadRequest(badRequest.TranslationKey), + unhandled => Error.ServerError(unhandled.TranslationKey)); + } +} diff --git a/src/Api/Billing/Models/Requests/PreviewTaxAmountForOrganizationTrialRequestBody.cs b/src/Api/Billing/Models/Requests/PreviewTaxAmountForOrganizationTrialRequestBody.cs new file mode 100644 index 0000000000..a3fda0fd6c --- /dev/null +++ b/src/Api/Billing/Models/Requests/PreviewTaxAmountForOrganizationTrialRequestBody.cs @@ -0,0 +1,27 @@ +#nullable enable +using System.ComponentModel.DataAnnotations; +using Bit.Core.Billing.Enums; + +namespace Bit.Api.Billing.Models.Requests; + +public class PreviewTaxAmountForOrganizationTrialRequestBody +{ + [Required] + public PlanType PlanType { get; set; } + + [Required] + public ProductType ProductType { get; set; } + + [Required] public TaxInformationDTO TaxInformation { get; set; } = null!; + + public class TaxInformationDTO + { + [Required] + public string Country { get; set; } = null!; + + [Required] + public string PostalCode { get; set; } = null!; + + public string? TaxId { get; set; } + } +} diff --git a/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs b/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs index 32ba2effb2..edc45ce483 100644 --- a/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs +++ b/src/Api/Billing/Models/Requests/TaxInformationRequestBody.cs @@ -1,5 +1,5 @@ using System.ComponentModel.DataAnnotations; -using Bit.Core.Billing.Models; +using Bit.Core.Billing.Tax.Models; namespace Bit.Api.Billing.Models.Requests; diff --git a/src/Api/Billing/Models/Responses/PaymentMethodResponse.cs b/src/Api/Billing/Models/Responses/PaymentMethodResponse.cs index b89c1e9db9..fd248a0a00 100644 --- a/src/Api/Billing/Models/Responses/PaymentMethodResponse.cs +++ b/src/Api/Billing/Models/Responses/PaymentMethodResponse.cs @@ -1,4 +1,5 @@ using Bit.Core.Billing.Models; +using Bit.Core.Billing.Tax.Models; namespace Bit.Api.Billing.Models.Responses; diff --git a/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs b/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs index ea1479c9df..a2c6827314 100644 --- a/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs +++ b/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs @@ -2,6 +2,7 @@ using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models; +using Bit.Core.Billing.Tax.Models; using Stripe; namespace Bit.Api.Billing.Models.Responses; diff --git a/src/Api/Billing/Models/Responses/TaxInformationResponse.cs b/src/Api/Billing/Models/Responses/TaxInformationResponse.cs index 02349d74f7..59e4934751 100644 --- a/src/Api/Billing/Models/Responses/TaxInformationResponse.cs +++ b/src/Api/Billing/Models/Responses/TaxInformationResponse.cs @@ -1,4 +1,4 @@ -using Bit.Core.Billing.Models; +using Bit.Core.Billing.Tax.Models; namespace Bit.Api.Billing.Models.Responses; diff --git a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs index f75cbf8a8b..4c27098f38 100644 --- a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs +++ b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs @@ -4,8 +4,8 @@ using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Contracts; +using Bit.Core.Billing.Tax.Services; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; diff --git a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs index 17285e0676..5c7a42e9b8 100644 --- a/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs +++ b/src/Core/Billing/Extensions/ServiceCollectionExtensions.cs @@ -4,7 +4,9 @@ using Bit.Core.Billing.Licenses.Extensions; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Implementations; -using Bit.Core.Billing.Services.Implementations.AutomaticTax; +using Bit.Core.Billing.Tax.Commands; +using Bit.Core.Billing.Tax.Services; +using Bit.Core.Billing.Tax.Services.Implementations; namespace Bit.Core.Billing.Extensions; @@ -24,5 +26,6 @@ public static class ServiceCollectionExtensions services.AddTransient(); services.AddLicenseServices(); services.AddPricingClient(); + services.AddTransient(); } } diff --git a/src/Core/Billing/Models/BillingCommandResult.cs b/src/Core/Billing/Models/BillingCommandResult.cs new file mode 100644 index 0000000000..1b8eefe8df --- /dev/null +++ b/src/Core/Billing/Models/BillingCommandResult.cs @@ -0,0 +1,36 @@ +using OneOf; + +namespace Bit.Core.Billing.Models; + +public record BadRequest(string TranslationKey) +{ + public static BadRequest TaxIdNumberInvalid => new(BillingErrorTranslationKeys.TaxIdInvalid); + public static BadRequest TaxLocationInvalid => new(BillingErrorTranslationKeys.CustomerTaxLocationInvalid); + public static BadRequest UnknownTaxIdType => new(BillingErrorTranslationKeys.UnknownTaxIdType); +} + +public record Unhandled(string TranslationKey = BillingErrorTranslationKeys.UnhandledError); + +public class BillingCommandResult : OneOfBase +{ + private BillingCommandResult(OneOf input) : base(input) { } + + public static implicit operator BillingCommandResult(T output) => new(output); + public static implicit operator BillingCommandResult(BadRequest badRequest) => new(badRequest); + public static implicit operator BillingCommandResult(Unhandled unhandled) => new(unhandled); +} + +public static class BillingErrorTranslationKeys +{ + // "The tax ID number you provided was invalid. Please try again or contact support." + public const string TaxIdInvalid = "taxIdInvalid"; + + // "Your location wasn't recognized. Please ensure your country and postal code are valid and try again." + public const string CustomerTaxLocationInvalid = "customerTaxLocationInvalid"; + + // "Something went wrong with your request. Please contact support." + public const string UnhandledError = "unhandledBillingError"; + + // "We couldn't find a corresponding tax ID type for the tax ID you provided. Please try again or contact support." + public const string UnknownTaxIdType = "unknownTaxIdType"; +} diff --git a/src/Core/Billing/Models/PaymentMethod.cs b/src/Core/Billing/Models/PaymentMethod.cs index b07fe82e46..2b8c59fa05 100644 --- a/src/Core/Billing/Models/PaymentMethod.cs +++ b/src/Core/Billing/Models/PaymentMethod.cs @@ -1,4 +1,6 @@ -namespace Bit.Core.Billing.Models; +using Bit.Core.Billing.Tax.Models; + +namespace Bit.Core.Billing.Models; public record PaymentMethod( long AccountCredit, diff --git a/src/Core/Billing/Models/Sales/CustomerSetup.cs b/src/Core/Billing/Models/Sales/CustomerSetup.cs index bb4f2352e3..aa67c712b5 100644 --- a/src/Core/Billing/Models/Sales/CustomerSetup.cs +++ b/src/Core/Billing/Models/Sales/CustomerSetup.cs @@ -1,4 +1,6 @@ -namespace Bit.Core.Billing.Models.Sales; +using Bit.Core.Billing.Tax.Models; + +namespace Bit.Core.Billing.Models.Sales; #nullable enable diff --git a/src/Core/Billing/Models/Sales/OrganizationSale.cs b/src/Core/Billing/Models/Sales/OrganizationSale.cs index 0602cf1dd9..87d17551eb 100644 --- a/src/Core/Billing/Models/Sales/OrganizationSale.cs +++ b/src/Core/Billing/Models/Sales/OrganizationSale.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Models.Business; namespace Bit.Core.Billing.Models.Sales; diff --git a/src/Core/Billing/Models/Sales/PremiumUserSale.cs b/src/Core/Billing/Models/Sales/PremiumUserSale.cs index 6bc054eac5..8c9b696aa3 100644 --- a/src/Core/Billing/Models/Sales/PremiumUserSale.cs +++ b/src/Core/Billing/Models/Sales/PremiumUserSale.cs @@ -1,4 +1,5 @@ -using Bit.Core.Entities; +using Bit.Core.Billing.Tax.Models; +using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; diff --git a/src/Core/Billing/Services/IOrganizationBillingService.cs b/src/Core/Billing/Services/IOrganizationBillingService.cs index db62d545e3..5f7d33f118 100644 --- a/src/Core/Billing/Services/IOrganizationBillingService.cs +++ b/src/Core/Billing/Services/IOrganizationBillingService.cs @@ -1,6 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; +using Bit.Core.Billing.Tax.Models; namespace Bit.Core.Billing.Services; diff --git a/src/Core/Billing/Services/IPremiumUserBillingService.cs b/src/Core/Billing/Services/IPremiumUserBillingService.cs index b3bb580e2d..ed7a003599 100644 --- a/src/Core/Billing/Services/IPremiumUserBillingService.cs +++ b/src/Core/Billing/Services/IPremiumUserBillingService.cs @@ -1,5 +1,6 @@ using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Entities; namespace Bit.Core.Billing.Services; diff --git a/src/Core/Billing/Services/IProviderBillingService.cs b/src/Core/Billing/Services/IProviderBillingService.cs index 0171a7e1c3..b6ddbdd642 100644 --- a/src/Core/Billing/Services/IProviderBillingService.cs +++ b/src/Core/Billing/Services/IProviderBillingService.cs @@ -4,6 +4,7 @@ using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models; using Bit.Core.Billing.Services.Contracts; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Models.Business; using Stripe; diff --git a/src/Core/Billing/Services/ISubscriberService.cs b/src/Core/Billing/Services/ISubscriberService.cs index bb0a23020c..6910948436 100644 --- a/src/Core/Billing/Services/ISubscriberService.cs +++ b/src/Core/Billing/Services/ISubscriberService.cs @@ -1,4 +1,5 @@ using Bit.Core.Billing.Models; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Entities; using Bit.Core.Enums; using Stripe; diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs index 2e902ca028..20f6105c2a 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs @@ -6,6 +6,8 @@ using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services.Contracts; +using Bit.Core.Billing.Tax.Models; +using Bit.Core.Billing.Tax.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; diff --git a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs index cbd4dbbdff..1b845e93f1 100644 --- a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs +++ b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs @@ -2,7 +2,9 @@ using Bit.Core.Billing.Constants; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; -using Bit.Core.Billing.Services.Implementations.AutomaticTax; +using Bit.Core.Billing.Tax.Models; +using Bit.Core.Billing.Tax.Services; +using Bit.Core.Billing.Tax.Services.Implementations; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs index 1b0e5b665b..10247cdf92 100644 --- a/src/Core/Billing/Services/Implementations/SubscriberService.cs +++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs @@ -2,6 +2,8 @@ using Bit.Core.Billing.Constants; using Bit.Core.Billing.Models; using Bit.Core.Billing.Services.Contracts; +using Bit.Core.Billing.Tax.Models; +using Bit.Core.Billing.Tax.Services; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; diff --git a/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs b/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs new file mode 100644 index 0000000000..304abbaae0 --- /dev/null +++ b/src/Core/Billing/Tax/Commands/PreviewTaxAmountCommand.cs @@ -0,0 +1,147 @@ +#nullable enable +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Models; +using Bit.Core.Billing.Pricing; +using Bit.Core.Billing.Tax.Services; +using Bit.Core.Services; +using Microsoft.Extensions.Logging; +using Stripe; + +namespace Bit.Core.Billing.Tax.Commands; + +public interface IPreviewTaxAmountCommand +{ + Task> Run(OrganizationTrialParameters parameters); +} + +public class PreviewTaxAmountCommand( + ILogger logger, + IPricingClient pricingClient, + IStripeAdapter stripeAdapter, + ITaxService taxService) : IPreviewTaxAmountCommand +{ + public async Task> Run(OrganizationTrialParameters parameters) + { + var (planType, productType, taxInformation) = parameters; + + var plan = await pricingClient.GetPlanOrThrow(planType); + + var options = new InvoiceCreatePreviewOptions + { + Currency = "usd", + CustomerDetails = new InvoiceCustomerDetailsOptions + { + Address = new AddressOptions + { + Country = taxInformation.Country, + PostalCode = taxInformation.PostalCode + } + }, + SubscriptionDetails = new InvoiceSubscriptionDetailsOptions + { + Items = [ + new InvoiceSubscriptionDetailsItemOptions + { + Price = plan.HasNonSeatBasedPasswordManagerPlan() ? plan.PasswordManager.StripePlanId : plan.PasswordManager.StripeSeatPlanId, + Quantity = 1 + } + ] + } + }; + + if (productType == ProductType.SecretsManager) + { + options.SubscriptionDetails.Items.Add(new InvoiceSubscriptionDetailsItemOptions + { + Price = plan.SecretsManager.StripeSeatPlanId, + Quantity = 1 + }); + + options.Coupon = StripeConstants.CouponIDs.SecretsManagerStandalone; + } + + if (!string.IsNullOrEmpty(taxInformation.TaxId)) + { + var taxIdType = taxService.GetStripeTaxCode( + taxInformation.Country, + taxInformation.TaxId); + + if (string.IsNullOrEmpty(taxIdType)) + { + return BadRequest.UnknownTaxIdType; + } + + options.CustomerDetails.TaxIds = [ + new InvoiceCustomerDetailsTaxIdOptions + { + Type = taxIdType, + Value = taxInformation.TaxId + } + ]; + } + + if (planType.GetProductTier() == ProductTierType.Families) + { + options.AutomaticTax = new InvoiceAutomaticTaxOptions { Enabled = true }; + } + else + { + options.AutomaticTax = new InvoiceAutomaticTaxOptions + { + Enabled = options.CustomerDetails.Address.Country == "US" || + options.CustomerDetails.TaxIds is [_, ..] + }; + } + + try + { + var invoice = await stripeAdapter.InvoiceCreatePreviewAsync(options); + return Convert.ToDecimal(invoice.Tax) / 100; + } + catch (StripeException stripeException) when (stripeException.StripeError.Code == + StripeConstants.ErrorCodes.CustomerTaxLocationInvalid) + { + return BadRequest.TaxLocationInvalid; + } + catch (StripeException stripeException) when (stripeException.StripeError.Code == + StripeConstants.ErrorCodes.TaxIdInvalid) + { + return BadRequest.TaxIdNumberInvalid; + } + catch (StripeException stripeException) + { + logger.LogError(stripeException, "Stripe responded with an error during {Operation}. Code: {Code}", nameof(PreviewTaxAmountCommand), stripeException.StripeError.Code); + return new Unhandled(); + } + } +} + +#region Command Parameters + +public record OrganizationTrialParameters +{ + public required PlanType PlanType { get; set; } + public required ProductType ProductType { get; set; } + public required TaxInformationDTO TaxInformation { get; set; } + + public void Deconstruct( + out PlanType planType, + out ProductType productType, + out TaxInformationDTO taxInformation) + { + planType = PlanType; + productType = ProductType; + taxInformation = TaxInformation; + } + + public record TaxInformationDTO + { + public required string Country { get; set; } + public required string PostalCode { get; set; } + public string? TaxId { get; set; } + } +} + +#endregion diff --git a/src/Core/Billing/Models/TaxIdType.cs b/src/Core/Billing/Tax/Models/TaxIdType.cs similarity index 92% rename from src/Core/Billing/Models/TaxIdType.cs rename to src/Core/Billing/Tax/Models/TaxIdType.cs index 3fc246d68b..6f8cfdde99 100644 --- a/src/Core/Billing/Models/TaxIdType.cs +++ b/src/Core/Billing/Tax/Models/TaxIdType.cs @@ -1,6 +1,6 @@ using System.Text.RegularExpressions; -namespace Bit.Core.Billing.Models; +namespace Bit.Core.Billing.Tax.Models; public class TaxIdType { diff --git a/src/Core/Billing/Models/TaxInformation.cs b/src/Core/Billing/Tax/Models/TaxInformation.cs similarity index 93% rename from src/Core/Billing/Models/TaxInformation.cs rename to src/Core/Billing/Tax/Models/TaxInformation.cs index 23ed3e5faa..2408ee0ecd 100644 --- a/src/Core/Billing/Models/TaxInformation.cs +++ b/src/Core/Billing/Tax/Models/TaxInformation.cs @@ -1,6 +1,6 @@ using Bit.Core.Models.Business; -namespace Bit.Core.Billing.Models; +namespace Bit.Core.Billing.Tax.Models; public record TaxInformation( string Country, diff --git a/src/Core/Billing/Models/Api/Requests/Accounts/PreviewIndividualInvoiceRequestModel.cs b/src/Core/Billing/Tax/Requests/PreviewIndividualInvoiceRequestModel.cs similarity index 87% rename from src/Core/Billing/Models/Api/Requests/Accounts/PreviewIndividualInvoiceRequestModel.cs rename to src/Core/Billing/Tax/Requests/PreviewIndividualInvoiceRequestModel.cs index 8597cea09b..340f07b56c 100644 --- a/src/Core/Billing/Models/Api/Requests/Accounts/PreviewIndividualInvoiceRequestModel.cs +++ b/src/Core/Billing/Tax/Requests/PreviewIndividualInvoiceRequestModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Bit.Core.Billing.Models.Api.Requests.Accounts; +namespace Bit.Core.Billing.Tax.Requests; public class PreviewIndividualInvoiceRequestBody { diff --git a/src/Core/Billing/Models/Api/Requests/Organizations/PreviewOrganizationInvoiceRequestModel.cs b/src/Core/Billing/Tax/Requests/PreviewOrganizationInvoiceRequestModel.cs similarity index 93% rename from src/Core/Billing/Models/Api/Requests/Organizations/PreviewOrganizationInvoiceRequestModel.cs rename to src/Core/Billing/Tax/Requests/PreviewOrganizationInvoiceRequestModel.cs index 461a6dca65..bfb47e7b2c 100644 --- a/src/Core/Billing/Models/Api/Requests/Organizations/PreviewOrganizationInvoiceRequestModel.cs +++ b/src/Core/Billing/Tax/Requests/PreviewOrganizationInvoiceRequestModel.cs @@ -2,7 +2,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Enums; -namespace Bit.Core.Billing.Models.Api.Requests.Organizations; +namespace Bit.Core.Billing.Tax.Requests; public class PreviewOrganizationInvoiceRequestBody { diff --git a/src/Core/Billing/Models/Api/Requests/TaxInformationRequestModel.cs b/src/Core/Billing/Tax/Requests/TaxInformationRequestModel.cs similarity index 84% rename from src/Core/Billing/Models/Api/Requests/TaxInformationRequestModel.cs rename to src/Core/Billing/Tax/Requests/TaxInformationRequestModel.cs index 9cb43645c6..13d4870ac5 100644 --- a/src/Core/Billing/Models/Api/Requests/TaxInformationRequestModel.cs +++ b/src/Core/Billing/Tax/Requests/TaxInformationRequestModel.cs @@ -1,6 +1,6 @@ using System.ComponentModel.DataAnnotations; -namespace Bit.Core.Billing.Models.Api.Requests; +namespace Bit.Core.Billing.Tax.Requests; public class TaxInformationRequestModel { diff --git a/src/Core/Billing/Models/Api/Responses/PreviewInvoiceResponseModel.cs b/src/Core/Billing/Tax/Responses/PreviewInvoiceResponseModel.cs similarity index 74% rename from src/Core/Billing/Models/Api/Responses/PreviewInvoiceResponseModel.cs rename to src/Core/Billing/Tax/Responses/PreviewInvoiceResponseModel.cs index fdde7dae1e..2753487e2f 100644 --- a/src/Core/Billing/Models/Api/Responses/PreviewInvoiceResponseModel.cs +++ b/src/Core/Billing/Tax/Responses/PreviewInvoiceResponseModel.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Billing.Models.Api.Responses; +namespace Bit.Core.Billing.Tax.Responses; public record PreviewInvoiceResponseModel( decimal EffectiveTaxRate, diff --git a/src/Core/Billing/Services/IAutomaticTaxFactory.cs b/src/Core/Billing/Tax/Services/IAutomaticTaxFactory.cs similarity index 88% rename from src/Core/Billing/Services/IAutomaticTaxFactory.cs rename to src/Core/Billing/Tax/Services/IAutomaticTaxFactory.cs index c52a8f2671..90a3bc08ad 100644 --- a/src/Core/Billing/Services/IAutomaticTaxFactory.cs +++ b/src/Core/Billing/Tax/Services/IAutomaticTaxFactory.cs @@ -1,6 +1,6 @@ using Bit.Core.Billing.Services.Contracts; -namespace Bit.Core.Billing.Services; +namespace Bit.Core.Billing.Tax.Services; /// /// Responsible for defining the correct automatic tax strategy for either personal use of business use. diff --git a/src/Core/Billing/Services/IAutomaticTaxStrategy.cs b/src/Core/Billing/Tax/Services/IAutomaticTaxStrategy.cs similarity index 96% rename from src/Core/Billing/Services/IAutomaticTaxStrategy.cs rename to src/Core/Billing/Tax/Services/IAutomaticTaxStrategy.cs index 292f2d0939..557bb1d30c 100644 --- a/src/Core/Billing/Services/IAutomaticTaxStrategy.cs +++ b/src/Core/Billing/Tax/Services/IAutomaticTaxStrategy.cs @@ -1,7 +1,7 @@ #nullable enable using Stripe; -namespace Bit.Core.Billing.Services; +namespace Bit.Core.Billing.Tax.Services; public interface IAutomaticTaxStrategy { diff --git a/src/Core/Billing/Services/ITaxService.cs b/src/Core/Billing/Tax/Services/ITaxService.cs similarity index 94% rename from src/Core/Billing/Services/ITaxService.cs rename to src/Core/Billing/Tax/Services/ITaxService.cs index beee113d17..00cbf56a9b 100644 --- a/src/Core/Billing/Services/ITaxService.cs +++ b/src/Core/Billing/Tax/Services/ITaxService.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Billing.Services; +namespace Bit.Core.Billing.Tax.Services; public interface ITaxService { diff --git a/src/Core/Billing/Services/Implementations/AutomaticTax/AutomaticTaxFactory.cs b/src/Core/Billing/Tax/Services/Implementations/AutomaticTaxFactory.cs similarity index 96% rename from src/Core/Billing/Services/Implementations/AutomaticTax/AutomaticTaxFactory.cs rename to src/Core/Billing/Tax/Services/Implementations/AutomaticTaxFactory.cs index 133cd2c7a7..fa110f79d5 100644 --- a/src/Core/Billing/Services/Implementations/AutomaticTax/AutomaticTaxFactory.cs +++ b/src/Core/Billing/Tax/Services/Implementations/AutomaticTaxFactory.cs @@ -5,7 +5,7 @@ using Bit.Core.Billing.Services.Contracts; using Bit.Core.Entities; using Bit.Core.Services; -namespace Bit.Core.Billing.Services.Implementations.AutomaticTax; +namespace Bit.Core.Billing.Tax.Services.Implementations; public class AutomaticTaxFactory( IFeatureService featureService, diff --git a/src/Core/Billing/Services/Implementations/AutomaticTax/BusinessUseAutomaticTaxStrategy.cs b/src/Core/Billing/Tax/Services/Implementations/BusinessUseAutomaticTaxStrategy.cs similarity index 97% rename from src/Core/Billing/Services/Implementations/AutomaticTax/BusinessUseAutomaticTaxStrategy.cs rename to src/Core/Billing/Tax/Services/Implementations/BusinessUseAutomaticTaxStrategy.cs index 40eb6e4540..310aced130 100644 --- a/src/Core/Billing/Services/Implementations/AutomaticTax/BusinessUseAutomaticTaxStrategy.cs +++ b/src/Core/Billing/Tax/Services/Implementations/BusinessUseAutomaticTaxStrategy.cs @@ -3,7 +3,7 @@ using Bit.Core.Billing.Extensions; using Bit.Core.Services; using Stripe; -namespace Bit.Core.Billing.Services.Implementations.AutomaticTax; +namespace Bit.Core.Billing.Tax.Services.Implementations; public class BusinessUseAutomaticTaxStrategy(IFeatureService featureService) : IAutomaticTaxStrategy { diff --git a/src/Core/Billing/Services/Implementations/AutomaticTax/PersonalUseAutomaticTaxStrategy.cs b/src/Core/Billing/Tax/Services/Implementations/PersonalUseAutomaticTaxStrategy.cs similarity index 96% rename from src/Core/Billing/Services/Implementations/AutomaticTax/PersonalUseAutomaticTaxStrategy.cs rename to src/Core/Billing/Tax/Services/Implementations/PersonalUseAutomaticTaxStrategy.cs index 15ee1adf8f..e89fc6a3b3 100644 --- a/src/Core/Billing/Services/Implementations/AutomaticTax/PersonalUseAutomaticTaxStrategy.cs +++ b/src/Core/Billing/Tax/Services/Implementations/PersonalUseAutomaticTaxStrategy.cs @@ -3,7 +3,7 @@ using Bit.Core.Billing.Extensions; using Bit.Core.Services; using Stripe; -namespace Bit.Core.Billing.Services.Implementations.AutomaticTax; +namespace Bit.Core.Billing.Tax.Services.Implementations; public class PersonalUseAutomaticTaxStrategy(IFeatureService featureService) : IAutomaticTaxStrategy { diff --git a/src/Core/Billing/Services/TaxService.cs b/src/Core/Billing/Tax/Services/Implementations/TaxService.cs similarity index 99% rename from src/Core/Billing/Services/TaxService.cs rename to src/Core/Billing/Tax/Services/Implementations/TaxService.cs index 3066be92d1..204c997335 100644 --- a/src/Core/Billing/Services/TaxService.cs +++ b/src/Core/Billing/Tax/Services/Implementations/TaxService.cs @@ -1,7 +1,7 @@ using System.Text.RegularExpressions; -using Bit.Core.Billing.Models; +using Bit.Core.Billing.Tax.Models; -namespace Bit.Core.Billing.Services; +namespace Bit.Core.Billing.Tax.Services.Implementations; public class TaxService : ITaxService { diff --git a/src/Core/Billing/Utilities.cs b/src/Core/Billing/Utilities.cs index 695a3b1bb4..ebb7b0e525 100644 --- a/src/Core/Billing/Utilities.cs +++ b/src/Core/Billing/Utilities.cs @@ -1,4 +1,5 @@ using Bit.Core.Billing.Models; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Services; using Stripe; diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index ded9f4cfd3..3fdb829cf4 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -1,9 +1,8 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.Billing.Models; -using Bit.Core.Billing.Models.Api.Requests.Accounts; -using Bit.Core.Billing.Models.Api.Requests.Organizations; -using Bit.Core.Billing.Models.Api.Responses; +using Bit.Core.Billing.Tax.Requests; +using Bit.Core.Billing.Tax.Responses; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 85ad7d64d7..65c0525535 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -3,14 +3,15 @@ using Bit.Core.AdminConsole.Models.Business; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; -using Bit.Core.Billing.Models.Api.Requests.Accounts; -using Bit.Core.Billing.Models.Api.Requests.Organizations; -using Bit.Core.Billing.Models.Api.Responses; using Bit.Core.Billing.Models.Business; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Contracts; -using Bit.Core.Billing.Services.Implementations.AutomaticTax; +using Bit.Core.Billing.Tax.Models; +using Bit.Core.Billing.Tax.Requests; +using Bit.Core.Billing.Tax.Responses; +using Bit.Core.Billing.Tax.Services; +using Bit.Core.Billing.Tax.Services.Implementations; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 71661493ec..151ff38aa5 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -16,6 +16,7 @@ using Bit.Core.Billing.Constants; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Services; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; diff --git a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs index df84f74d11..36990c7f9a 100644 --- a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs @@ -7,10 +7,10 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Models; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Repositories; using Bit.Core.Billing.Services; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Context; using Bit.Core.Models.Api; using Bit.Core.Models.BitStripe; diff --git a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs index 9e4be78787..b1f78ed987 100644 --- a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs +++ b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs @@ -3,13 +3,14 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Models; -using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Contracts; using Bit.Core.Billing.Services.Implementations; +using Bit.Core.Billing.Tax.Models; +using Bit.Core.Billing.Tax.Services; using Bit.Core.Enums; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Test.Billing.Stubs; +using Bit.Core.Test.Billing.Tax.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Braintree; diff --git a/test/Core.Test/Billing/Tax/Commands/PreviewTaxAmountCommandTests.cs b/test/Core.Test/Billing/Tax/Commands/PreviewTaxAmountCommandTests.cs new file mode 100644 index 0000000000..c35dc275e6 --- /dev/null +++ b/test/Core.Test/Billing/Tax/Commands/PreviewTaxAmountCommandTests.cs @@ -0,0 +1,346 @@ +using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Models; +using Bit.Core.Billing.Pricing; +using Bit.Core.Billing.Tax.Commands; +using Bit.Core.Billing.Tax.Services; +using Bit.Core.Services; +using Bit.Core.Utilities; +using Microsoft.Extensions.Logging; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Stripe; +using Xunit; +using static Bit.Core.Billing.Tax.Commands.OrganizationTrialParameters; + +namespace Bit.Core.Test.Billing.Tax.Commands; + +public class PreviewTaxAmountCommandTests +{ + private readonly ILogger _logger = Substitute.For>(); + private readonly IPricingClient _pricingClient = Substitute.For(); + private readonly IStripeAdapter _stripeAdapter = Substitute.For(); + private readonly ITaxService _taxService = Substitute.For(); + + private readonly PreviewTaxAmountCommand _command; + + public PreviewTaxAmountCommandTests() + { + _command = new PreviewTaxAmountCommand(_logger, _pricingClient, _stripeAdapter, _taxService); + } + + [Fact] + public async Task Run_WithSeatBasedPasswordManagerPlan_GetsTaxAmount() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.EnterpriseAnnually, + ProductType = ProductType.PasswordManager, + TaxInformation = new TaxInformationDTO + { + Country = "US", + PostalCode = "12345" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Is(options => + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "US" && + options.CustomerDetails.Address.PostalCode == "12345" && + options.SubscriptionDetails.Items.Count == 1 && + options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripeSeatPlanId && + options.SubscriptionDetails.Items[0].Quantity == 1 && + options.AutomaticTax.Enabled == true + )) + .Returns(expectedInvoice); + + // Act + var result = await _command.Run(parameters); + + // Assert + Assert.True(result.IsT0); + var taxAmount = result.AsT0; + Assert.Equal(expectedInvoice.Tax, (long)taxAmount * 100); + } + + [Fact] + public async Task Run_WithNonSeatBasedPasswordManagerPlan_GetsTaxAmount() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.FamiliesAnnually, + ProductType = ProductType.PasswordManager, + TaxInformation = new TaxInformationDTO + { + Country = "US", + PostalCode = "12345" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Is(options => + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "US" && + options.CustomerDetails.Address.PostalCode == "12345" && + options.SubscriptionDetails.Items.Count == 1 && + options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripePlanId && + options.SubscriptionDetails.Items[0].Quantity == 1 && + options.AutomaticTax.Enabled == true + )) + .Returns(expectedInvoice); + + // Act + var result = await _command.Run(parameters); + + // Assert + Assert.True(result.IsT0); + var taxAmount = result.AsT0; + Assert.Equal(expectedInvoice.Tax, (long)taxAmount * 100); + } + + [Fact] + public async Task Run_WithSecretsManagerPlan_GetsTaxAmount() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.EnterpriseAnnually, + ProductType = ProductType.SecretsManager, + TaxInformation = new TaxInformationDTO + { + Country = "US", + PostalCode = "12345" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Is(options => + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "US" && + options.CustomerDetails.Address.PostalCode == "12345" && + options.SubscriptionDetails.Items.Count == 2 && + options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripeSeatPlanId && + options.SubscriptionDetails.Items[0].Quantity == 1 && + options.SubscriptionDetails.Items[1].Price == plan.SecretsManager.StripeSeatPlanId && + options.SubscriptionDetails.Items[1].Quantity == 1 && + options.Coupon == StripeConstants.CouponIDs.SecretsManagerStandalone && + options.AutomaticTax.Enabled == true + )) + .Returns(expectedInvoice); + + // Act + var result = await _command.Run(parameters); + + // Assert + Assert.True(result.IsT0); + var taxAmount = result.AsT0; + Assert.Equal(expectedInvoice.Tax, (long)taxAmount * 100); + } + + [Fact] + public async Task Run_NonUSWithoutTaxId_GetsTaxAmount() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.EnterpriseAnnually, + ProductType = ProductType.PasswordManager, + TaxInformation = new TaxInformationDTO + { + Country = "CA", + PostalCode = "12345" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Is(options => + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "CA" && + options.CustomerDetails.Address.PostalCode == "12345" && + options.SubscriptionDetails.Items.Count == 1 && + options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripeSeatPlanId && + options.SubscriptionDetails.Items[0].Quantity == 1 && + options.AutomaticTax.Enabled == false + )) + .Returns(expectedInvoice); + + // Act + var result = await _command.Run(parameters); + + // Assert + Assert.True(result.IsT0); + var taxAmount = result.AsT0; + Assert.Equal(expectedInvoice.Tax, (long)taxAmount * 100); + } + + [Fact] + public async Task Run_NonUSWithTaxId_GetsTaxAmount() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.EnterpriseAnnually, + ProductType = ProductType.PasswordManager, + TaxInformation = new TaxInformationDTO + { + Country = "CA", + PostalCode = "12345", + TaxId = "123456789" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + _taxService.GetStripeTaxCode(parameters.TaxInformation.Country, parameters.TaxInformation.TaxId) + .Returns("ca_st"); + + var expectedInvoice = new Invoice { Tax = 1000 }; // $10.00 in cents + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Is(options => + options.Currency == "usd" && + options.CustomerDetails.Address.Country == "CA" && + options.CustomerDetails.Address.PostalCode == "12345" && + options.CustomerDetails.TaxIds.Count == 1 && + options.CustomerDetails.TaxIds[0].Type == "ca_st" && + options.CustomerDetails.TaxIds[0].Value == "123456789" && + options.SubscriptionDetails.Items.Count == 1 && + options.SubscriptionDetails.Items[0].Price == plan.PasswordManager.StripeSeatPlanId && + options.SubscriptionDetails.Items[0].Quantity == 1 && + options.AutomaticTax.Enabled == true + )) + .Returns(expectedInvoice); + + // Act + var result = await _command.Run(parameters); + + // Assert + Assert.True(result.IsT0); + var taxAmount = result.AsT0; + Assert.Equal(expectedInvoice.Tax, (long)taxAmount * 100); + } + + [Fact] + public async Task Run_NonUSWithTaxId_UnknownTaxIdType_BadRequest() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.EnterpriseAnnually, + ProductType = ProductType.PasswordManager, + TaxInformation = new TaxInformationDTO + { + Country = "CA", + PostalCode = "12345", + TaxId = "123456789" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + _taxService.GetStripeTaxCode(parameters.TaxInformation.Country, parameters.TaxInformation.TaxId) + .Returns((string)null); + + // Act + var result = await _command.Run(parameters); + + // Assert + Assert.True(result.IsT1); + var badRequest = result.AsT1; + Assert.Equal(BillingErrorTranslationKeys.UnknownTaxIdType, badRequest.TranslationKey); + } + + [Fact] + public async Task Run_CustomerTaxLocationInvalid_BadRequest() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.EnterpriseAnnually, + ProductType = ProductType.PasswordManager, + TaxInformation = new TaxInformationDTO + { + Country = "US", + PostalCode = "12345" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()) + .Throws(new StripeException + { + StripeError = new StripeError { Code = StripeConstants.ErrorCodes.CustomerTaxLocationInvalid } + }); + + // Act + var result = await _command.Run(parameters); + + // Assert + Assert.True(result.IsT1); + var badRequest = result.AsT1; + Assert.Equal(BillingErrorTranslationKeys.CustomerTaxLocationInvalid, badRequest.TranslationKey); + } + + [Fact] + public async Task Run_TaxIdInvalid_BadRequest() + { + // Arrange + var parameters = new OrganizationTrialParameters + { + PlanType = PlanType.EnterpriseAnnually, + ProductType = ProductType.PasswordManager, + TaxInformation = new TaxInformationDTO + { + Country = "US", + PostalCode = "12345" + } + }; + + var plan = StaticStore.GetPlan(parameters.PlanType); + + _pricingClient.GetPlanOrThrow(parameters.PlanType).Returns(plan); + + _stripeAdapter.InvoiceCreatePreviewAsync(Arg.Any()) + .Throws(new StripeException + { + StripeError = new StripeError { Code = StripeConstants.ErrorCodes.TaxIdInvalid } + }); + + // Act + var result = await _command.Run(parameters); + + // Assert + Assert.True(result.IsT1); + var badRequest = result.AsT1; + Assert.Equal(BillingErrorTranslationKeys.TaxIdInvalid, badRequest.TranslationKey); + } +} diff --git a/test/Core.Test/Billing/Services/Implementations/AutomaticTaxFactoryTests.cs b/test/Core.Test/Billing/Tax/Services/AutomaticTaxFactoryTests.cs similarity index 96% rename from test/Core.Test/Billing/Services/Implementations/AutomaticTaxFactoryTests.cs rename to test/Core.Test/Billing/Tax/Services/AutomaticTaxFactoryTests.cs index 7d5c9c3a26..8de51b1745 100644 --- a/test/Core.Test/Billing/Services/Implementations/AutomaticTaxFactoryTests.cs +++ b/test/Core.Test/Billing/Tax/Services/AutomaticTaxFactoryTests.cs @@ -3,14 +3,14 @@ using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services.Contracts; -using Bit.Core.Billing.Services.Implementations.AutomaticTax; +using Bit.Core.Billing.Tax.Services.Implementations; using Bit.Core.Entities; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; -namespace Bit.Core.Test.Billing.Services.Implementations; +namespace Bit.Core.Test.Billing.Tax.Services; [SutProviderCustomize] public class AutomaticTaxFactoryTests diff --git a/test/Core.Test/Billing/Services/Implementations/AutomaticTax/BusinessUseAutomaticTaxStrategyTests.cs b/test/Core.Test/Billing/Tax/Services/BusinessUseAutomaticTaxStrategyTests.cs similarity index 99% rename from test/Core.Test/Billing/Services/Implementations/AutomaticTax/BusinessUseAutomaticTaxStrategyTests.cs rename to test/Core.Test/Billing/Tax/Services/BusinessUseAutomaticTaxStrategyTests.cs index dc40656275..dc10d222f1 100644 --- a/test/Core.Test/Billing/Services/Implementations/AutomaticTax/BusinessUseAutomaticTaxStrategyTests.cs +++ b/test/Core.Test/Billing/Tax/Services/BusinessUseAutomaticTaxStrategyTests.cs @@ -1,5 +1,5 @@ using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Services.Implementations.AutomaticTax; +using Bit.Core.Billing.Tax.Services.Implementations; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -7,7 +7,7 @@ using NSubstitute; using Stripe; using Xunit; -namespace Bit.Core.Test.Billing.Services.Implementations.AutomaticTax; +namespace Bit.Core.Test.Billing.Tax.Services; [SutProviderCustomize] public class BusinessUseAutomaticTaxStrategyTests diff --git a/test/Core.Test/Billing/Stubs/FakeAutomaticTaxStrategy.cs b/test/Core.Test/Billing/Tax/Services/FakeAutomaticTaxStrategy.cs similarity index 92% rename from test/Core.Test/Billing/Stubs/FakeAutomaticTaxStrategy.cs rename to test/Core.Test/Billing/Tax/Services/FakeAutomaticTaxStrategy.cs index 253aead5c7..2f3cbc98ee 100644 --- a/test/Core.Test/Billing/Stubs/FakeAutomaticTaxStrategy.cs +++ b/test/Core.Test/Billing/Tax/Services/FakeAutomaticTaxStrategy.cs @@ -1,7 +1,7 @@ -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Tax.Services; using Stripe; -namespace Bit.Core.Test.Billing.Stubs; +namespace Bit.Core.Test.Billing.Tax.Services; /// /// Whether the subscription options will have automatic tax enabled or not. diff --git a/test/Core.Test/Billing/Services/Implementations/AutomaticTax/PersonalUseAutomaticTaxStrategyTests.cs b/test/Core.Test/Billing/Tax/Services/PersonalUseAutomaticTaxStrategyTests.cs similarity index 98% rename from test/Core.Test/Billing/Services/Implementations/AutomaticTax/PersonalUseAutomaticTaxStrategyTests.cs rename to test/Core.Test/Billing/Tax/Services/PersonalUseAutomaticTaxStrategyTests.cs index 2d50c9f75a..30614b94ba 100644 --- a/test/Core.Test/Billing/Services/Implementations/AutomaticTax/PersonalUseAutomaticTaxStrategyTests.cs +++ b/test/Core.Test/Billing/Tax/Services/PersonalUseAutomaticTaxStrategyTests.cs @@ -1,5 +1,5 @@ using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Services.Implementations.AutomaticTax; +using Bit.Core.Billing.Tax.Services.Implementations; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -7,7 +7,7 @@ using NSubstitute; using Stripe; using Xunit; -namespace Bit.Core.Test.Billing.Services.Implementations.AutomaticTax; +namespace Bit.Core.Test.Billing.Tax.Services; [SutProviderCustomize] public class PersonalUseAutomaticTaxStrategyTests diff --git a/test/Core.Test/Services/StripePaymentServiceTests.cs b/test/Core.Test/Services/StripePaymentServiceTests.cs index 835f69b214..fa1dd60617 100644 --- a/test/Core.Test/Services/StripePaymentServiceTests.cs +++ b/test/Core.Test/Services/StripePaymentServiceTests.cs @@ -1,13 +1,12 @@ using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Models.Api.Requests; -using Bit.Core.Billing.Models.Api.Requests.Organizations; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Contracts; +using Bit.Core.Billing.Tax.Requests; +using Bit.Core.Billing.Tax.Services; using Bit.Core.Enums; using Bit.Core.Services; -using Bit.Core.Test.Billing.Stubs; +using Bit.Core.Test.Billing.Tax.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; From ca402263802ae38a32f36f58332adc8d36116bc7 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Tue, 13 May 2025 09:31:42 -0400 Subject: [PATCH 048/114] [PM-21418] Do not start organization on trial when request includes `SkipTrial` (#5793) * Do not start organization on a trial when createRequest.SkipTrial is true * Run dotnet format --- .../OrganizationCreateRequestModel.cs | 3 +++ src/Core/Billing/Models/Sales/OrganizationSale.cs | 15 ++++++++++++--- src/Core/Models/Business/OrganizationSignup.cs | 1 + 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs index 539260a312..e18122fd2b 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs @@ -75,6 +75,8 @@ public class OrganizationCreateRequestModel : IValidatableObject public string InitiationPath { get; set; } + public bool SkipTrial { get; set; } + public virtual OrganizationSignup ToOrganizationSignup(User user) { var orgSignup = new OrganizationSignup @@ -107,6 +109,7 @@ public class OrganizationCreateRequestModel : IValidatableObject BillingAddressCountry = BillingAddressCountry, }, InitiationPath = InitiationPath, + SkipTrial = SkipTrial }; Keys?.ToOrganizationSignup(orgSignup); diff --git a/src/Core/Billing/Models/Sales/OrganizationSale.cs b/src/Core/Billing/Models/Sales/OrganizationSale.cs index 87d17551eb..78ad26871b 100644 --- a/src/Core/Billing/Models/Sales/OrganizationSale.cs +++ b/src/Core/Billing/Models/Sales/OrganizationSale.cs @@ -27,12 +27,21 @@ public class OrganizationSale public static OrganizationSale From( Organization organization, - OrganizationSignup signup) => new() + OrganizationSignup signup) + { + var customerSetup = string.IsNullOrEmpty(organization.GatewayCustomerId) ? GetCustomerSetup(signup) : null; + + var subscriptionSetup = GetSubscriptionSetup(signup); + + subscriptionSetup.SkipTrial = signup.SkipTrial; + + return new OrganizationSale { Organization = organization, - CustomerSetup = string.IsNullOrEmpty(organization.GatewayCustomerId) ? GetCustomerSetup(signup) : null, - SubscriptionSetup = GetSubscriptionSetup(signup) + CustomerSetup = customerSetup, + SubscriptionSetup = subscriptionSetup }; + } public static OrganizationSale From( Organization organization, diff --git a/src/Core/Models/Business/OrganizationSignup.cs b/src/Core/Models/Business/OrganizationSignup.cs index b5ac69e73f..b8bd670d21 100644 --- a/src/Core/Models/Business/OrganizationSignup.cs +++ b/src/Core/Models/Business/OrganizationSignup.cs @@ -16,4 +16,5 @@ public class OrganizationSignup : OrganizationUpgrade public string InitiationPath { get; set; } public bool IsFromSecretsManagerTrial { get; set; } public bool IsFromProvider { get; set; } + public bool SkipTrial { get; set; } } From 4195baf1c560b10c00018c6d44f908ad4d77c8b2 Mon Sep 17 00:00:00 2001 From: Vijay Oommen Date: Tue, 13 May 2025 09:05:22 -0500 Subject: [PATCH 049/114] [PM-20980] Add a note to freshdesk ticket when no user is found (#5768) --- .../Controllers/FreshdeskController.cs | 34 +++++++++++------- .../Controllers/FreshdeskControllerTests.cs | 36 +++++++++++++++++++ 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/src/Billing/Controllers/FreshdeskController.cs b/src/Billing/Controllers/FreshdeskController.cs index 4bf6b7bad4..1fb0fb7ac7 100644 --- a/src/Billing/Controllers/FreshdeskController.cs +++ b/src/Billing/Controllers/FreshdeskController.cs @@ -63,6 +63,12 @@ public class FreshdeskController : Controller note += $"
  • Region: {_billingSettings.FreshDesk.Region}
  • "; var customFields = new Dictionary(); var user = await _userRepository.GetByEmailAsync(ticketContactEmail); + if (user == null) + { + note += $"
  • No user found: {ticketContactEmail}
  • "; + await CreateNote(ticketId, note); + } + if (user != null) { var userLink = $"{_globalSettings.BaseServiceUri.Admin}/users/edit/{user.Id}"; @@ -121,18 +127,7 @@ public class FreshdeskController : Controller Content = JsonContent.Create(updateBody), }; await CallFreshdeskApiAsync(updateRequest); - - var noteBody = new Dictionary - { - { "body", $"
      {note}
    " }, - { "private", true } - }; - var noteRequest = new HttpRequestMessage(HttpMethod.Post, - string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/notes", ticketId)) - { - Content = JsonContent.Create(noteBody), - }; - await CallFreshdeskApiAsync(noteRequest); + await CreateNote(ticketId, note); } return new OkResult(); @@ -208,6 +203,21 @@ public class FreshdeskController : Controller return true; } + private async Task CreateNote(string ticketId, string note) + { + var noteBody = new Dictionary + { + { "body", $"
      {note}
    " }, + { "private", true } + }; + var noteRequest = new HttpRequestMessage(HttpMethod.Post, + string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/notes", ticketId)) + { + Content = JsonContent.Create(noteBody), + }; + await CallFreshdeskApiAsync(noteRequest); + } + private async Task AddAnswerNoteToTicketAsync(string note, string ticketId) { // if there is no content, then we don't need to add a note diff --git a/test/Billing.Test/Controllers/FreshdeskControllerTests.cs b/test/Billing.Test/Controllers/FreshdeskControllerTests.cs index 26ce310b9c..90f8a09ea0 100644 --- a/test/Billing.Test/Controllers/FreshdeskControllerTests.cs +++ b/test/Billing.Test/Controllers/FreshdeskControllerTests.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Options; using NSubstitute; +using NSubstitute.ReceivedExtensions; using Xunit; namespace Bit.Billing.Test.Controllers; @@ -71,6 +72,41 @@ public class FreshdeskControllerTests _ = mockHttpMessageHandler.Received(1).Send(Arg.Is(m => m.Method == HttpMethod.Post && m.RequestUri.ToString().EndsWith($"{model.TicketId}/notes")), Arg.Any()); } + [Theory] + [BitAutoData(WebhookKey)] + public async Task PostWebhook_add_note_when_user_is_invalid( + string freshdeskWebhookKey, FreshdeskWebhookModel model, + SutProvider sutProvider) + { + // Arrange - for an invalid user + model.TicketContactEmail = "invalid@user"; + sutProvider.GetDependency().GetByEmailAsync(model.TicketContactEmail).Returns((User)null); + sutProvider.GetDependency>().Value.FreshDesk.WebhookKey.Returns(WebhookKey); + + var mockHttpMessageHandler = Substitute.ForPartsOf(); + var mockResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK); + mockHttpMessageHandler.Send(Arg.Any(), Arg.Any()) + .Returns(mockResponse); + var httpClient = new HttpClient(mockHttpMessageHandler); + sutProvider.GetDependency().CreateClient("FreshdeskApi").Returns(httpClient); + + // Act + var response = await sutProvider.Sut.PostWebhook(freshdeskWebhookKey, model); + + // Assert + var statusCodeResult = Assert.IsAssignableFrom(response); + Assert.Equal(StatusCodes.Status200OK, statusCodeResult.StatusCode); + + await mockHttpMessageHandler + .Received(1).Send( + Arg.Is( + m => m.Method == HttpMethod.Post + && m.RequestUri.ToString().EndsWith($"{model.TicketId}/notes") + && m.Content.ReadAsStringAsync().Result.Contains("No user found")), + Arg.Any()); + } + + [Theory] [BitAutoData((string)null, null)] [BitAutoData((string)null)] From d58836bb600182fbb194c6fcd7fe3a6166f80345 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 13 May 2025 16:48:37 +0100 Subject: [PATCH 050/114] Resolve the remove button issue (#5811) --- .../Controllers/OrganizationSponsorshipsController.cs | 4 ++-- src/Core/Repositories/IOrganizationSponsorshipRepository.cs | 2 +- .../Repositories/OrganizationSponsorshipRepository.cs | 5 +++-- .../Repositories/OrganizationSponsorshipRepository.cs | 5 +++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs index c4dc5fae75..0e04385dc9 100644 --- a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs @@ -207,7 +207,7 @@ public class OrganizationSponsorshipsController : Controller [HttpDelete("{sponsoringOrganizationId}")] [HttpPost("{sponsoringOrganizationId}/delete")] [SelfHosted(NotSelfHostedOnly = true)] - public async Task RevokeSponsorship(Guid sponsoringOrganizationId) + public async Task RevokeSponsorship(Guid sponsoringOrganizationId, [FromQuery] bool isAdminInitiated = false) { var orgUser = await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrganizationId, _currentContext.UserId ?? default); @@ -217,7 +217,7 @@ public class OrganizationSponsorshipsController : Controller } var existingOrgSponsorship = await _organizationSponsorshipRepository - .GetBySponsoringOrganizationUserIdAsync(orgUser.Id); + .GetBySponsoringOrganizationUserIdAsync(orgUser.Id, isAdminInitiated); await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship); } diff --git a/src/Core/Repositories/IOrganizationSponsorshipRepository.cs b/src/Core/Repositories/IOrganizationSponsorshipRepository.cs index 30e6ee4a33..00cf6c8cce 100644 --- a/src/Core/Repositories/IOrganizationSponsorshipRepository.cs +++ b/src/Core/Repositories/IOrganizationSponsorshipRepository.cs @@ -11,7 +11,7 @@ public interface IOrganizationSponsorshipRepository : IRepository organizationSponsorships); Task DeleteManyAsync(IEnumerable organizationSponsorshipIds); Task> GetManyBySponsoringOrganizationAsync(Guid sponsoringOrganizationId); - Task GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId); + Task GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId, bool isAdminInitiated = false); Task GetBySponsoredOrganizationIdAsync(Guid sponsoredOrganizationId); Task GetLatestSyncDateBySponsoringOrganizationIdAsync(Guid sponsoringOrganizationId); } diff --git a/src/Infrastructure.Dapper/Repositories/OrganizationSponsorshipRepository.cs b/src/Infrastructure.Dapper/Repositories/OrganizationSponsorshipRepository.cs index cebf4b55c6..7033f2113b 100644 --- a/src/Infrastructure.Dapper/Repositories/OrganizationSponsorshipRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/OrganizationSponsorshipRepository.cs @@ -89,7 +89,7 @@ public class OrganizationSponsorshipRepository : Repository GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId) + public async Task GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId, bool isAdminInitiated) { using (var connection = new SqlConnection(ConnectionString)) { @@ -97,7 +97,8 @@ public class OrganizationSponsorshipRepository : Repository GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId) + public async Task GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId, bool isAdminInitiated = false) { using (var scope = ServiceScopeFactory.CreateScope()) { var dbContext = GetDatabaseContext(scope); - var orgSponsorship = await GetDbSet(dbContext).Where(e => e.SponsoringOrganizationUserId == sponsoringOrganizationUserId) + var orgSponsorship = await GetDbSet(dbContext) + .Where(e => e.SponsoringOrganizationUserId == sponsoringOrganizationUserId && e.IsAdminInitiated == isAdminInitiated) .FirstOrDefaultAsync(); return orgSponsorship; } From 5700347e08509fb749fa90dfb2dfb0a2f641c66a Mon Sep 17 00:00:00 2001 From: Graham Walker Date: Tue, 13 May 2025 13:53:29 -0500 Subject: [PATCH 051/114] PM-21037 adding UserGuid field to MemberCipherDetailsResponseModel (#5813) --- .../Dirt/Models/Response/MemberCipherDetailsResponseModel.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs b/src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs index 5c87264c51..d927da8123 100644 --- a/src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs +++ b/src/Api/Dirt/Models/Response/MemberCipherDetailsResponseModel.cs @@ -4,18 +4,20 @@ namespace Bit.Api.Tools.Models.Response; public class MemberCipherDetailsResponseModel { + public Guid? UserGuid { get; set; } public string UserName { get; set; } public string Email { get; set; } public bool UsesKeyConnector { get; set; } /// - /// A distinct list of the cipher ids associated with + /// A distinct list of the cipher ids associated with /// the organization member /// public IEnumerable CipherIds { get; set; } public MemberCipherDetailsResponseModel(MemberAccessCipherDetails memberAccessCipherDetails) { + this.UserGuid = memberAccessCipherDetails.UserGuid; this.UserName = memberAccessCipherDetails.UserName; this.Email = memberAccessCipherDetails.Email; this.UsesKeyConnector = memberAccessCipherDetails.UsesKeyConnector; From dd2ea41b7484010431142859d8121d7f3fe5113b Mon Sep 17 00:00:00 2001 From: Patrick-Pimentel-Bitwarden Date: Tue, 13 May 2025 15:43:11 -0400 Subject: [PATCH 052/114] Auth/pm 17111/add browser to list of approving clients (#5792) * feat(update-auth-approving-clients): [PM-17111] Add Browser to List of Approving Clients - Initial changes. * feat(update-auth-approving-clients): [PM-17111] Add Browser to List of Approving Clients - Updated tests. * test(update-auth-approving-clients): [PM-17111] Add Browser to List of Approving Clients - Strengthened tests. --- .../UserDecryptionOptionsBuilder.cs | 8 +-- .../Utilities/LoginApprovingClientTypes.cs | 22 ++++++++ .../Utilities/LoginApprovingDeviceTypes.cs | 20 ------- .../UserDecryptionOptionsBuilderTests.cs | 52 +++++++++++++++++-- 4 files changed, 74 insertions(+), 28 deletions(-) create mode 100644 src/Identity/Utilities/LoginApprovingClientTypes.cs delete mode 100644 src/Identity/Utilities/LoginApprovingDeviceTypes.cs diff --git a/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs b/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs index 2dc1f2926b..34a9d6c573 100644 --- a/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs +++ b/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs @@ -6,6 +6,7 @@ using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; +using Bit.Core.Utilities; using Bit.Identity.Utilities; namespace Bit.Identity.IdentityServer; @@ -24,7 +25,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder private UserDecryptionOptions _options = new UserDecryptionOptions(); private User? _user; - private Core.Auth.Entities.SsoConfig? _ssoConfig; + private SsoConfig? _ssoConfig; private Device? _device; public UserDecryptionOptionsBuilder( @@ -45,7 +46,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder return this; } - public IUserDecryptionOptionsBuilder WithSso(Core.Auth.Entities.SsoConfig ssoConfig) + public IUserDecryptionOptionsBuilder WithSso(SsoConfig ssoConfig) { _ssoConfig = ssoConfig; return this; @@ -119,8 +120,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder // their current device. // NOTE: this doesn't check for if the users have configured the devices to be capable of approving requests as that is a client side setting. hasLoginApprovingDevice = allDevices - .Where(d => d.Identifier != _device.Identifier && LoginApprovingDeviceTypes.Types.Contains(d.Type)) - .Any(); + .Any(d => d.Identifier != _device.Identifier && LoginApprovingClientTypes.TypesThatCanApprove.Contains(DeviceTypes.ToClientType(d.Type))); } // Determine if user has manage reset password permission as post sso logic requires it for forcing users with this permission to set a MP diff --git a/src/Identity/Utilities/LoginApprovingClientTypes.cs b/src/Identity/Utilities/LoginApprovingClientTypes.cs new file mode 100644 index 0000000000..dd27a87550 --- /dev/null +++ b/src/Identity/Utilities/LoginApprovingClientTypes.cs @@ -0,0 +1,22 @@ +using Bit.Core.Enums; + +namespace Bit.Identity.Utilities; + +public static class LoginApprovingClientTypes +{ + private static readonly IReadOnlyCollection _clientTypesThatCanApprove; + + static LoginApprovingClientTypes() + { + var clientTypes = new List + { + ClientType.Desktop, + ClientType.Mobile, + ClientType.Web, + ClientType.Browser, + }; + _clientTypesThatCanApprove = clientTypes.AsReadOnly(); + } + + public static IReadOnlyCollection TypesThatCanApprove => _clientTypesThatCanApprove; +} diff --git a/src/Identity/Utilities/LoginApprovingDeviceTypes.cs b/src/Identity/Utilities/LoginApprovingDeviceTypes.cs deleted file mode 100644 index b8b11a4d19..0000000000 --- a/src/Identity/Utilities/LoginApprovingDeviceTypes.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Bit.Core.Enums; -using Bit.Core.Utilities; - -namespace Bit.Identity.Utilities; - -public static class LoginApprovingDeviceTypes -{ - private static readonly IReadOnlyCollection _deviceTypes; - - static LoginApprovingDeviceTypes() - { - var deviceTypes = new List(); - deviceTypes.AddRange(DeviceTypes.DesktopTypes); - deviceTypes.AddRange(DeviceTypes.MobileTypes); - deviceTypes.AddRange(DeviceTypes.BrowserTypes); - _deviceTypes = deviceTypes.AsReadOnly(); - } - - public static IReadOnlyCollection Types => _deviceTypes; -} diff --git a/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs b/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs index 89940275b0..0a6346fe9e 100644 --- a/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs +++ b/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs @@ -6,7 +6,6 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Identity.IdentityServer; -using Bit.Identity.Utilities; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; @@ -102,12 +101,39 @@ public class UserDecryptionOptionsBuilderTests Assert.Equal(device.EncryptedUserKey, result.TrustedDeviceOption?.EncryptedUserKey); } - [Theory, BitAutoData] - public async Task Build_WhenHasLoginApprovingDevice_ShouldApprovingDeviceTrue(SsoConfig ssoConfig, SsoConfigurationData configurationData, User user, Device device, Device approvingDevice) + [Theory] + // Desktop + [BitAutoData(DeviceType.LinuxDesktop)] + [BitAutoData(DeviceType.MacOsDesktop)] + [BitAutoData(DeviceType.WindowsDesktop)] + [BitAutoData(DeviceType.UWP)] + // Mobile + [BitAutoData(DeviceType.Android)] + [BitAutoData(DeviceType.iOS)] + [BitAutoData(DeviceType.AndroidAmazon)] + // Web + [BitAutoData(DeviceType.ChromeBrowser)] + [BitAutoData(DeviceType.FirefoxBrowser)] + [BitAutoData(DeviceType.OperaBrowser)] + [BitAutoData(DeviceType.EdgeBrowser)] + [BitAutoData(DeviceType.IEBrowser)] + [BitAutoData(DeviceType.SafariBrowser)] + [BitAutoData(DeviceType.VivaldiBrowser)] + [BitAutoData(DeviceType.UnknownBrowser)] + // Extension + [BitAutoData(DeviceType.ChromeExtension)] + [BitAutoData(DeviceType.FirefoxExtension)] + [BitAutoData(DeviceType.OperaExtension)] + [BitAutoData(DeviceType.EdgeExtension)] + [BitAutoData(DeviceType.VivaldiExtension)] + [BitAutoData(DeviceType.SafariExtension)] + public async Task Build_WhenHasLoginApprovingDevice_ShouldApprovingDeviceTrue( + DeviceType deviceType, + SsoConfig ssoConfig, SsoConfigurationData configurationData, User user, Device device, Device approvingDevice) { configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption; ssoConfig.Data = configurationData.Serialize(); - approvingDevice.Type = LoginApprovingDeviceTypes.Types.First(); + approvingDevice.Type = deviceType; _deviceRepository.GetManyByUserIdAsync(user.Id).Returns(new Device[] { approvingDevice }); var result = await _builder.ForUser(user).WithSso(ssoConfig).WithDevice(device).BuildAsync(); @@ -115,6 +141,24 @@ public class UserDecryptionOptionsBuilderTests Assert.True(result.TrustedDeviceOption?.HasLoginApprovingDevice); } + [Theory] + [BitAutoData(DeviceType.WindowsCLI)] + [BitAutoData(DeviceType.MacOsCLI)] + [BitAutoData(DeviceType.LinuxCLI)] + public async Task Build_WhenHasLoginApprovingDevice_ShouldApprovingDeviceFalse( + DeviceType deviceType, + SsoConfig ssoConfig, SsoConfigurationData configurationData, User user, Device device, Device approvingDevice) + { + configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption; + ssoConfig.Data = configurationData.Serialize(); + approvingDevice.Type = deviceType; + _deviceRepository.GetManyByUserIdAsync(user.Id).Returns(new Device[] { approvingDevice }); + + var result = await _builder.ForUser(user).WithSso(ssoConfig).WithDevice(device).BuildAsync(); + + Assert.False(result.TrustedDeviceOption?.HasLoginApprovingDevice); + } + [Theory, BitAutoData] public async Task Build_WhenManageResetPasswordPermissions_ShouldReturnHasManageResetPasswordPermissionTrue( SsoConfig ssoConfig, From 9ebe16587175b1c0e9208f84397bb75d0d595510 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Wed, 14 May 2025 11:13:32 -0400 Subject: [PATCH 053/114] fix(RegistrationViaOrgInviteWelcomeEmail): [Auth/PM-21428] Registration via Org Invite should send welcome email even if reference data isn't provided (#5796) --- .../Registration/Implementations/RegisterUserCommand.cs | 7 +++++++ .../UserFeatures/Registration/RegisterUserCommandTests.cs | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs b/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs index 834d2722cc..e721649dc9 100644 --- a/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs +++ b/src/Core/Auth/UserFeatures/Registration/Implementations/RegisterUserCommand.cs @@ -108,6 +108,7 @@ public class RegisterUserCommand : IRegisterUserCommand var result = await _userService.CreateUserAsync(user, masterPasswordHash); if (result == IdentityResult.Success) { + var sentWelcomeEmail = false; if (!string.IsNullOrEmpty(user.ReferenceData)) { var referenceData = JsonConvert.DeserializeObject>(user.ReferenceData); @@ -115,6 +116,7 @@ public class RegisterUserCommand : IRegisterUserCommand { var initiationPath = value.ToString(); await SendAppropriateWelcomeEmailAsync(user, initiationPath); + sentWelcomeEmail = true; if (!string.IsNullOrEmpty(initiationPath)) { await _referenceEventService.RaiseEventAsync( @@ -128,6 +130,11 @@ public class RegisterUserCommand : IRegisterUserCommand } } + if (!sentWelcomeEmail) + { + await _mailService.SendWelcomeEmailAsync(user); + } + await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.Signup, user, _currentContext)); } diff --git a/test/Core.Test/Auth/UserFeatures/Registration/RegisterUserCommandTests.cs b/test/Core.Test/Auth/UserFeatures/Registration/RegisterUserCommandTests.cs index 02ecb4ecd7..ffc56e89b2 100644 --- a/test/Core.Test/Auth/UserFeatures/Registration/RegisterUserCommandTests.cs +++ b/test/Core.Test/Auth/UserFeatures/Registration/RegisterUserCommandTests.cs @@ -226,6 +226,11 @@ public class RegisterUserCommandTests await sutProvider.GetDependency() .Received(1) .RaiseEventAsync(Arg.Is(refEvent => refEvent.Type == ReferenceEventType.Signup && refEvent.SignupInitiationPath == default)); + + // Even if user doesn't have reference data, we should send them welcome email + await sutProvider.GetDependency() + .Received(1) + .SendWelcomeEmailAsync(user); } Assert.True(result.Succeeded); From 941d06985d007cd7c12b131ace40a856481860b2 Mon Sep 17 00:00:00 2001 From: Robyn MacCallum Date: Wed, 14 May 2025 11:38:51 -0400 Subject: [PATCH 054/114] Update Constants.cs (#5801) --- src/Core/Constants.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 3399a729d1..abd530b2dd 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -169,8 +169,6 @@ public static class FeatureFlagKeys public const string NativeCreateAccountFlow = "native-create-account-flow"; public const string AndroidImportLoginsFlow = "import-logins-flow"; public const string AppReviewPrompt = "app-review-prompt"; - public const string EnablePasswordManagerSyncAndroid = "enable-password-manager-sync-android"; - public const string EnablePasswordManagerSynciOS = "enable-password-manager-sync-ios"; public const string AndroidMutualTls = "mutual-tls"; public const string SingleTapPasskeyCreation = "single-tap-passkey-creation"; public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication"; From 828ed7402c1362974e9313e0e0043cbeece230d6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 May 2025 09:54:09 -0700 Subject: [PATCH 055/114] [deps] Platform: Update quartznet monorepo to 3.14.0 (#5736) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 6397e0b8ea..92f9396fb2 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -60,9 +60,9 @@ - - - + + + From a973a11d90023e5965deca23a543dd05ff2f3439 Mon Sep 17 00:00:00 2001 From: aj-bw <81774843+aj-bw@users.noreply.github.com> Date: Thu, 15 May 2025 09:38:57 -0400 Subject: [PATCH 056/114] scan and build target updates (#5783) --- .github/workflows/build_target.yml | 4 +++- .github/workflows/scan.yml | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build_target.yml b/.github/workflows/build_target.yml index 313446c949..d825721a7d 100644 --- a/.github/workflows/build_target.yml +++ b/.github/workflows/build_target.yml @@ -2,7 +2,9 @@ name: Build on PR Target on: pull_request_target: - types: [opened, synchronize] + types: [opened, synchronize, reopened] + branches: + - "main" defaults: run: diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index fe88782e35..f24a0973fd 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -7,8 +7,14 @@ on: - "main" - "rc" - "hotfix-rc" + pull_request: + types: [opened, synchronize, reopened] + branches-ignore: + - main pull_request_target: - types: [opened, synchronize] + types: [opened, synchronize, reopened] + branches: + - "main" jobs: check-run: From 9e2562fc8d025da47640655e63a7f68b8c6ebf00 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Thu, 15 May 2025 09:42:01 -0400 Subject: [PATCH 057/114] Updated Braintree ID string validation to account for providers (#5794) --- src/Admin/Models/ChargeBraintreeModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Admin/Models/ChargeBraintreeModel.cs b/src/Admin/Models/ChargeBraintreeModel.cs index 2ba06cb980..8c2f39e58d 100644 --- a/src/Admin/Models/ChargeBraintreeModel.cs +++ b/src/Admin/Models/ChargeBraintreeModel.cs @@ -17,7 +17,7 @@ public class ChargeBraintreeModel : IValidatableObject { if (Id != null) { - if (Id.Length != 36 || (Id[0] != 'o' && Id[0] != 'u') || + if (Id.Length != 36 || (Id[0] != 'o' && Id[0] != 'u' && Id[0] != 'p') || !Guid.TryParse(Id.Substring(1, 32), out var guid)) { yield return new ValidationResult("Customer Id is not a valid format."); From 81bff5e5cf4e67ae4b034a88db0e0873f40e693a Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Thu, 15 May 2025 10:42:51 -0400 Subject: [PATCH 058/114] [PM-11622] use organization domains (#5560) * DBO changes * Add migration scripts * wip * wip * wip * add EF migrations * run dotnet format * cleanup * revert business logic * wip * add update statement to mssql migration script * fix user service tests * increment license, add UseOrganizationDomains * add migration helpers to seed initial value from UseSso * clean up * cleanup * fix mssql migrations * fix license version and test * fix file names * fix license json * add missing property to license test * rename file * fix migrations * fix migration * add WHERE to helper scripts * separate schema/data migrations * restore comment * Merge conflict * fix migrations * add new property to migration * wip * fix file names * fix file name --- .../Controllers/OrganizationsController.cs | 1 + .../Models/OrganizationEditModel.cs | 6 +- .../Views/Shared/_OrganizationForm.cshtml | 4 + .../Shared/_OrganizationFormScripts.cshtml | 1 + .../OrganizationResponseModel.cs | 2 + .../ProfileOrganizationResponseModel.cs | 2 + ...rofileProviderOrganizationResponseModel.cs | 1 + src/Api/Models/Response/PlanResponseModel.cs | 2 + .../AdminConsole/Entities/Organization.cs | 6 + .../Data/Organizations/OrganizationAbility.cs | 2 + .../OrganizationUserOrganizationDetails.cs | 1 + .../ProviderUserOrganizationDetails.cs | 1 + .../CloudOrganizationSignUpCommand.cs | 3 +- .../Implementations/OrganizationService.cs | 2 + src/Core/Billing/Licenses/LicenseConstants.cs | 1 + .../OrganizationLicenseClaimsFactory.cs | 1 + src/Core/Billing/Models/StaticStore/Plan.cs | 1 + .../Models/Business/OrganizationLicense.cs | 5 +- .../UpgradeOrganizationPlanCommand.cs | 1 + src/Core/Resources/SharedResources.en.resx | 56 +- .../Repositories/OrganizationRepository.cs | 1 + ...izationUserOrganizationDetailsViewQuery.cs | 3 +- ...roviderUserOrganizationDetailsViewQuery.cs | 3 +- .../Stored Procedures/Organization_Create.sql | 3 + .../Organization_ReadAbilities.sql | 1 + .../Stored Procedures/Organization_Update.sql | 2 + src/Sql/dbo/Tables/Organization.sql | 1 + ...rganizationUserOrganizationDetailsView.sql | 3 +- src/Sql/dbo/Views/OrganizationView.sql | 4 +- ...derUserProviderOrganizationDetailsView.sql | 3 +- .../OrganizationLicenseFileFixtures.cs | 3 +- .../UpdateOrganizationLicenseCommandTests.cs | 3 +- ...ddUseOrganizationDomainsToOrganization.sql | 364 ++ ...13-01_AddUseOrganizationDomainsToViews.sql | 131 + ...AddUseOrganizationDomainsDataMigration.sql | 9 + ...025-05-13_00_AddUseOrganizationDomains.sql | 3 + ...1140_AddUseOrganizationDomains.Designer.cs | 3115 ++++++++++++++++ ...0250513151140_AddUseOrganizationDomains.cs | 26 + ...513151141_AddUseOrganizationDomainsData.cs | 23 + .../DatabaseContextModelSnapshot.cs | 3 + util/MySqlMigrations/MySqlMigrations.csproj | 3 +- ...25-05-13_00_AddUseOrganizationDomains.psql | 3 + ...1148_AddUseOrganizationDomains.Designer.cs | 3121 +++++++++++++++++ ...0250513151148_AddUseOrganizationDomains.cs | 26 + ...513151149_AddUseOrganizationDomainsData.cs | 25 + .../DatabaseContextModelSnapshot.cs | 3 + .../PostgresMigrations.csproj | 1 + ...025-05-13_00_AddUseOrganizationDomains.sql | 3 + ...1144_AddUseOrganizationDomains.Designer.cs | 3104 ++++++++++++++++ ...0250513151144_AddUseOrganizationDomains.cs | 26 + ...513151145_AddUseOrganizationDomainsData.cs | 25 + .../DatabaseContextModelSnapshot.cs | 3 + util/SqliteMigrations/SqliteMigrations.csproj | 65 +- 53 files changed, 10139 insertions(+), 72 deletions(-) create mode 100644 util/Migrator/DbScripts/2025-05-13-00_AddUseOrganizationDomainsToOrganization.sql create mode 100644 util/Migrator/DbScripts/2025-05-13-01_AddUseOrganizationDomainsToViews.sql create mode 100644 util/Migrator/DbScripts/2025-05-13-02_AddUseOrganizationDomainsDataMigration.sql create mode 100644 util/MySqlMigrations/HelperScripts/2025-05-13_00_AddUseOrganizationDomains.sql create mode 100644 util/MySqlMigrations/Migrations/20250513151140_AddUseOrganizationDomains.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20250513151140_AddUseOrganizationDomains.cs create mode 100644 util/MySqlMigrations/Migrations/20250513151141_AddUseOrganizationDomainsData.cs create mode 100644 util/PostgresMigrations/HelperScripts/2025-05-13_00_AddUseOrganizationDomains.psql create mode 100644 util/PostgresMigrations/Migrations/20250513151148_AddUseOrganizationDomains.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20250513151148_AddUseOrganizationDomains.cs create mode 100644 util/PostgresMigrations/Migrations/20250513151149_AddUseOrganizationDomainsData.cs create mode 100644 util/SqliteMigrations/HelperScripts/2025-05-13_00_AddUseOrganizationDomains.sql create mode 100644 util/SqliteMigrations/Migrations/20250513151144_AddUseOrganizationDomains.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20250513151144_AddUseOrganizationDomains.cs create mode 100644 util/SqliteMigrations/Migrations/20250513151145_AddUseOrganizationDomainsData.cs diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index cb163f400a..6eb81b5956 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -462,6 +462,7 @@ public class OrganizationsController : Controller organization.UsersGetPremium = model.UsersGetPremium; organization.UseSecretsManager = model.UseSecretsManager; organization.UseRiskInsights = model.UseRiskInsights; + organization.UseOrganizationDomains = model.UseOrganizationDomains; organization.UseAdminSponsoredFamilies = model.UseAdminSponsoredFamilies; //secrets diff --git a/src/Admin/AdminConsole/Models/OrganizationEditModel.cs b/src/Admin/AdminConsole/Models/OrganizationEditModel.cs index 6af6c1b50a..c79124688e 100644 --- a/src/Admin/AdminConsole/Models/OrganizationEditModel.cs +++ b/src/Admin/AdminConsole/Models/OrganizationEditModel.cs @@ -102,7 +102,7 @@ public class OrganizationEditModel : OrganizationViewModel MaxAutoscaleSmSeats = org.MaxAutoscaleSmSeats; SmServiceAccounts = org.SmServiceAccounts; MaxAutoscaleSmServiceAccounts = org.MaxAutoscaleSmServiceAccounts; - + UseOrganizationDomains = org.UseOrganizationDomains; _plans = plans; } @@ -186,6 +186,8 @@ public class OrganizationEditModel : OrganizationViewModel public int? SmServiceAccounts { get; set; } [Display(Name = "Max Autoscale Machine Accounts")] public int? MaxAutoscaleSmServiceAccounts { get; set; } + [Display(Name = "Use Organization Domains")] + public bool UseOrganizationDomains { get; set; } /** * Creates a Plan[] object for use in Javascript @@ -215,6 +217,7 @@ public class OrganizationEditModel : OrganizationViewModel Has2fa = p.Has2fa, HasApi = p.HasApi, HasSso = p.HasSso, + HasOrganizationDomains = p.HasOrganizationDomains, HasKeyConnector = p.HasKeyConnector, HasScim = p.HasScim, HasResetPassword = p.HasResetPassword, @@ -315,6 +318,7 @@ public class OrganizationEditModel : OrganizationViewModel existingOrganization.MaxAutoscaleSmSeats = MaxAutoscaleSmSeats; existingOrganization.SmServiceAccounts = SmServiceAccounts; existingOrganization.MaxAutoscaleSmServiceAccounts = MaxAutoscaleSmServiceAccounts; + existingOrganization.UseOrganizationDomains = UseOrganizationDomains; return existingOrganization; } } diff --git a/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml b/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml index 7b19b19939..267264a38f 100644 --- a/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml +++ b/src/Admin/AdminConsole/Views/Shared/_OrganizationForm.cshtml @@ -124,6 +124,10 @@ +
    + + +
    diff --git a/src/Admin/AdminConsole/Views/Shared/_OrganizationFormScripts.cshtml b/src/Admin/AdminConsole/Views/Shared/_OrganizationFormScripts.cshtml index 98d4c0d900..0ce25c700f 100644 --- a/src/Admin/AdminConsole/Views/Shared/_OrganizationFormScripts.cshtml +++ b/src/Admin/AdminConsole/Views/Shared/_OrganizationFormScripts.cshtml @@ -69,6 +69,7 @@ document.getElementById('@(nameof(Model.UseGroups))').checked = plan.hasGroups; document.getElementById('@(nameof(Model.UsePolicies))').checked = plan.hasPolicies; document.getElementById('@(nameof(Model.UseSso))').checked = plan.hasSso; + document.getElementById('@(nameof(Model.UseOrganizationDomains))').checked = hasOrganizationDomains; document.getElementById('@(nameof(Model.UseScim))').checked = plan.hasScim; document.getElementById('@(nameof(Model.UseDirectory))').checked = plan.hasDirectory; document.getElementById('@(nameof(Model.UseEvents))').checked = plan.hasEvents; diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs index a14e3efb51..95754598b9 100644 --- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationResponseModel.cs @@ -64,6 +64,7 @@ public class OrganizationResponseModel : ResponseModel LimitItemDeletion = organization.LimitItemDeletion; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; UseRiskInsights = organization.UseRiskInsights; + UseOrganizationDomains = organization.UseOrganizationDomains; UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies; } @@ -111,6 +112,7 @@ public class OrganizationResponseModel : ResponseModel public bool LimitItemDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool UseRiskInsights { get; set; } + public bool UseOrganizationDomains { get; set; } public bool UseAdminSponsoredFamilies { get; set; } } diff --git a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs index 259ce3e795..cb0ab62fd1 100644 --- a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs @@ -73,6 +73,7 @@ public class ProfileOrganizationResponseModel : ResponseModel AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; UserIsClaimedByOrganization = organizationIdsClaimingUser.Contains(organization.OrganizationId); UseRiskInsights = organization.UseRiskInsights; + UseOrganizationDomains = organization.UseOrganizationDomains; UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies; if (organization.SsoConfig != null) @@ -153,6 +154,7 @@ public class ProfileOrganizationResponseModel : ResponseModel /// public bool UserIsClaimedByOrganization { get; set; } public bool UseRiskInsights { get; set; } + public bool UseOrganizationDomains { get; set; } public bool UseAdminSponsoredFamilies { get; set; } public bool IsAdminInitiated { get; set; } } diff --git a/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs index 5d5e1f9b85..24b6fed704 100644 --- a/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs +++ b/src/Api/AdminConsole/Models/Response/ProfileProviderOrganizationResponseModel.cs @@ -50,6 +50,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo LimitItemDeletion = organization.LimitItemDeletion; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; UseRiskInsights = organization.UseRiskInsights; + UseOrganizationDomains = organization.UseOrganizationDomains; UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies; } } diff --git a/src/Api/Models/Response/PlanResponseModel.cs b/src/Api/Models/Response/PlanResponseModel.cs index 74bcb59661..f48a06b4ec 100644 --- a/src/Api/Models/Response/PlanResponseModel.cs +++ b/src/Api/Models/Response/PlanResponseModel.cs @@ -32,6 +32,7 @@ public class PlanResponseModel : ResponseModel HasTotp = plan.HasTotp; Has2fa = plan.Has2fa; HasSso = plan.HasSso; + HasOrganizationDomains = plan.HasOrganizationDomains; HasResetPassword = plan.HasResetPassword; UsersGetPremium = plan.UsersGetPremium; UpgradeSortOrder = plan.UpgradeSortOrder; @@ -71,6 +72,7 @@ public class PlanResponseModel : ResponseModel public bool Has2fa { get; set; } public bool HasApi { get; set; } public bool HasSso { get; set; } + public bool HasOrganizationDomains { get; set; } public bool HasResetPassword { get; set; } public bool UsersGetPremium { get; set; } diff --git a/src/Core/AdminConsole/Entities/Organization.cs b/src/Core/AdminConsole/Entities/Organization.cs index 17d9847574..4b7d497277 100644 --- a/src/Core/AdminConsole/Entities/Organization.cs +++ b/src/Core/AdminConsole/Entities/Organization.cs @@ -114,6 +114,11 @@ public class Organization : ITableObject, IStorableSubscriber, IRevisable, ///
    public bool UseRiskInsights { get; set; } + /// + /// If true, the organization can claim domains, which unlocks additional enterprise features + /// + public bool UseOrganizationDomains { get; set; } + /// /// If set to true, admins can initiate organization-issued sponsorships. /// @@ -319,5 +324,6 @@ public class Organization : ITableObject, IStorableSubscriber, IRevisable, SmSeats = license.SmSeats; SmServiceAccounts = license.SmServiceAccounts; UseRiskInsights = license.UseRiskInsights; + UseOrganizationDomains = license.UseOrganizationDomains; } } diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs index d27bf40994..ae91f204e3 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationAbility.cs @@ -26,6 +26,7 @@ public class OrganizationAbility LimitItemDeletion = organization.LimitItemDeletion; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; UseRiskInsights = organization.UseRiskInsights; + UseOrganizationDomains = organization.UseOrganizationDomains; UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies; } @@ -46,5 +47,6 @@ public class OrganizationAbility public bool LimitItemDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool UseRiskInsights { get; set; } + public bool UseOrganizationDomains { get; set; } public bool UseAdminSponsoredFamilies { get; set; } } diff --git a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs index a804dc0f6a..8de422ee31 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs @@ -59,6 +59,7 @@ public class OrganizationUserOrganizationDetails public bool LimitItemDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool UseRiskInsights { get; set; } + public bool UseOrganizationDomains { get; set; } public bool UseAdminSponsoredFamilies { get; set; } public bool? IsAdminInitiated { get; set; } } diff --git a/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs index 8717a8f008..4621de8268 100644 --- a/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Provider/ProviderUserOrganizationDetails.cs @@ -45,6 +45,7 @@ public class ProviderUserOrganizationDetails public bool LimitItemDeletion { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool UseRiskInsights { get; set; } + public bool UseOrganizationDomains { get; set; } public bool UseAdminSponsoredFamilies { get; set; } public ProviderType ProviderType { get; set; } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs index 60e090de2a..7449628ed0 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/CloudOrganizationSignUpCommand.cs @@ -104,7 +104,8 @@ public class CloudOrganizationSignUpCommand( RevisionDate = DateTime.UtcNow, Status = OrganizationStatusType.Created, UsePasswordManager = true, - UseSecretsManager = signup.UseSecretsManager + UseSecretsManager = signup.UseSecretsManager, + UseOrganizationDomains = plan.HasOrganizationDomains, }; if (signup.UseSecretsManager) diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 5c7e5e29ed..3a9fcff847 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -449,6 +449,7 @@ public class OrganizationService : IOrganizationService MaxStorageGb = 1, UsePolicies = plan.HasPolicies, UseSso = plan.HasSso, + UseOrganizationDomains = plan.HasOrganizationDomains, UseGroups = plan.HasGroups, UseEvents = plan.HasEvents, UseDirectory = plan.HasDirectory, @@ -570,6 +571,7 @@ public class OrganizationService : IOrganizationService SmSeats = license.SmSeats, SmServiceAccounts = license.SmServiceAccounts, UseRiskInsights = license.UseRiskInsights, + UseOrganizationDomains = license.UseOrganizationDomains, }; var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false); diff --git a/src/Core/Billing/Licenses/LicenseConstants.cs b/src/Core/Billing/Licenses/LicenseConstants.cs index 513578f43e..cdfac76614 100644 --- a/src/Core/Billing/Licenses/LicenseConstants.cs +++ b/src/Core/Billing/Licenses/LicenseConstants.cs @@ -42,6 +42,7 @@ public static class OrganizationLicenseConstants public const string ExpirationWithoutGracePeriod = nameof(ExpirationWithoutGracePeriod); public const string Trial = nameof(Trial); public const string UseAdminSponsoredFamilies = nameof(UseAdminSponsoredFamilies); + public const string UseOrganizationDomains = nameof(UseOrganizationDomains); } public static class UserLicenseConstants diff --git a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs index 6819d3cc0b..b3f2ab4ec9 100644 --- a/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs +++ b/src/Core/Billing/Licenses/Services/Implementations/OrganizationLicenseClaimsFactory.cs @@ -54,6 +54,7 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory(nameof(SmSeats)); var smServiceAccounts = claimsPrincipal.GetValue(nameof(SmServiceAccounts)); var useAdminSponsoredFamilies = claimsPrincipal.GetValue(nameof(UseAdminSponsoredFamilies)); + var useOrganizationDomains = claimsPrincipal.GetValue(nameof(UseOrganizationDomains)); return issued <= DateTime.UtcNow && expires >= DateTime.UtcNow && @@ -473,7 +475,8 @@ public class OrganizationLicense : ILicense usePasswordManager == organization.UsePasswordManager && smSeats == organization.SmSeats && smServiceAccounts == organization.SmServiceAccounts && - useAdminSponsoredFamilies == organization.UseAdminSponsoredFamilies; + useAdminSponsoredFamilies == organization.UseAdminSponsoredFamilies && + useOrganizationDomains == organization.UseOrganizationDomains; } diff --git a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs index 09b766e885..cb37e478f7 100644 --- a/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs +++ b/src/Core/OrganizationFeatures/OrganizationSubscriptions/UpgradeOrganizationPlanCommand.cs @@ -263,6 +263,7 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand organization.Use2fa = newPlan.Has2fa; organization.UseApi = newPlan.HasApi; organization.UseSso = newPlan.HasSso; + organization.UseOrganizationDomains = newPlan.HasOrganizationDomains; organization.UseKeyConnector = newPlan.HasKeyConnector; organization.UseScim = newPlan.HasScim; organization.UseResetPassword = newPlan.HasResetPassword; diff --git a/src/Core/Resources/SharedResources.en.resx b/src/Core/Resources/SharedResources.en.resx index 3ef0b54efe..90a791222f 100644 --- a/src/Core/Resources/SharedResources.en.resx +++ b/src/Core/Resources/SharedResources.en.resx @@ -1,17 +1,17 @@ - + - diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs index 91e29c1b52..f83f7b70b6 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationRepository.cs @@ -107,6 +107,7 @@ public class OrganizationRepository : Repository +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20250513151140_AddUseOrganizationDomains")] + partial class AddUseOrganizationDomains + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .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("LimitCollectionCreation") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("LimitItemDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("int"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property("Plan") + .IsRequired() + .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("SmSeats") + .HasColumnType("int"); + + b.Property("SmServiceAccounts") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseCustomPermissions") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UseOrganizationDomains") + .HasColumnType("tinyint(1)"); + + b.Property("UsePasswordManager") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property("UseRiskInsights") + .HasColumnType("tinyint(1)"); + + b.Property("UseScim") + .HasColumnType("tinyint(1)"); + + b.Property("UseSecretsManager") + .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.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Template") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.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") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasColumnType("longtext"); + + b.Property("BillingPhone") + .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("DiscountId") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasColumnType("longtext"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("varchar(25)"); + + b.Property("Approved") + .HasColumnType("tinyint(1)"); + + b.Property("AuthenticationDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MasterPasswordHash") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ResponseDate") + .HasColumnType("datetime(6)"); + + b.Property("ResponseDeviceId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ConsumedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AaGuid") + .HasColumnType("char(36)"); + + b.Property("Counter") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SupportsPrf") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AssignedSeats") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Total") + .HasColumnType("decimal(65,30)"); + + b.Property("UsedSeats") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllocatedSeats") + .HasColumnType("int"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("PurchasedSeats") + .HasColumnType("int"); + + b.Property("SeatMinimum") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("varchar(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAtTime") + .HasColumnType("datetime(6)"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longblob"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.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") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Active") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("longtext"); + + b.Property("EncryptedPublicKey") + .HasColumnType("longtext"); + + b.Property("EncryptedUserKey") + .HasColumnType("longtext"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .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("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.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("DomainName") + .HasColumnType("longtext"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InstallationId") + .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("SecretId") + .HasColumnType("char(36)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SystemUser") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + 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("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("JobRunCount") + .HasColumnType("int"); + + b.Property("LastCheckedDate") + .HasColumnType("datetime(6)"); + + b.Property("NextRunDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VerifiedDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("IsAdminInitiated") + .HasColumnType("tinyint(1)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessSecretsManager") + .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("smallint"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + 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("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .IsRequired() + .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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.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("ProviderId") + .HasColumnType("char(36)"); + + 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("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.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("AvatarColor") + .HasMaxLength(7) + .HasColumnType("varchar(7)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .IsRequired() + .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("FailedLoginCount") + .HasColumnType("int"); + + 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("KdfMemory") + .HasColumnType("int"); + + b.Property("KdfParallelism") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LastEmailChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastFailedLoginDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKdfChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("datetime(6)"); + + 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("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("VerifyDevices") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("varchar(3000)"); + + b.Property("ClientType") + .HasColumnType("tinyint unsigned"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Global") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Priority") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("TaskId") + .HasColumnType("char(36)"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("NotificationId") + .HasColumnType("char(36)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("ReadDate") + .HasColumnType("datetime(6)"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("LastActivityDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar(34)"); + + b.Property("Read") + .HasColumnType("tinyint(1)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Write") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpireAt") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Uri") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.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("Key") + .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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("char(36)"); + + b.Property("SecretsId") + .HasColumnType("char(36)"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/MySqlMigrations/Migrations/20250513151140_AddUseOrganizationDomains.cs b/util/MySqlMigrations/Migrations/20250513151140_AddUseOrganizationDomains.cs new file mode 100644 index 0000000000..3f363d5f2c --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250513151140_AddUseOrganizationDomains.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class AddUseOrganizationDomains : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UseOrganizationDomains", + table: "Organization", + type: "tinyint(1)", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + throw new Exception("Irreversible migration."); + } +} diff --git a/util/MySqlMigrations/Migrations/20250513151141_AddUseOrganizationDomainsData.cs b/util/MySqlMigrations/Migrations/20250513151141_AddUseOrganizationDomainsData.cs new file mode 100644 index 0000000000..e5ec2538bb --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250513151141_AddUseOrganizationDomainsData.cs @@ -0,0 +1,23 @@ +using Bit.Core.Utilities; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class AddUseOrganizationDomainsData : Migration +{ + private const string _addUseOrganizationDomainsMigrationScript = "MySqlMigrations.HelperScripts.2025-05-13_00_AddUseOrganizationDomains.sql"; + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(CoreHelpers.GetEmbeddedResourceContentsAsync(_addUseOrganizationDomainsMigrationScript)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + throw new Exception("Irreversible migration"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index 8addf3f1dd..98768e0447 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -185,6 +185,9 @@ namespace Bit.MySqlMigrations.Migrations b.Property("UseKeyConnector") .HasColumnType("tinyint(1)"); + b.Property("UseOrganizationDomains") + .HasColumnType("tinyint(1)"); + b.Property("UsePasswordManager") .HasColumnType("tinyint(1)"); diff --git a/util/MySqlMigrations/MySqlMigrations.csproj b/util/MySqlMigrations/MySqlMigrations.csproj index f6739f5b68..641ad90924 100644 --- a/util/MySqlMigrations/MySqlMigrations.csproj +++ b/util/MySqlMigrations/MySqlMigrations.csproj @@ -1,4 +1,4 @@ - + 9f1cd3e0-70f2-4921-8068-b2538fd7c3f7 @@ -32,5 +32,6 @@ +
    diff --git a/util/PostgresMigrations/HelperScripts/2025-05-13_00_AddUseOrganizationDomains.psql b/util/PostgresMigrations/HelperScripts/2025-05-13_00_AddUseOrganizationDomains.psql new file mode 100644 index 0000000000..befdf36558 --- /dev/null +++ b/util/PostgresMigrations/HelperScripts/2025-05-13_00_AddUseOrganizationDomains.psql @@ -0,0 +1,3 @@ +UPDATE "Organization" +SET "UseOrganizationDomains" = "UseSso" +WHERE "UseSso" IS true diff --git a/util/PostgresMigrations/Migrations/20250513151148_AddUseOrganizationDomains.Designer.cs b/util/PostgresMigrations/Migrations/20250513151148_AddUseOrganizationDomains.Designer.cs new file mode 100644 index 0000000000..895306bb58 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250513151148_AddUseOrganizationDomains.Designer.cs @@ -0,0 +1,3121 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20250513151148_AddUseOrganizationDomains")] + partial class AddUseOrganizationDomains + { + /// + 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("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .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 with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with 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("LimitCollectionCreation") + .HasColumnType("boolean"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("boolean"); + + b.Property("LimitItemDeletion") + .HasColumnType("boolean"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("integer"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp with time zone"); + + b.Property("Plan") + .IsRequired() + .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 with time zone"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("SelfHost") + .HasColumnType("boolean"); + + b.Property("SmSeats") + .HasColumnType("integer"); + + b.Property("SmServiceAccounts") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseCustomPermissions") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UseOrganizationDomains") + .HasColumnType("boolean"); + + b.Property("UsePasswordManager") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .HasColumnType("boolean"); + + b.Property("UseRiskInsights") + .HasColumnType("boolean"); + + b.Property("UseScim") + .HasColumnType("boolean"); + + b.Property("UseSecretsManager") + .HasColumnType("boolean"); + + b.Property("UseSso") + .HasColumnType("boolean"); + + b.Property("UseTotp") + .HasColumnType("boolean"); + + b.Property("UsersGetPremium") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Id", "Enabled"), new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Template") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasColumnType("text"); + + b.Property("BillingPhone") + .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 with time zone"); + + b.Property("DiscountId") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasColumnType("text"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with 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 with 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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.Property("Approved") + .HasColumnType("boolean"); + + b.Property("AuthenticationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MasterPasswordHash") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("smallint"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ResponseDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ResponseDeviceId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with 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 with time zone"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with 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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ConsumedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with 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") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("OrganizationId", "ExternalId"), new[] { "UserId" }); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AaGuid") + .HasColumnType("uuid"); + + b.Property("Counter") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SupportsPrf") + .HasColumnType("boolean"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssignedSeats") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UsedSeats") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllocatedSeats") + .HasColumnType("integer"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("PurchasedSeats") + .HasColumnType("integer"); + + b.Property("SeatMinimum") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("character varying(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAtTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("text"); + + b.Property("EncryptedPublicKey") + .HasColumnType("text"); + + b.Property("EncryptedUserKey") + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.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 with time zone"); + + b.Property("DeviceType") + .HasColumnType("smallint"); + + b.Property("DomainName") + .HasColumnType("text"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("InstallationId") + .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("SecretId") + .HasColumnType("uuid"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SystemUser") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("JobRunCount") + .HasColumnType("integer"); + + b.Property("LastCheckedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextRunDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerifiedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAdminInitiated") + .HasColumnType("boolean"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessSecretsManager") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp with 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 with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with 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 with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .IsRequired() + .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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp with 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("ProviderId") + .HasColumnType("uuid"); + + 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("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("character varying(7)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Culture") + .IsRequired() + .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("FailedLoginCount") + .HasColumnType("integer"); + + 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("KdfMemory") + .HasColumnType("integer"); + + b.Property("KdfParallelism") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LastEmailChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastFailedLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKdfChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("timestamp with time zone"); + + 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 with time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with 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("UsesKeyConnector") + .HasColumnType("boolean"); + + b.Property("VerifyDevices") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("character varying(3000)"); + + b.Property("ClientType") + .HasColumnType("smallint"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Global") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Priority") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskId") + .HasColumnType("uuid"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("NotificationId") + .HasColumnType("uuid"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReadDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("Read") + .HasColumnType("boolean"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Write") + .HasColumnType("boolean"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Uri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("uuid"); + + b.Property("SecretsId") + .HasColumnType("uuid"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/PostgresMigrations/Migrations/20250513151148_AddUseOrganizationDomains.cs b/util/PostgresMigrations/Migrations/20250513151148_AddUseOrganizationDomains.cs new file mode 100644 index 0000000000..130bbd38d8 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250513151148_AddUseOrganizationDomains.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class AddUseOrganizationDomains : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UseOrganizationDomains", + table: "Organization", + type: "boolean", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + throw new Exception("Irreversible migration."); + } +} diff --git a/util/PostgresMigrations/Migrations/20250513151149_AddUseOrganizationDomainsData.cs b/util/PostgresMigrations/Migrations/20250513151149_AddUseOrganizationDomainsData.cs new file mode 100644 index 0000000000..d525c8513f --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250513151149_AddUseOrganizationDomainsData.cs @@ -0,0 +1,25 @@ +using Bit.Core.Utilities; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class AddUseOrganizationDomainsData : Migration +{ + + private const string _addUseOrganizationDomainsMigrationScript = "PostgresMigrations.HelperScripts.2025-05-13_00_AddUseOrganizationDomains.psql"; + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(CoreHelpers.GetEmbeddedResourceContentsAsync(_addUseOrganizationDomainsMigrationScript)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + throw new Exception("Irreversible migration."); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index bd9c99ff80..736f01c95a 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -187,6 +187,9 @@ namespace Bit.PostgresMigrations.Migrations b.Property("UseKeyConnector") .HasColumnType("boolean"); + b.Property("UseOrganizationDomains") + .HasColumnType("boolean"); + b.Property("UsePasswordManager") .HasColumnType("boolean"); diff --git a/util/PostgresMigrations/PostgresMigrations.csproj b/util/PostgresMigrations/PostgresMigrations.csproj index d446f0597a..3496ff67c1 100644 --- a/util/PostgresMigrations/PostgresMigrations.csproj +++ b/util/PostgresMigrations/PostgresMigrations.csproj @@ -27,5 +27,6 @@ + diff --git a/util/SqliteMigrations/HelperScripts/2025-05-13_00_AddUseOrganizationDomains.sql b/util/SqliteMigrations/HelperScripts/2025-05-13_00_AddUseOrganizationDomains.sql new file mode 100644 index 0000000000..1d501246db --- /dev/null +++ b/util/SqliteMigrations/HelperScripts/2025-05-13_00_AddUseOrganizationDomains.sql @@ -0,0 +1,3 @@ +UPDATE [Organization] +SET [UseOrganizationDomains] = [UseSso] +WHERE [UseSso] = 1 diff --git a/util/SqliteMigrations/Migrations/20250513151144_AddUseOrganizationDomains.Designer.cs b/util/SqliteMigrations/Migrations/20250513151144_AddUseOrganizationDomains.Designer.cs new file mode 100644 index 0000000000..5902f5f9b6 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250513151144_AddUseOrganizationDomains.Designer.cs @@ -0,0 +1,3104 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20250513151144_AddUseOrganizationDomains")] + partial class AddUseOrganizationDomains + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LimitCollectionCreation") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("INTEGER"); + + b.Property("LimitItemDeletion") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("MaxCollections") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("TEXT"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("SelfHost") + .HasColumnType("INTEGER"); + + b.Property("SmSeats") + .HasColumnType("INTEGER"); + + b.Property("SmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("Use2fa") + .HasColumnType("INTEGER"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("INTEGER"); + + b.Property("UseApi") + .HasColumnType("INTEGER"); + + b.Property("UseCustomPermissions") + .HasColumnType("INTEGER"); + + b.Property("UseDirectory") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.Property("UseGroups") + .HasColumnType("INTEGER"); + + b.Property("UseKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("UseOrganizationDomains") + .HasColumnType("INTEGER"); + + b.Property("UsePasswordManager") + .HasColumnType("INTEGER"); + + b.Property("UsePolicies") + .HasColumnType("INTEGER"); + + b.Property("UseResetPassword") + .HasColumnType("INTEGER"); + + b.Property("UseRiskInsights") + .HasColumnType("INTEGER"); + + b.Property("UseScim") + .HasColumnType("INTEGER"); + + b.Property("UseSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("UseSso") + .HasColumnType("INTEGER"); + + b.Property("UseTotp") + .HasColumnType("INTEGER"); + + b.Property("UsersGetPremium") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Template") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BillingEmail") + .HasColumnType("TEXT"); + + b.Property("BillingPhone") + .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("TEXT"); + + b.Property("DiscountId") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("TEXT"); + + b.Property("Approved") + .HasColumnType("INTEGER"); + + b.Property("AuthenticationDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHash") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceType") + .HasColumnType("INTEGER"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ResponseDate") + .HasColumnType("TEXT"); + + b.Property("ResponseDeviceId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("GranteeId") + .HasColumnType("TEXT"); + + b.Property("GrantorId") + .HasColumnType("TEXT"); + + b.Property("KeyEncrypted") + .HasColumnType("TEXT"); + + b.Property("LastNotificationDate") + .HasColumnType("TEXT"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WaitTimeDays") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ConsumedDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AaGuid") + .HasColumnType("TEXT"); + + b.Property("Counter") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SupportsPrf") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AssignedSeats") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("TEXT"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Total") + .HasColumnType("TEXT"); + + b.Property("UsedSeats") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllocatedSeats") + .HasColumnType("INTEGER"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("PurchasedSeats") + .HasColumnType("INTEGER"); + + b.Property("SeatMinimum") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("TEXT"); + + b.Property("AbsoluteExpiration") + .HasColumnType("TEXT"); + + b.Property("ExpiresAtTime") + .HasColumnType("TEXT"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("BLOB"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasColumnType("TEXT"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActingUserId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasColumnType("INTEGER"); + + b.Property("DomainName") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("PolicyId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderUserId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SystemUser") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Config") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("JobRunCount") + .HasColumnType("INTEGER"); + + b.Property("LastCheckedDate") + .HasColumnType("TEXT"); + + b.Property("NextRunDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VerifiedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("IsAdminInitiated") + .HasColumnType("INTEGER"); + + b.Property("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PlanSponsorshipType") + .HasColumnType("INTEGER"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("ToDelete") + .HasColumnType("INTEGER"); + + b.Property("ValidUntil") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletionDate") + .HasColumnType("TEXT"); + + b.Property("Disabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("HideEmail") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MaxAccessCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Rate") + .HasColumnType("TEXT"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaymentMethodType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Refunded") + .HasColumnType("INTEGER"); + + b.Property("RefundedAmount") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountRevisionDate") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailVerified") + .HasColumnType("INTEGER"); + + b.Property("EquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("FailedLoginCount") + .HasColumnType("INTEGER"); + + b.Property("ForcePasswordReset") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Kdf") + .HasColumnType("INTEGER"); + + b.Property("KdfIterations") + .HasColumnType("INTEGER"); + + b.Property("KdfMemory") + .HasColumnType("INTEGER"); + + b.Property("KdfParallelism") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("LastEmailChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastFailedLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastKdfChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Premium") + .HasColumnType("INTEGER"); + + b.Property("PremiumExpirationDate") + .HasColumnType("TEXT"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("VerifyDevices") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("TEXT"); + + b.Property("ClientType") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Global") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("TaskId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("NotificationId") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("ReadDate") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("TEXT"); + + b.Property("Read") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Write") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpireAt") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Attachments") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Favorites") + .HasColumnType("TEXT"); + + b.Property("Folders") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Reprompt") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("TEXT"); + + b.Property("SecretsId") + .HasColumnType("TEXT"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/SqliteMigrations/Migrations/20250513151144_AddUseOrganizationDomains.cs b/util/SqliteMigrations/Migrations/20250513151144_AddUseOrganizationDomains.cs new file mode 100644 index 0000000000..50bbec5902 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250513151144_AddUseOrganizationDomains.cs @@ -0,0 +1,26 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class AddUseOrganizationDomains : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "UseOrganizationDomains", + table: "Organization", + type: "INTEGER", + nullable: false, + defaultValue: false); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + throw new Exception("Irreversible migration."); + } +} diff --git a/util/SqliteMigrations/Migrations/20250513151145_AddUseOrganizationDomainsData.cs b/util/SqliteMigrations/Migrations/20250513151145_AddUseOrganizationDomainsData.cs new file mode 100644 index 0000000000..248b306d97 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250513151145_AddUseOrganizationDomainsData.cs @@ -0,0 +1,25 @@ +using Bit.Core.Utilities; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class AddUseOrganizationDomainsData : Migration +{ + private const string _addUseOrganizationDomainsMigrationScript = "SqliteMigrations.HelperScripts.2025-05-13_00_AddUseOrganizationDomains.sql"; + + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(CoreHelpers.GetEmbeddedResourceContentsAsync(_addUseOrganizationDomainsMigrationScript)); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + throw new Exception("Irreversible migration."); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index 5e82f311a8..1bc1ffbc58 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -180,6 +180,9 @@ namespace Bit.SqliteMigrations.Migrations b.Property("UseKeyConnector") .HasColumnType("INTEGER"); + b.Property("UseOrganizationDomains") + .HasColumnType("INTEGER"); + b.Property("UsePasswordManager") .HasColumnType("INTEGER"); diff --git a/util/SqliteMigrations/SqliteMigrations.csproj b/util/SqliteMigrations/SqliteMigrations.csproj index d58498ee7a..dce863036f 100644 --- a/util/SqliteMigrations/SqliteMigrations.csproj +++ b/util/SqliteMigrations/SqliteMigrations.csproj @@ -1,32 +1,33 @@ - - - - enable - enable - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - - - - - + + + + enable + enable + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + From 3f0bd911b0aed18f3b0d828d7d927587884363ad Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 May 2025 16:14:08 +0000 Subject: [PATCH 059/114] [deps] Tools: Update MailKit to 4.12.0 (#5804) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> --- src/Core/Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 92f9396fb2..8e0e1865ab 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -34,7 +34,7 @@ - + From 95a194623b2d686147d162381545065d019a498b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 May 2025 13:13:38 -0400 Subject: [PATCH 060/114] [deps]: Update RabbitMQ.Client to 7.1.2 (#5739) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 8e0e1865ab..633c3452d9 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -63,7 +63,7 @@ - + From 07de9aa8bc85d26239d69c50c0169bb43f7ddee6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 May 2025 15:41:19 -0400 Subject: [PATCH 061/114] [deps] Auth: Update expose-loader to v5.0.1 (#5688) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bitwarden_license/src/Sso/package-lock.json | 8 ++++---- bitwarden_license/src/Sso/package.json | 2 +- src/Admin/package-lock.json | 8 ++++---- src/Admin/package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bitwarden_license/src/Sso/package-lock.json b/bitwarden_license/src/Sso/package-lock.json index 0b861365bc..98ea72c69e 100644 --- a/bitwarden_license/src/Sso/package-lock.json +++ b/bitwarden_license/src/Sso/package-lock.json @@ -15,7 +15,7 @@ }, "devDependencies": { "css-loader": "7.1.2", - "expose-loader": "5.0.0", + "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", "sass": "1.85.0", "sass-loader": "16.0.4", @@ -1083,9 +1083,9 @@ } }, "node_modules/expose-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.0.tgz", - "integrity": "sha512-BtUqYRmvx1bEY5HN6eK2I9URUZgNmN0x5UANuocaNjXSgfoDlkXt+wyEMe7i5DzDNh2BKJHPc5F4rBwEdSQX6w==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.1.tgz", + "integrity": "sha512-5YPZuszN/eWND/B+xuq5nIpb/l5TV1HYmdO6SubYtHv+HenVw9/6bn33Mm5reY8DNid7AVtbARvyUD34edfCtg==", "dev": true, "license": "MIT", "engines": { diff --git a/bitwarden_license/src/Sso/package.json b/bitwarden_license/src/Sso/package.json index d9aefafef3..289612e79a 100644 --- a/bitwarden_license/src/Sso/package.json +++ b/bitwarden_license/src/Sso/package.json @@ -14,7 +14,7 @@ }, "devDependencies": { "css-loader": "7.1.2", - "expose-loader": "5.0.0", + "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", "sass": "1.85.0", "sass-loader": "16.0.4", diff --git a/src/Admin/package-lock.json b/src/Admin/package-lock.json index 24c2466746..3d339bd80c 100644 --- a/src/Admin/package-lock.json +++ b/src/Admin/package-lock.json @@ -16,7 +16,7 @@ }, "devDependencies": { "css-loader": "7.1.2", - "expose-loader": "5.0.0", + "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", "sass": "1.85.0", "sass-loader": "16.0.4", @@ -1084,9 +1084,9 @@ } }, "node_modules/expose-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.0.tgz", - "integrity": "sha512-BtUqYRmvx1bEY5HN6eK2I9URUZgNmN0x5UANuocaNjXSgfoDlkXt+wyEMe7i5DzDNh2BKJHPc5F4rBwEdSQX6w==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/expose-loader/-/expose-loader-5.0.1.tgz", + "integrity": "sha512-5YPZuszN/eWND/B+xuq5nIpb/l5TV1HYmdO6SubYtHv+HenVw9/6bn33Mm5reY8DNid7AVtbARvyUD34edfCtg==", "dev": true, "license": "MIT", "engines": { diff --git a/src/Admin/package.json b/src/Admin/package.json index 7f3c8046a2..eed8eaf7aa 100644 --- a/src/Admin/package.json +++ b/src/Admin/package.json @@ -15,7 +15,7 @@ }, "devDependencies": { "css-loader": "7.1.2", - "expose-loader": "5.0.0", + "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", "sass": "1.85.0", "sass-loader": "16.0.4", From 97fbf219774c271feb7752b4f42dc1f1574a3aa3 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Thu, 15 May 2025 14:00:48 -0700 Subject: [PATCH 062/114] [PM-20543] - remove restrict-provider-access feature flag (#5700) * remove restrict-provider-access feature flag * remove feature flag * re-add flag * remove unnecessary tests * fix bad merge * fix bad merge * remove RestrictProviderAccess key --- .../Vault/Controllers/CiphersController.cs | 56 +---- src/Core/Constants.cs | 1 - .../Controllers/CiphersControllerTests.cs | 209 +----------------- 3 files changed, 18 insertions(+), 248 deletions(-) diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 02dace894d..4f105128ea 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -315,26 +315,10 @@ public class CiphersController : Controller { var org = _currentContext.GetOrganization(organizationId); - // If we're not an "admin", we don't need to check the ciphers - if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true })) + // If we're not an "admin" or if we're not a provider user we don't need to check the ciphers + if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true }) || await _currentContext.ProviderUserForOrgAsync(organizationId)) { - // Are we a provider user? If so, we need to be sure we're not restricted - // Once the feature flag is removed, this check can be combined with the above - if (await _currentContext.ProviderUserForOrgAsync(organizationId)) - { - // Provider is restricted from editing ciphers, so we're not an "admin" - if (_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess)) - { - return false; - } - - // Provider is unrestricted, so we're an "admin", don't return early - } - else - { - // Not a provider or admin - return false; - } + return false; } // We know we're an "admin", now check the ciphers explicitly (in case admins are restricted) @@ -350,26 +334,10 @@ public class CiphersController : Controller var org = _currentContext.GetOrganization(organizationId); - // If we're not an "admin", we don't need to check the ciphers - if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true })) + // If we're not an "admin" or if we're a provider user we don't need to check the ciphers + if (org is not ({ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or { Permissions.EditAnyCollection: true }) || await _currentContext.ProviderUserForOrgAsync(organizationId)) { - // Are we a provider user? If so, we need to be sure we're not restricted - // Once the feature flag is removed, this check can be combined with the above - if (await _currentContext.ProviderUserForOrgAsync(organizationId)) - { - // Provider is restricted from editing ciphers, so we're not an "admin" - if (_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess)) - { - return false; - } - - // Provider is unrestricted, so we're an "admin", don't return early - } - else - { - // Not a provider or admin - return false; - } + return false; } // If the user can edit all ciphers for the organization, just check they all belong to the org @@ -462,10 +430,10 @@ public class CiphersController : Controller return true; } - // Provider users can edit all ciphers if RestrictProviderAccess is disabled + // Provider users cannot edit ciphers if (await _currentContext.ProviderUserForOrgAsync(organizationId)) { - return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess); + return false; } return false; @@ -485,10 +453,10 @@ public class CiphersController : Controller return true; } - // Provider users can only access organization ciphers if RestrictProviderAccess is disabled + // Provider users cannot access organization ciphers if (await _currentContext.ProviderUserForOrgAsync(organizationId)) { - return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess); + return false; } return false; @@ -508,10 +476,10 @@ public class CiphersController : Controller return true; } - // Provider users can only access all ciphers if RestrictProviderAccess is disabled + // Provider users cannot access ciphers if (await _currentContext.ProviderUserForOrgAsync(organizationId)) { - return !_featureService.IsEnabled(FeatureFlagKeys.RestrictProviderAccess); + return false; } return false; diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index abd530b2dd..847214598c 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -193,7 +193,6 @@ public static class FeatureFlagKeys /* Vault Team */ public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge"; public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form"; - public const string RestrictProviderAccess = "restrict-provider-access"; public const string SecurityTasks = "security-tasks"; public const string CipherKeyEncryption = "cipher-key-encryption"; public const string DesktopCipherForms = "pm-18520-desktop-cipher-forms"; diff --git a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs index 0bdc6ab545..e4643f3185 100644 --- a/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs +++ b/test/Api.Test/Vault/Controllers/CiphersControllerTests.cs @@ -193,49 +193,6 @@ public class CiphersControllerTests } } - [Theory] - [BitAutoData(false)] - [BitAutoData(false)] - [BitAutoData(true)] - public async Task CanEditCiphersAsAdminAsync_Providers( - bool restrictProviders, CipherDetails cipherDetails, CurrentContextOrganization organization, Guid userId, SutProvider sutProvider - ) - { - cipherDetails.OrganizationId = organization.Id; - - // Simulate that the user is a provider for the organization - sutProvider.GetDependency().EditAnyCollection(organization.Id).Returns(true); - sutProvider.GetDependency().ProviderUserForOrgAsync(organization.Id).Returns(true); - - sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); - - sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); - sutProvider.GetDependency().GetManyByOrganizationIdAsync(organization.Id).Returns(new List { cipherDetails }); - - sutProvider.GetDependency().GetOrganizationAbilityAsync(organization.Id).Returns(new OrganizationAbility - { - Id = organization.Id, - AllowAdminAccessToAllCollectionItems = false - }); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(restrictProviders); - - // Non restricted providers should succeed - if (!restrictProviders) - { - await sutProvider.Sut.DeleteAdmin(cipherDetails.Id); - await sutProvider.GetDependency().ReceivedWithAnyArgs() - .DeleteAsync(default, default); - } - else // Otherwise, they should fail - { - await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteAdmin(cipherDetails.Id)); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .DeleteAsync(default, default); - } - - await sutProvider.GetDependency().Received().ProviderUserForOrgAsync(organization.Id); - } - [Theory] [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Admin)] @@ -456,24 +413,7 @@ public class CiphersControllerTests [Theory] [BitAutoData] - public async Task DeleteAdmin_WithProviderUser_DeletesCipher( - CipherDetails cipherDetails, Guid userId, SutProvider sutProvider) - { - cipherDetails.OrganizationId = Guid.NewGuid(); - - sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); - sutProvider.GetDependency().ProviderUserForOrgAsync(cipherDetails.OrganizationId.Value).Returns(true); - sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); - sutProvider.GetDependency().GetManyByOrganizationIdAsync(cipherDetails.OrganizationId.Value).Returns(new List { cipherDetails }); - - await sutProvider.Sut.DeleteAdmin(cipherDetails.Id); - - await sutProvider.GetDependency().Received(1).DeleteAsync(cipherDetails, userId, true); - } - - [Theory] - [BitAutoData] - public async Task DeleteAdmin_WithProviderUser_WithRestrictProviderAccessTrue_ThrowsNotFoundException( + public async Task DeleteAdmin_WithProviderUser_ThrowsNotFoundException( Cipher cipher, Guid userId, SutProvider sutProvider) { cipher.OrganizationId = Guid.NewGuid(); @@ -481,7 +421,6 @@ public class CiphersControllerTests sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency().ProviderUserForOrgAsync(cipher.OrganizationId.Value).Returns(true); sutProvider.GetDependency().GetByIdAsync(cipher.Id).Returns(cipher); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(true); await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteAdmin(cipher.Id)); } @@ -737,43 +676,13 @@ public class CiphersControllerTests [Theory] [BitAutoData] - public async Task DeleteManyAdmin_WithProviderUser_DeletesCiphers( - CipherBulkDeleteRequestModel model, Guid userId, - List ciphers, SutProvider sutProvider) - { - var organizationId = Guid.NewGuid(); - model.OrganizationId = organizationId.ToString(); - model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); - - foreach (var cipher in ciphers) - { - cipher.OrganizationId = organizationId; - } - - sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); - sutProvider.GetDependency().ProviderUserForOrgAsync(organizationId).Returns(true); - sutProvider.GetDependency().GetManyByOrganizationIdAsync(organizationId).Returns(ciphers); - - await sutProvider.Sut.DeleteManyAdmin(model); - - await sutProvider.GetDependency() - .Received(1) - .DeleteManyAsync( - Arg.Is>(ids => - ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()), - userId, organizationId, true); - } - - [Theory] - [BitAutoData] - public async Task DeleteManyAdmin_WithProviderUser_WithRestrictProviderAccessTrue_ThrowsNotFoundException( + public async Task DeleteManyAdmin_WithProviderUser_ThrowsNotFoundException( CipherBulkDeleteRequestModel model, SutProvider sutProvider) { var organizationId = Guid.NewGuid(); model.OrganizationId = organizationId.ToString(); sutProvider.GetDependency().ProviderUserForOrgAsync(organizationId).Returns(true); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(true); await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteManyAdmin(model)); } @@ -1000,24 +909,7 @@ public class CiphersControllerTests [Theory] [BitAutoData] - public async Task PutDeleteAdmin_WithProviderUser_SoftDeletesCipher( - CipherDetails cipherDetails, Guid userId, SutProvider sutProvider) - { - cipherDetails.OrganizationId = Guid.NewGuid(); - - sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); - sutProvider.GetDependency().ProviderUserForOrgAsync(cipherDetails.OrganizationId.Value).Returns(true); - sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); - sutProvider.GetDependency().GetManyByOrganizationIdAsync(cipherDetails.OrganizationId.Value).Returns(new List { cipherDetails }); - - await sutProvider.Sut.PutDeleteAdmin(cipherDetails.Id); - - await sutProvider.GetDependency().Received(1).SoftDeleteAsync(cipherDetails, userId, true); - } - - [Theory] - [BitAutoData] - public async Task PutDeleteAdmin_WithProviderUser_WithRestrictProviderAccessTrue_ThrowsNotFoundException( + public async Task PutDeleteAdmin_WithProviderUser_ThrowsNotFoundException( Cipher cipher, Guid userId, SutProvider sutProvider) { cipher.OrganizationId = Guid.NewGuid(); @@ -1025,7 +917,6 @@ public class CiphersControllerTests sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency().ProviderUserForOrgAsync(cipher.OrganizationId.Value).Returns(true); sutProvider.GetDependency().GetByIdAsync(cipher.Id).Returns(cipher); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(true); await Assert.ThrowsAsync(() => sutProvider.Sut.PutDeleteAdmin(cipher.Id)); } @@ -1272,43 +1163,13 @@ public class CiphersControllerTests [Theory] [BitAutoData] - public async Task PutDeleteManyAdmin_WithProviderUser_SoftDeletesCiphers( - CipherBulkDeleteRequestModel model, Guid userId, - List ciphers, SutProvider sutProvider) - { - var organizationId = Guid.NewGuid(); - model.OrganizationId = organizationId.ToString(); - model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); - - foreach (var cipher in ciphers) - { - cipher.OrganizationId = organizationId; - } - - sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); - sutProvider.GetDependency().ProviderUserForOrgAsync(organizationId).Returns(true); - sutProvider.GetDependency().GetManyByOrganizationIdAsync(organizationId).Returns(ciphers); - - await sutProvider.Sut.PutDeleteManyAdmin(model); - - await sutProvider.GetDependency() - .Received(1) - .SoftDeleteManyAsync( - Arg.Is>(ids => - ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count() == model.Ids.Count()), - userId, organizationId, true); - } - - [Theory] - [BitAutoData] - public async Task PutDeleteManyAdmin_WithProviderUser_WithRestrictProviderAccessTrue_ThrowsNotFoundException( + public async Task PutDeleteManyAdmin_WithProviderUser_ThrowsNotFoundException( CipherBulkDeleteRequestModel model, SutProvider sutProvider) { var organizationId = Guid.NewGuid(); model.OrganizationId = organizationId.ToString(); sutProvider.GetDependency().ProviderUserForOrgAsync(organizationId).Returns(true); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(true); await Assert.ThrowsAsync(() => sutProvider.Sut.PutDeleteManyAdmin(model)); } @@ -1546,27 +1407,7 @@ public class CiphersControllerTests [Theory] [BitAutoData] - public async Task PutRestoreAdmin_WithProviderUser_RestoresCipher( - CipherDetails cipherDetails, Guid userId, SutProvider sutProvider) - { - cipherDetails.OrganizationId = Guid.NewGuid(); - cipherDetails.Type = CipherType.Login; - cipherDetails.Data = JsonSerializer.Serialize(new CipherLoginData()); - - sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); - sutProvider.GetDependency().ProviderUserForOrgAsync(cipherDetails.OrganizationId.Value).Returns(true); - sutProvider.GetDependency().GetByIdAsync(cipherDetails.Id, userId).Returns(cipherDetails); - sutProvider.GetDependency().GetManyByOrganizationIdAsync(cipherDetails.OrganizationId.Value).Returns(new List { cipherDetails }); - - var result = await sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id); - - Assert.IsType(result); - await sutProvider.GetDependency().Received(1).RestoreAsync(cipherDetails, userId, true); - } - - [Theory] - [BitAutoData] - public async Task PutRestoreAdmin_WithProviderUser_WithRestrictProviderAccessTrue_ThrowsNotFoundException( + public async Task PutRestoreAdmin_WithProviderUser_ThrowsNotFoundException( CipherDetails cipherDetails, Guid userId, SutProvider sutProvider) { cipherDetails.OrganizationId = Guid.NewGuid(); @@ -1574,7 +1415,6 @@ public class CiphersControllerTests sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); sutProvider.GetDependency().ProviderUserForOrgAsync(cipherDetails.OrganizationId.Value).Returns(true); sutProvider.GetDependency().GetOrganizationDetailsByIdAsync(cipherDetails.Id).Returns(cipherDetails); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(true); await Assert.ThrowsAsync(() => sutProvider.Sut.PutRestoreAdmin(cipherDetails.Id)); } @@ -1896,49 +1736,12 @@ public class CiphersControllerTests [Theory] [BitAutoData] - public async Task PutRestoreManyAdmin_WithProviderUser_RestoresCiphers( - CipherBulkRestoreRequestModel model, Guid userId, - List ciphers, SutProvider sutProvider) - { - model.OrganizationId = Guid.NewGuid(); - model.Ids = ciphers.Select(c => c.Id.ToString()).ToList(); - - sutProvider.GetDependency().GetProperUserId(default).ReturnsForAnyArgs(userId); - sutProvider.GetDependency().ProviderUserForOrgAsync(model.OrganizationId).Returns(true); - sutProvider.GetDependency().GetManyByOrganizationIdAsync(model.OrganizationId).Returns(ciphers); - - var cipherOrgDetails = ciphers.Select(c => new CipherOrganizationDetails - { - Id = c.Id, - OrganizationId = model.OrganizationId - }).ToList(); - - sutProvider.GetDependency() - .RestoreManyAsync( - Arg.Any>(), - userId, model.OrganizationId, true) - .Returns(cipherOrgDetails); - - var result = await sutProvider.Sut.PutRestoreManyAdmin(model); - - Assert.NotNull(result); - await sutProvider.GetDependency() - .Received(1) - .RestoreManyAsync( - Arg.Is>(ids => - ids.All(id => model.Ids.Contains(id.ToString())) && ids.Count == model.Ids.Count()), - userId, model.OrganizationId, true); - } - - [Theory] - [BitAutoData] - public async Task PutRestoreManyAdmin_WithProviderUser_WithRestrictProviderAccessTrue_ThrowsNotFoundException( + public async Task PutRestoreManyAdmin_WithProviderUser_ThrowsNotFoundException( CipherBulkRestoreRequestModel model, SutProvider sutProvider) { model.OrganizationId = Guid.NewGuid(); sutProvider.GetDependency().ProviderUserForOrgAsync(model.OrganizationId).Returns(true); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.RestrictProviderAccess).Returns(true); await Assert.ThrowsAsync(() => sutProvider.Sut.PutRestoreManyAdmin(model)); } From bbbc7a64222feb9d4e8588fdd707d938c8218316 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Thu, 15 May 2025 19:13:55 -0400 Subject: [PATCH 063/114] Add docker related files to platform ownership (#5820) --- .github/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9f3048a340..5399bed391 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -90,6 +90,9 @@ src/Admin/Views/Tools @bitwarden/team-billing-dev .github/workflows/test-database.yml @bitwarden/team-platform-dev .github/workflows/test.yml @bitwarden/team-platform-dev **/*Platform* @bitwarden/team-platform-dev +**/.dockerignore @bitwarden/team-platform-dev +**/Dockerfile @bitwarden/team-platform-dev +**/entrypoint.sh @bitwarden/team-platform-dev # Multiple owners - DO NOT REMOVE (BRE) **/packages.lock.json From 3bf4f11c3a6aa778ddd93a79bd636869d4434d21 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Fri, 16 May 2025 13:55:13 +1000 Subject: [PATCH 064/114] Add MariaDB as development and test database (#5816) --- dev/.env.example | 1 + dev/docker-compose.yml | 14 ++++++++++++++ dev/migrate.ps1 | 16 ++++++++++++---- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/dev/.env.example b/dev/.env.example index f0aed83a59..7f049728d7 100644 --- a/dev/.env.example +++ b/dev/.env.example @@ -11,6 +11,7 @@ MAILCATCHER_PORT=1080 # Alternative databases POSTGRES_PASSWORD=SET_A_PASSWORD_HERE_123 MYSQL_ROOT_PASSWORD=SET_A_PASSWORD_HERE_123 +MARIADB_ROOT_PASSWORD=SET_A_PASSWORD_HERE_123 # IdP configuration # Complete using the values from the Manage SSO page in the web vault diff --git a/dev/docker-compose.yml b/dev/docker-compose.yml index a21f1ac6b8..601989a473 100644 --- a/dev/docker-compose.yml +++ b/dev/docker-compose.yml @@ -70,6 +70,20 @@ services: profiles: - mysql + mariadb: + image: mariadb:10 + ports: + - 4306:3306 + environment: + MARIADB_USER: maria + MARIADB_PASSWORD: ${MARIADB_ROOT_PASSWORD} + MARIADB_DATABASE: vault_dev + MARIADB_RANDOM_ROOT_PASSWORD: "true" + volumes: + - mariadb_dev_data:/var/lib/mysql + profiles: + - mariadb + idp: image: kenchan0130/simplesamlphp:1.19.8 container_name: idp diff --git a/dev/migrate.ps1 b/dev/migrate.ps1 index d129af4e6e..287a2d18ee 100755 --- a/dev/migrate.ps1 +++ b/dev/migrate.ps1 @@ -5,6 +5,7 @@ param( [switch]$all, [switch]$postgres, [switch]$mysql, + [switch]$mariadb, [switch]$mssql, [switch]$sqlite, [switch]$selfhost, @@ -15,11 +16,15 @@ param( $ErrorActionPreference = "Stop" $currentDir = Get-Location -if (!$all -and !$postgres -and !$mysql -and !$sqlite) { +function Get-IsEFDatabase { + return $postgres -or $mysql -or $mariadb -or $sqlite; +} + +if (!$all -and !$(Get-IsEFDatabase)) { $mssql = $true; } -if ($all -or $postgres -or $mysql -or $sqlite) { +if ($all -or $(Get-IsEFDatabase)) { dotnet ef *> $null if ($LASTEXITCODE -ne 0) { Write-Host "Entity Framework Core tools were not found in the dotnet global tools. Attempting to install" @@ -60,9 +65,12 @@ if ($all -or $mssql) { } Foreach ($item in @( - @($mysql, "MySQL", "MySqlMigrations", "mySql", 2), @($postgres, "PostgreSQL", "PostgresMigrations", "postgreSql", 0), - @($sqlite, "SQLite", "SqliteMigrations", "sqlite", 1) + @($sqlite, "SQLite", "SqliteMigrations", "sqlite", 1), + @($mysql, "MySQL", "MySqlMigrations", "mySql", 2), + # MariaDB shares the MySQL connection string in the server config so they are mutually exclusive in that context. + # However they can still be run independently for integration tests. + @($mariadb, "MariaDB", "MySqlMigrations", "mySql", 3) )) { if (!$item[0] -and !$all) { continue From d72d72168405e100b3b2eeb85aa1bfe099439997 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Fri, 16 May 2025 09:00:40 -0400 Subject: [PATCH 065/114] Set BU trial length to 4 (#5824) --- .../Commercial.Core/Billing/ProviderBillingService.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs index 99831aa3f1..b1eefbffe3 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs @@ -694,6 +694,13 @@ public class ProviderBillingService( customer.Metadata.ContainsKey(BraintreeCustomerIdKey) || setupIntent.IsUnverifiedBankAccount()); + int? trialPeriodDays = provider.Type switch + { + ProviderType.Msp when usePaymentMethod => 14, + ProviderType.BusinessUnit when usePaymentMethod => 4, + _ => null + }; + var subscriptionCreateOptions = new SubscriptionCreateOptions { CollectionMethod = usePaymentMethod ? @@ -707,7 +714,7 @@ public class ProviderBillingService( }, OffSession = true, ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations, - TrialPeriodDays = usePaymentMethod ? 14 : null + TrialPeriodDays = trialPeriodDays }; if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements)) From 67f745ebc4761530931c026f888f031d05370b65 Mon Sep 17 00:00:00 2001 From: bitwarden-charlie Date: Fri, 16 May 2025 08:45:04 -0500 Subject: [PATCH 066/114] chore/SRE-583 Deprecate usage of Auth-Email Header (#5709) * chore/SRE-583 Deprecate usage of Auth-Email Header * SRE-583 cleanup function and references * SRE-583 cleanup tests --------- Co-authored-by: sneakernuts <671942+sneakernuts@users.noreply.github.com> --- perf/load/helpers/auth.js | 2 - .../ResourceOwnerPasswordValidator.cs | 31 ------- .../Endpoints/IdentityServerTests.cs | 91 ++----------------- .../Endpoints/IdentityServerTwoFactorTests.cs | 10 +- .../ResourceOwnerPasswordValidatorTests.cs | 34 +------ .../Factories/IdentityApplicationFactory.cs | 4 +- .../WebApplicationFactoryExtensions.cs | 6 -- 7 files changed, 22 insertions(+), 156 deletions(-) diff --git a/perf/load/helpers/auth.js b/perf/load/helpers/auth.js index 1e225d5e49..7d7fd50678 100644 --- a/perf/load/helpers/auth.js +++ b/perf/load/helpers/auth.js @@ -40,8 +40,6 @@ export function authenticate( payload["deviceName"] = "chrome"; payload["username"] = username; payload["password"] = password; - - params.headers["Auth-Email"] = encoding.b64encode(username); } else { payload["scope"] = "api.organization"; payload["grant_type"] = "client_credentials"; diff --git a/src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs b/src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs index 68ae2ced4d..c30c94eeee 100644 --- a/src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/ResourceOwnerPasswordValidator.cs @@ -64,12 +64,6 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator var localFactory = new IdentityApplicationFactory(); var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel); - var context = await PostLoginAsync(localFactory.Server, user, requestModel.MasterPasswordHash, - context => context.SetAuthEmail(user.Email)); + var context = await PostLoginAsync(localFactory.Server, user, requestModel.MasterPasswordHash); using var body = await AssertDefaultTokenBodyAsync(context); var root = body.RootElement; @@ -72,71 +71,6 @@ public class IdentityServerTests : IClassFixture AssertUserDecryptionOptions(root); } - [Theory, BitAutoData, RegisterFinishRequestModelCustomize] - public async Task TokenEndpoint_GrantTypePassword_NoAuthEmailHeader_Fails( - RegisterFinishRequestModel requestModel) - { - requestModel.Email = "test+noauthemailheader@email.com"; - - var localFactory = new IdentityApplicationFactory(); - var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel); - - var context = await PostLoginAsync(localFactory.Server, user, requestModel.MasterPasswordHash, null); - - Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); - - var body = await AssertHelper.AssertResponseTypeIs(context); - var root = body.RootElement; - - var error = AssertHelper.AssertJsonProperty(root, "error", JsonValueKind.String).GetString(); - Assert.Equal("invalid_grant", error); - AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String); - } - - [Theory, BitAutoData, RegisterFinishRequestModelCustomize] - public async Task TokenEndpoint_GrantTypePassword_InvalidBase64AuthEmailHeader_Fails( - RegisterFinishRequestModel requestModel) - { - requestModel.Email = "test+badauthheader@email.com"; - - var localFactory = new IdentityApplicationFactory(); - var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel); - - var context = await PostLoginAsync(localFactory.Server, user, requestModel.MasterPasswordHash, - context => context.Request.Headers.Append("Auth-Email", "bad_value")); - - Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); - - var body = await AssertHelper.AssertResponseTypeIs(context); - var root = body.RootElement; - - var error = AssertHelper.AssertJsonProperty(root, "error", JsonValueKind.String).GetString(); - Assert.Equal("invalid_grant", error); - AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String); - } - - [Theory, BitAutoData, RegisterFinishRequestModelCustomize] - public async Task TokenEndpoint_GrantTypePassword_WrongAuthEmailHeader_Fails( - RegisterFinishRequestModel requestModel) - { - requestModel.Email = "test+badauthheader@email.com"; - - var localFactory = new IdentityApplicationFactory(); - var user = await localFactory.RegisterNewIdentityFactoryUserAsync(requestModel); - - var context = await PostLoginAsync(localFactory.Server, user, requestModel.MasterPasswordHash, - context => context.SetAuthEmail("bad_value")); - - Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); - - var body = await AssertHelper.AssertResponseTypeIs(context); - var root = body.RootElement; - - var error = AssertHelper.AssertJsonProperty(root, "error", JsonValueKind.String).GetString(); - Assert.Equal("invalid_grant", error); - AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String); - } - [Theory, RegisterFinishRequestModelCustomize] [BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Admin)] @@ -157,8 +91,7 @@ public class IdentityServerTests : IClassFixture await CreateOrganizationWithSsoPolicyAsync(localFactory, organizationId, user.Email, organizationUserType, ssoPolicyEnabled: false); - var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash, - context => context.SetAuthEmail(user.Email)); + var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash); Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); } @@ -184,8 +117,7 @@ public class IdentityServerTests : IClassFixture await CreateOrganizationWithSsoPolicyAsync( localFactory, organizationId, user.Email, organizationUserType, ssoPolicyEnabled: false); - var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash, - context => context.SetAuthEmail(user.Email)); + var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash); Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); } @@ -209,8 +141,7 @@ public class IdentityServerTests : IClassFixture await CreateOrganizationWithSsoPolicyAsync(localFactory, organizationId, user.Email, organizationUserType, ssoPolicyEnabled: true); - var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash, - context => context.SetAuthEmail(user.Email)); + var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash); Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); await AssertRequiredSsoAuthenticationResponseAsync(context); @@ -234,8 +165,7 @@ public class IdentityServerTests : IClassFixture await CreateOrganizationWithSsoPolicyAsync(localFactory, organizationId, user.Email, organizationUserType, ssoPolicyEnabled: true); - var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash, - context => context.SetAuthEmail(user.Email)); + var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash); Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode); } @@ -258,8 +188,7 @@ public class IdentityServerTests : IClassFixture await CreateOrganizationWithSsoPolicyAsync(localFactory, organizationId, user.Email, organizationUserType, ssoPolicyEnabled: true); - var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash, - context => context.SetAuthEmail(user.Email)); + var context = await PostLoginAsync(server, user, requestModel.MasterPasswordHash); Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); await AssertRequiredSsoAuthenticationResponseAsync(context); @@ -342,7 +271,7 @@ public class IdentityServerTests : IClassFixture { "grant_type", "password" }, { "username", model.Email }, { "password", model.MasterPasswordHash }, - }), context => context.SetAuthEmail(model.Email)); + })); Assert.Equal(StatusCodes.Status400BadRequest, context.Response.StatusCode); @@ -554,12 +483,12 @@ public class IdentityServerTests : IClassFixture { "grant_type", "password" }, { "username", user.Email}, { "password", "master_password_hash" }, - }), context => context.SetAuthEmail(user.Email).SetIp("1.1.1.2")); + }), context => context.SetIp("1.1.1.2")); } } private async Task PostLoginAsync( - TestServer server, User user, string MasterPasswordHash, Action extraConfiguration) + TestServer server, User user, string MasterPasswordHash) { return await server.PostAsync("/connect/token", new FormUrlEncodedContent(new Dictionary { @@ -571,7 +500,7 @@ public class IdentityServerTests : IClassFixture { "grant_type", "password" }, { "username", user.Email }, { "password", MasterPasswordHash }, - }), extraConfiguration); + })); } private async Task CreateOrganizationWithSsoPolicyAsync( diff --git a/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs b/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs index 53116960f6..553decd542 100644 --- a/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs +++ b/test/Identity.IntegrationTest/Endpoints/IdentityServerTwoFactorTests.cs @@ -143,7 +143,7 @@ public class IdentityServerTwoFactorTests : IClassFixture context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail))); + })); // Assert using var responseBody = await AssertHelper.AssertResponseTypeIs(context); @@ -263,7 +263,7 @@ public class IdentityServerTwoFactorTests : IClassFixture context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail))); + })); // Assert using var responseBody = await AssertHelper.AssertResponseTypeIs(context); @@ -307,7 +307,7 @@ public class IdentityServerTwoFactorTests : IClassFixture context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail))); + })); Assert.Equal(StatusCodes.Status400BadRequest, failedTokenContext.Response.StatusCode); Assert.NotNull(emailToken); @@ -326,7 +326,7 @@ public class IdentityServerTwoFactorTests : IClassFixture context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail))); + })); // Assert @@ -363,7 +363,7 @@ public class IdentityServerTwoFactorTests : IClassFixture context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(_testEmail))); + })); // Assert using var responseBody = await AssertHelper.AssertResponseTypeIs(context); diff --git a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs b/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs index 9a1b8141ae..537aae0935 100644 --- a/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs +++ b/test/Identity.IntegrationTest/RequestValidation/ResourceOwnerPasswordValidatorTests.cs @@ -29,8 +29,7 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture context.SetAuthEmail(DefaultUsername)); + GetFormUrlEncodedContent()); // Assert var body = await AssertHelper.AssertResponseTypeIs(context); @@ -40,27 +39,6 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture(context); - var root = body.RootElement; - - var error = AssertHelper.AssertJsonProperty(root, "error_description", JsonValueKind.String).GetString(); - Assert.Equal("Auth-Email header invalid.", error); - } - [Theory, BitAutoData] public async Task ValidateAsync_UserNull_Failure(string username) { @@ -68,8 +46,7 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture context.SetAuthEmail(username)); + GetFormUrlEncodedContent(username: username)); // Assert var body = await AssertHelper.AssertResponseTypeIs(context); @@ -106,8 +83,7 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture context.SetAuthEmail(DefaultUsername)); + GetFormUrlEncodedContent(password: badPassword)); // Assert var body = await AssertHelper.AssertResponseTypeIs(context); @@ -155,7 +131,7 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture context.SetAuthEmail(DefaultUsername)); + })); // Assert var body = await AssertHelper.AssertResponseTypeIs(context); @@ -197,7 +173,7 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture context.SetAuthEmail(DefaultUsername)); + })); // Assert diff --git a/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs b/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs index a686605836..474d48b57c 100644 --- a/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs +++ b/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs @@ -98,7 +98,7 @@ public class IdentityApplicationFactory : WebApplicationFactoryBase { "grant_type", "password" }, { "username", username }, { "password", password }, - }), context => context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(username))); + })); return context; } @@ -126,7 +126,7 @@ public class IdentityApplicationFactory : WebApplicationFactoryBase { "TwoFactorToken", twoFactorToken }, { "TwoFactorProvider", twoFactorProviderType }, { "TwoFactorRemember", "1" }, - }), context => context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(username))); + })); return context; } diff --git a/test/IntegrationTestCommon/Factories/WebApplicationFactoryExtensions.cs b/test/IntegrationTestCommon/Factories/WebApplicationFactoryExtensions.cs index 562156b09e..c6740a9d6d 100644 --- a/test/IntegrationTestCommon/Factories/WebApplicationFactoryExtensions.cs +++ b/test/IntegrationTestCommon/Factories/WebApplicationFactoryExtensions.cs @@ -62,12 +62,6 @@ public static class WebApplicationFactoryExtensions Action extraConfiguration = null) => SendAsync(server, HttpMethod.Delete, requestUri, content: content, extraConfiguration); - public static HttpContext SetAuthEmail(this HttpContext context, string username) - { - context.Request.Headers.Append("Auth-Email", CoreHelpers.Base64UrlEncodeString(username)); - return context; - } - public static HttpContext SetIp(this HttpContext context, string ip) { context.Connection.RemoteIpAddress = IPAddress.Parse(ip); From 8d2629fe58ca9c7648d27d9bfe2e31dbfdabc7fc Mon Sep 17 00:00:00 2001 From: Patrick-Pimentel-Bitwarden Date: Fri, 16 May 2025 09:50:32 -0400 Subject: [PATCH 067/114] Auth/pm 17111/add browser to list of approving clients (#5825) * refactor(update-auth-approving-clients): [PM-17111] Add Browser to List of Approving Clients - Refactored how it works to fit different priorities. --- src/Core/Constants.cs | 1 + .../UserDecryptionOptionsBuilder.cs | 8 +- .../Utilities/LoginApprovingClientTypes.cs | 41 +++++++--- .../Utilities/ServiceCollectionExtensions.cs | 1 + .../UserDecryptionOptionsBuilderTests.cs | 75 +++++++++++++++++-- 5 files changed, 103 insertions(+), 23 deletions(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 847214598c..707001ddcc 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -115,6 +115,7 @@ public static class FeatureFlagKeys public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence"; public const string EmailVerification = "email-verification"; public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh"; + public const string BrowserExtensionLoginApproval = "pm-14938-browser-extension-login-approvals"; public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor"; public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor"; public const string RecoveryCodeLogin = "pm-17128-recovery-code-login"; diff --git a/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs b/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs index 34a9d6c573..61543f9751 100644 --- a/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs +++ b/src/Identity/IdentityServer/UserDecryptionOptionsBuilder.cs @@ -22,6 +22,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder private readonly ICurrentContext _currentContext; private readonly IDeviceRepository _deviceRepository; private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly ILoginApprovingClientTypes _loginApprovingClientTypes; private UserDecryptionOptions _options = new UserDecryptionOptions(); private User? _user; @@ -31,12 +32,14 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder public UserDecryptionOptionsBuilder( ICurrentContext currentContext, IDeviceRepository deviceRepository, - IOrganizationUserRepository organizationUserRepository + IOrganizationUserRepository organizationUserRepository, + ILoginApprovingClientTypes loginApprovingClientTypes ) { _currentContext = currentContext; _deviceRepository = deviceRepository; _organizationUserRepository = organizationUserRepository; + _loginApprovingClientTypes = loginApprovingClientTypes; } public IUserDecryptionOptionsBuilder ForUser(User user) @@ -119,8 +122,7 @@ public class UserDecryptionOptionsBuilder : IUserDecryptionOptionsBuilder // Checks if the current user has any devices that are capable of approving login with device requests except for // their current device. // NOTE: this doesn't check for if the users have configured the devices to be capable of approving requests as that is a client side setting. - hasLoginApprovingDevice = allDevices - .Any(d => d.Identifier != _device.Identifier && LoginApprovingClientTypes.TypesThatCanApprove.Contains(DeviceTypes.ToClientType(d.Type))); + hasLoginApprovingDevice = allDevices.Any(d => d.Identifier != _device.Identifier && _loginApprovingClientTypes.TypesThatCanApprove.Contains(DeviceTypes.ToClientType(d.Type))); } // Determine if user has manage reset password permission as post sso logic requires it for forcing users with this permission to set a MP diff --git a/src/Identity/Utilities/LoginApprovingClientTypes.cs b/src/Identity/Utilities/LoginApprovingClientTypes.cs index dd27a87550..f0c7b831b7 100644 --- a/src/Identity/Utilities/LoginApprovingClientTypes.cs +++ b/src/Identity/Utilities/LoginApprovingClientTypes.cs @@ -1,22 +1,39 @@ -using Bit.Core.Enums; +using Bit.Core; +using Bit.Core.Enums; +using Bit.Core.Services; namespace Bit.Identity.Utilities; -public static class LoginApprovingClientTypes +public interface ILoginApprovingClientTypes { - private static readonly IReadOnlyCollection _clientTypesThatCanApprove; + IReadOnlyCollection TypesThatCanApprove { get; } +} - static LoginApprovingClientTypes() +public class LoginApprovingClientTypes : ILoginApprovingClientTypes +{ + public LoginApprovingClientTypes( + IFeatureService featureService) { - var clientTypes = new List + if (featureService.IsEnabled(FeatureFlagKeys.BrowserExtensionLoginApproval)) { - ClientType.Desktop, - ClientType.Mobile, - ClientType.Web, - ClientType.Browser, - }; - _clientTypesThatCanApprove = clientTypes.AsReadOnly(); + TypesThatCanApprove = new List + { + ClientType.Desktop, + ClientType.Mobile, + ClientType.Web, + ClientType.Browser, + }; + } + else + { + TypesThatCanApprove = new List + { + ClientType.Desktop, + ClientType.Mobile, + ClientType.Web, + }; + } } - public static IReadOnlyCollection TypesThatCanApprove => _clientTypesThatCanApprove; + public IReadOnlyCollection TypesThatCanApprove { get; } } diff --git a/src/Identity/Utilities/ServiceCollectionExtensions.cs b/src/Identity/Utilities/ServiceCollectionExtensions.cs index 36c38615a2..bf90b1aa24 100644 --- a/src/Identity/Utilities/ServiceCollectionExtensions.cs +++ b/src/Identity/Utilities/ServiceCollectionExtensions.cs @@ -23,6 +23,7 @@ public static class ServiceCollectionExtensions services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity); var identityServerBuilder = services diff --git a/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs b/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs index 0a6346fe9e..25182743e5 100644 --- a/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs +++ b/test/Identity.Test/IdentityServer/UserDecryptionOptionsBuilderTests.cs @@ -6,6 +6,7 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Identity.IdentityServer; +using Bit.Identity.Utilities; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; using Xunit; @@ -17,6 +18,7 @@ public class UserDecryptionOptionsBuilderTests private readonly ICurrentContext _currentContext; private readonly IDeviceRepository _deviceRepository; private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly ILoginApprovingClientTypes _loginApprovingClientTypes; private readonly UserDecryptionOptionsBuilder _builder; public UserDecryptionOptionsBuilderTests() @@ -24,7 +26,8 @@ public class UserDecryptionOptionsBuilderTests _currentContext = Substitute.For(); _deviceRepository = Substitute.For(); _organizationUserRepository = Substitute.For(); - _builder = new UserDecryptionOptionsBuilder(_currentContext, _deviceRepository, _organizationUserRepository); + _loginApprovingClientTypes = Substitute.For(); + _builder = new UserDecryptionOptionsBuilder(_currentContext, _deviceRepository, _organizationUserRepository, _loginApprovingClientTypes); } [Theory] @@ -120,17 +123,17 @@ public class UserDecryptionOptionsBuilderTests [BitAutoData(DeviceType.SafariBrowser)] [BitAutoData(DeviceType.VivaldiBrowser)] [BitAutoData(DeviceType.UnknownBrowser)] - // Extension - [BitAutoData(DeviceType.ChromeExtension)] - [BitAutoData(DeviceType.FirefoxExtension)] - [BitAutoData(DeviceType.OperaExtension)] - [BitAutoData(DeviceType.EdgeExtension)] - [BitAutoData(DeviceType.VivaldiExtension)] - [BitAutoData(DeviceType.SafariExtension)] public async Task Build_WhenHasLoginApprovingDevice_ShouldApprovingDeviceTrue( DeviceType deviceType, SsoConfig ssoConfig, SsoConfigurationData configurationData, User user, Device device, Device approvingDevice) { + _loginApprovingClientTypes.TypesThatCanApprove.Returns(new List + { + ClientType.Desktop, + ClientType.Mobile, + ClientType.Web, + }); + configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption; ssoConfig.Data = configurationData.Serialize(); approvingDevice.Type = deviceType; @@ -142,9 +145,65 @@ public class UserDecryptionOptionsBuilderTests } [Theory] + // Desktop + [BitAutoData(DeviceType.LinuxDesktop)] + [BitAutoData(DeviceType.MacOsDesktop)] + [BitAutoData(DeviceType.WindowsDesktop)] + [BitAutoData(DeviceType.UWP)] + // Mobile + [BitAutoData(DeviceType.Android)] + [BitAutoData(DeviceType.iOS)] + [BitAutoData(DeviceType.AndroidAmazon)] + // Web + [BitAutoData(DeviceType.ChromeBrowser)] + [BitAutoData(DeviceType.FirefoxBrowser)] + [BitAutoData(DeviceType.OperaBrowser)] + [BitAutoData(DeviceType.EdgeBrowser)] + [BitAutoData(DeviceType.IEBrowser)] + [BitAutoData(DeviceType.SafariBrowser)] + [BitAutoData(DeviceType.VivaldiBrowser)] + [BitAutoData(DeviceType.UnknownBrowser)] + // Extension + [BitAutoData(DeviceType.ChromeExtension)] + [BitAutoData(DeviceType.FirefoxExtension)] + [BitAutoData(DeviceType.OperaExtension)] + [BitAutoData(DeviceType.EdgeExtension)] + [BitAutoData(DeviceType.VivaldiExtension)] + [BitAutoData(DeviceType.SafariExtension)] + public async Task Build_WhenHasLoginApprovingDeviceFeatureFlag_ShouldApprovingDeviceTrue( + DeviceType deviceType, + SsoConfig ssoConfig, SsoConfigurationData configurationData, User user, Device device, Device approvingDevice) + { + _loginApprovingClientTypes.TypesThatCanApprove.Returns(new List + { + ClientType.Desktop, + ClientType.Mobile, + ClientType.Web, + ClientType.Browser, + }); + + configurationData.MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption; + ssoConfig.Data = configurationData.Serialize(); + approvingDevice.Type = deviceType; + _deviceRepository.GetManyByUserIdAsync(user.Id).Returns(new Device[] { approvingDevice }); + + var result = await _builder.ForUser(user).WithSso(ssoConfig).WithDevice(device).BuildAsync(); + + Assert.True(result.TrustedDeviceOption?.HasLoginApprovingDevice); + } + + [Theory] + // CLI [BitAutoData(DeviceType.WindowsCLI)] [BitAutoData(DeviceType.MacOsCLI)] [BitAutoData(DeviceType.LinuxCLI)] + // Extension + [BitAutoData(DeviceType.ChromeExtension)] + [BitAutoData(DeviceType.FirefoxExtension)] + [BitAutoData(DeviceType.OperaExtension)] + [BitAutoData(DeviceType.EdgeExtension)] + [BitAutoData(DeviceType.VivaldiExtension)] + [BitAutoData(DeviceType.SafariExtension)] public async Task Build_WhenHasLoginApprovingDevice_ShouldApprovingDeviceFalse( DeviceType deviceType, SsoConfig ssoConfig, SsoConfigurationData configurationData, User user, Device device, Device approvingDevice) From b48a09a3385f03083e80303cfb1bc630f19d9f7e Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Fri, 16 May 2025 15:23:09 +0100 Subject: [PATCH 068/114] Resolve the revoke for more than one Sponsorship (#5827) --- .../OrganizationSponsorshipsController.cs | 18 ++++++++++++++++-- ...HostedOrganizationSponsorshipsController.cs | 12 ++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs index 0e04385dc9..c45b34422c 100644 --- a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs @@ -207,7 +207,7 @@ public class OrganizationSponsorshipsController : Controller [HttpDelete("{sponsoringOrganizationId}")] [HttpPost("{sponsoringOrganizationId}/delete")] [SelfHosted(NotSelfHostedOnly = true)] - public async Task RevokeSponsorship(Guid sponsoringOrganizationId, [FromQuery] bool isAdminInitiated = false) + public async Task RevokeSponsorship(Guid sponsoringOrganizationId) { var orgUser = await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrganizationId, _currentContext.UserId ?? default); @@ -217,11 +217,25 @@ public class OrganizationSponsorshipsController : Controller } var existingOrgSponsorship = await _organizationSponsorshipRepository - .GetBySponsoringOrganizationUserIdAsync(orgUser.Id, isAdminInitiated); + .GetBySponsoringOrganizationUserIdAsync(orgUser.Id); await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship); } + [Authorize("Application")] + [HttpDelete("{sponsoringOrgId}/{sponsoredFriendlyName}/revoke")] + [SelfHosted(NotSelfHostedOnly = true)] + public async Task AdminInitiatedRevokeSponsorshipAsync(Guid sponsoringOrgId, string sponsoredFriendlyName) + { + var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId); + var existingOrgSponsorship = sponsorships.FirstOrDefault(s => s.FriendlyName != null && s.FriendlyName.Equals(sponsoredFriendlyName, StringComparison.OrdinalIgnoreCase)); + if (existingOrgSponsorship == null) + { + throw new BadRequestException("The specified sponsored organization could not be found under the given sponsoring organization."); + } + await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship); + } + [Authorize("Application")] [HttpDelete("sponsored/{sponsoredOrgId}")] [HttpPost("sponsored/{sponsoredOrgId}/remove")] diff --git a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs index e328b7c3e4..0c920028e8 100644 --- a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs +++ b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs @@ -84,4 +84,16 @@ public class SelfHostedOrganizationSponsorshipsController : Controller await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship); } + + [HttpDelete("{sponsoringOrgId}/{sponsoredFriendlyName}/revoke")] + public async Task AdminInitiatedRevokeSponsorshipAsync(Guid sponsoringOrgId, string sponsoredFriendlyName) + { + var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrgId); + var existingOrgSponsorship = sponsorships.FirstOrDefault(s => s.FriendlyName != null && s.FriendlyName.Equals(sponsoredFriendlyName, StringComparison.OrdinalIgnoreCase)); + if (existingOrgSponsorship == null) + { + throw new BadRequestException("The specified sponsored organization could not be found under the given sponsoring organization."); + } + await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship); + } } From 51e489ac9d748a1e970e5e02f3530656fcccfe18 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Fri, 16 May 2025 16:05:47 +0100 Subject: [PATCH 069/114] [PM 21081][Defect] Admin Sponsored Families not working in Self Host after uploading license file (#5818) * Resolve the licence issue Signed-off-by: Cy Okeke * resolve the pr comment * Remove unnecessary directives Signed-off-by: Cy Okeke --------- Signed-off-by: Cy Okeke --- ...ostedOrganizationSponsorshipsController.cs | 36 +++++++++++++++++-- .../AdminConsole/Entities/Organization.cs | 1 + .../SelfHostedOrganizationDetails.cs | 1 + .../Implementations/OrganizationService.cs | 1 + .../Factories/IdentityApplicationFactory.cs | 2 -- .../WebApplicationFactoryExtensions.cs | 1 - 6 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs index 0c920028e8..371b321a4c 100644 --- a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs +++ b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs @@ -1,6 +1,10 @@ -using Bit.Api.Models.Request.Organizations; +using Bit.Api.AdminConsole.Authorization.Requirements; +using Bit.Api.Models.Request.Organizations; +using Bit.Api.Models.Response; using Bit.Core.Context; using Bit.Core.Exceptions; +using Bit.Core.Models.Api.Response.OrganizationSponsorships; +using Bit.Core.Models.Data.Organizations.OrganizationSponsorships; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -22,6 +26,7 @@ public class SelfHostedOrganizationSponsorshipsController : Controller private readonly IRevokeSponsorshipCommand _revokeSponsorshipCommand; private readonly ICurrentContext _currentContext; private readonly IFeatureService _featureService; + private readonly IAuthorizationService _authorizationService; public SelfHostedOrganizationSponsorshipsController( ICreateSponsorshipCommand offerSponsorshipCommand, @@ -30,7 +35,8 @@ public class SelfHostedOrganizationSponsorshipsController : Controller IOrganizationSponsorshipRepository organizationSponsorshipRepository, IOrganizationUserRepository organizationUserRepository, ICurrentContext currentContext, - IFeatureService featureService + IFeatureService featureService, + IAuthorizationService authorizationService ) { _offerSponsorshipCommand = offerSponsorshipCommand; @@ -40,6 +46,7 @@ public class SelfHostedOrganizationSponsorshipsController : Controller _organizationUserRepository = organizationUserRepository; _currentContext = currentContext; _featureService = featureService; + _authorizationService = authorizationService; } [HttpPost("{sponsoringOrgId}/families-for-enterprise")] @@ -96,4 +103,29 @@ public class SelfHostedOrganizationSponsorshipsController : Controller } await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship); } + + [Authorize("Application")] + [HttpGet("{orgId}/sponsored")] + public async Task> GetSponsoredOrganizations(Guid orgId) + { + var sponsoringOrg = await _organizationRepository.GetByIdAsync(orgId); + if (sponsoringOrg == null) + { + throw new NotFoundException(); + } + + var authorizationResult = await _authorizationService.AuthorizeAsync(User, orgId, new ManageUsersRequirement()); + if (!authorizationResult.Succeeded) + { + throw new UnauthorizedAccessException(); + } + + var sponsorships = await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(orgId); + return new ListResponseModel( + sponsorships + .Where(s => s.IsAdminInitiated) + .Select(s => new OrganizationSponsorshipInvitesResponseModel(new OrganizationSponsorshipData(s))) + ); + + } } diff --git a/src/Core/AdminConsole/Entities/Organization.cs b/src/Core/AdminConsole/Entities/Organization.cs index 4b7d497277..e649406bb0 100644 --- a/src/Core/AdminConsole/Entities/Organization.cs +++ b/src/Core/AdminConsole/Entities/Organization.cs @@ -325,5 +325,6 @@ public class Organization : ITableObject, IStorableSubscriber, IRevisable, SmServiceAccounts = license.SmServiceAccounts; UseRiskInsights = license.UseRiskInsights; UseOrganizationDomains = license.UseOrganizationDomains; + UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies; } } diff --git a/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs b/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs index ab2dfd7e0e..a6ad47f829 100644 --- a/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Organizations/SelfHostedOrganizationDetails.cs @@ -150,6 +150,7 @@ public class SelfHostedOrganizationDetails : Organization AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems, Status = Status, UseRiskInsights = UseRiskInsights, + UseAdminSponsoredFamilies = UseAdminSponsoredFamilies, }; } } diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 3a9fcff847..1ced923b45 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -572,6 +572,7 @@ public class OrganizationService : IOrganizationService SmServiceAccounts = license.SmServiceAccounts, UseRiskInsights = license.UseRiskInsights, UseOrganizationDomains = license.UseOrganizationDomains, + UseAdminSponsoredFamilies = license.UseAdminSponsoredFamilies, }; var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false); diff --git a/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs b/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs index 474d48b57c..eced27f937 100644 --- a/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs +++ b/test/IntegrationTestCommon/Factories/IdentityApplicationFactory.cs @@ -5,10 +5,8 @@ using Bit.Core.Auth.Models.Api.Request.Accounts; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Services; -using Bit.Core.Utilities; using Bit.Identity; using Bit.Test.Common.Helpers; -using HandlebarsDotNet; using LinqToDB; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; diff --git a/test/IntegrationTestCommon/Factories/WebApplicationFactoryExtensions.cs b/test/IntegrationTestCommon/Factories/WebApplicationFactoryExtensions.cs index c6740a9d6d..128b38ff9a 100644 --- a/test/IntegrationTestCommon/Factories/WebApplicationFactoryExtensions.cs +++ b/test/IntegrationTestCommon/Factories/WebApplicationFactoryExtensions.cs @@ -1,5 +1,4 @@ using System.Net; -using Bit.Core.Utilities; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Primitives; From cbf49b915ba25a30146a3e931bea120f9152b942 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Fri, 16 May 2025 12:51:22 -0400 Subject: [PATCH 070/114] [PM-11622] Implement UseOrganizationDomains (#5822) * implement UseOrganizationDomains * fix undefined error --- .../Views/Shared/_OrganizationFormScripts.cshtml | 2 +- .../GetOrganizationUsersClaimedStatusQuery.cs | 4 +--- src/Core/Services/Implementations/UserService.cs | 4 +--- .../GetOrganizationUsersClaimedStatusQueryTests.cs | 8 ++++---- test/Core.Test/Services/UserServiceTests.cs | 8 ++++---- 5 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/Admin/AdminConsole/Views/Shared/_OrganizationFormScripts.cshtml b/src/Admin/AdminConsole/Views/Shared/_OrganizationFormScripts.cshtml index 0ce25c700f..ea4448d100 100644 --- a/src/Admin/AdminConsole/Views/Shared/_OrganizationFormScripts.cshtml +++ b/src/Admin/AdminConsole/Views/Shared/_OrganizationFormScripts.cshtml @@ -69,7 +69,7 @@ document.getElementById('@(nameof(Model.UseGroups))').checked = plan.hasGroups; document.getElementById('@(nameof(Model.UsePolicies))').checked = plan.hasPolicies; document.getElementById('@(nameof(Model.UseSso))').checked = plan.hasSso; - document.getElementById('@(nameof(Model.UseOrganizationDomains))').checked = hasOrganizationDomains; + document.getElementById('@(nameof(Model.UseOrganizationDomains))').checked = plan.hasOrganizationDomains; document.getElementById('@(nameof(Model.UseScim))').checked = plan.hasScim; document.getElementById('@(nameof(Model.UseDirectory))').checked = plan.hasDirectory; document.getElementById('@(nameof(Model.UseEvents))').checked = plan.hasEvents; diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQuery.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQuery.cs index 1dda9483cd..d8c510119a 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQuery.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQuery.cs @@ -24,9 +24,7 @@ public class GetOrganizationUsersClaimedStatusQuery : IGetOrganizationUsersClaim // Users can only be claimed by an Organization that is enabled and can have organization domains var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(organizationId); - // TODO: Replace "UseSso" with a new organization ability like "UseOrganizationDomains" (PM-11622). - // Verified domains were tied to SSO, so we currently check the "UseSso" organization ability. - if (organizationAbility is { Enabled: true, UseSso: true }) + if (organizationAbility is { Enabled: true, UseOrganizationDomains: true }) { // Get all organization users with claimed domains by the organization var organizationUsersWithClaimedDomain = await _organizationUserRepository.GetManyByOrganizationWithClaimedDomainsAsync(organizationId); diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 151ff38aa5..76520b4085 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -1341,9 +1341,7 @@ public class UserService : UserManager, IUserService, IDisposable var organizationsWithVerifiedUserEmailDomain = await _organizationRepository.GetByVerifiedUserEmailDomainAsync(userId); // Organizations must be enabled and able to have verified domains. - // TODO: Replace "UseSso" with a new organization ability like "UseOrganizationDomains" (PM-11622). - // Verified domains were tied to SSO, so we currently check the "UseSso" organization ability. - return organizationsWithVerifiedUserEmailDomain.Where(organization => organization is { Enabled: true, UseSso: true }); + return organizationsWithVerifiedUserEmailDomain.Where(organization => organization is { Enabled: true, UseOrganizationDomains: true }); } /// diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQueryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQueryTests.cs index fd6d827791..85dc643022 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQueryTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/GetOrganizationUsersClaimedStatusQueryTests.cs @@ -25,13 +25,13 @@ public class GetOrganizationUsersClaimedStatusQueryTests } [Theory, BitAutoData] - public async Task GetUsersOrganizationManagementStatusAsync_WithUseSsoEnabled_Success( + public async Task GetUsersOrganizationManagementStatusAsync_WithUseOrganizationDomainsEnabled_Success( Organization organization, ICollection usersWithClaimedDomain, SutProvider sutProvider) { organization.Enabled = true; - organization.UseSso = true; + organization.UseOrganizationDomains = true; var userIdWithoutClaimedDomain = Guid.NewGuid(); var userIdsToCheck = usersWithClaimedDomain.Select(u => u.Id).Concat(new List { userIdWithoutClaimedDomain }).ToList(); @@ -51,13 +51,13 @@ public class GetOrganizationUsersClaimedStatusQueryTests } [Theory, BitAutoData] - public async Task GetUsersOrganizationManagementStatusAsync_WithUseSsoDisabled_ReturnsAllFalse( + public async Task GetUsersOrganizationManagementStatusAsync_WithUseOrganizationDomainsDisabled_ReturnsAllFalse( Organization organization, ICollection usersWithClaimedDomain, SutProvider sutProvider) { organization.Enabled = true; - organization.UseSso = false; + organization.UseOrganizationDomains = false; var userIdWithoutClaimedDomain = Guid.NewGuid(); var userIdsToCheck = usersWithClaimedDomain.Select(u => u.Id).Concat(new List { userIdWithoutClaimedDomain }).ToList(); diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index 0458c7cdd9..ac7f6e4018 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -347,7 +347,7 @@ public class UserServiceTests SutProvider sutProvider, Guid userId, Organization organization) { organization.Enabled = true; - organization.UseSso = true; + organization.UseOrganizationDomains = true; sutProvider.GetDependency() .GetByVerifiedUserEmailDomainAsync(userId) @@ -362,7 +362,7 @@ public class UserServiceTests SutProvider sutProvider, Guid userId, Organization organization) { organization.Enabled = false; - organization.UseSso = true; + organization.UseOrganizationDomains = true; sutProvider.GetDependency() .GetByVerifiedUserEmailDomainAsync(userId) @@ -373,11 +373,11 @@ public class UserServiceTests } [Theory, BitAutoData] - public async Task IsClaimedByAnyOrganizationAsync_WithOrganizationUseSsoFalse_ReturnsFalse( + public async Task IsClaimedByAnyOrganizationAsync_WithOrganizationUseOrganizationDomaisFalse_ReturnsFalse( SutProvider sutProvider, Guid userId, Organization organization) { organization.Enabled = true; - organization.UseSso = false; + organization.UseOrganizationDomains = false; sutProvider.GetDependency() .GetByVerifiedUserEmailDomainAsync(userId) From fad91d86142c3de64ec7bdc992022bd1c5246dc2 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Fri, 16 May 2025 15:11:28 -0400 Subject: [PATCH 071/114] fix(captcha): [PM-21707] Add back captchaBypassToken stub value --- .../Response/Accounts/RegisterFinishResponseModel.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Identity/Models/Response/Accounts/RegisterFinishResponseModel.cs b/src/Identity/Models/Response/Accounts/RegisterFinishResponseModel.cs index d7c7b94366..564150ab30 100644 --- a/src/Identity/Models/Response/Accounts/RegisterFinishResponseModel.cs +++ b/src/Identity/Models/Response/Accounts/RegisterFinishResponseModel.cs @@ -6,5 +6,12 @@ public class RegisterFinishResponseModel : ResponseModel { public RegisterFinishResponseModel() : base("registerFinish") - { } + { + // We are setting this to an empty string so that old mobile clients don't break, as they reqiure a non-null value. + // This will be cleaned up in https://bitwarden.atlassian.net/browse/PM-21720. + CaptchaBypassToken = string.Empty; + } + + public string CaptchaBypassToken { get; set; } + } From b3f726512940f1aa69b0a326259b333998475670 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Fri, 16 May 2025 17:30:51 -0400 Subject: [PATCH 072/114] [PM-21727] Add feature to plan and license constructor (#5834) * add feature to plan * add license to ctor for old license file creation method --- .../Commercial.Core/Billing/BusinessUnitConverter.cs | 1 + .../Billing/ProviderBillingService.cs | 1 + .../Services/Implementations/OrganizationMigrator.cs | 1 + .../Models/StaticStore/Plans/Enterprise2019Plan.cs | 1 + .../Models/StaticStore/Plans/Enterprise2020Plan.cs | 1 + .../Models/StaticStore/Plans/EnterprisePlan.cs | 1 + .../Models/StaticStore/Plans/EnterprisePlan2023.cs | 1 + src/Core/Billing/Pricing/PlanAdapter.cs | 1 + src/Core/Models/Business/OrganizationLicense.cs | 12 ++++++++++-- .../Business/OrganizationLicenseFileFixtures.cs | 5 ++++- 10 files changed, 22 insertions(+), 3 deletions(-) diff --git a/bitwarden_license/src/Commercial.Core/Billing/BusinessUnitConverter.cs b/bitwarden_license/src/Commercial.Core/Billing/BusinessUnitConverter.cs index 97d9377cd6..d27b45af4a 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/BusinessUnitConverter.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/BusinessUnitConverter.cs @@ -67,6 +67,7 @@ public class BusinessUnitConverter( organization.MaxStorageGb = updatedPlan.PasswordManager.BaseStorageGb; organization.UsePolicies = updatedPlan.HasPolicies; organization.UseSso = updatedPlan.HasSso; + organization.UseOrganizationDomains = updatedPlan.HasOrganizationDomains; organization.UseGroups = updatedPlan.HasGroups; organization.UseEvents = updatedPlan.HasEvents; organization.UseDirectory = updatedPlan.HasDirectory; diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs index b1eefbffe3..fe6b8d4617 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs @@ -99,6 +99,7 @@ public class ProviderBillingService( organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb; organization.UsePolicies = plan.HasPolicies; organization.UseSso = plan.HasSso; + organization.UseOrganizationDomains = plan.HasOrganizationDomains; organization.UseGroups = plan.HasGroups; organization.UseEvents = plan.HasEvents; organization.UseDirectory = plan.HasDirectory; diff --git a/src/Core/Billing/Migration/Services/Implementations/OrganizationMigrator.cs b/src/Core/Billing/Migration/Services/Implementations/OrganizationMigrator.cs index 4d93c0119a..204022380d 100644 --- a/src/Core/Billing/Migration/Services/Implementations/OrganizationMigrator.cs +++ b/src/Core/Billing/Migration/Services/Implementations/OrganizationMigrator.cs @@ -309,6 +309,7 @@ public class OrganizationMigrator( organization.MaxStorageGb = plan.PasswordManager.BaseStorageGb; organization.UsePolicies = plan.HasPolicies; organization.UseSso = plan.HasSso; + organization.UseOrganizationDomains = plan.HasOrganizationDomains; organization.UseGroups = plan.HasGroups; organization.UseEvents = plan.HasEvents; organization.UseDirectory = plan.HasDirectory; diff --git a/src/Core/Billing/Models/StaticStore/Plans/Enterprise2019Plan.cs b/src/Core/Billing/Models/StaticStore/Plans/Enterprise2019Plan.cs index 72db7897b4..b584647a26 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/Enterprise2019Plan.cs +++ b/src/Core/Billing/Models/StaticStore/Plans/Enterprise2019Plan.cs @@ -26,6 +26,7 @@ public record Enterprise2019Plan : Plan Has2fa = true; HasApi = true; HasSso = true; + HasOrganizationDomains = true; HasKeyConnector = true; HasScim = true; HasResetPassword = true; diff --git a/src/Core/Billing/Models/StaticStore/Plans/Enterprise2020Plan.cs b/src/Core/Billing/Models/StaticStore/Plans/Enterprise2020Plan.cs index 42b984e7e5..a1a6113cbc 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/Enterprise2020Plan.cs +++ b/src/Core/Billing/Models/StaticStore/Plans/Enterprise2020Plan.cs @@ -26,6 +26,7 @@ public record Enterprise2020Plan : Plan Has2fa = true; HasApi = true; HasSso = true; + HasOrganizationDomains = true; HasKeyConnector = true; HasScim = true; HasResetPassword = true; diff --git a/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan.cs b/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan.cs index 2d498a7654..8aeca521d1 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan.cs +++ b/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan.cs @@ -26,6 +26,7 @@ public record EnterprisePlan : Plan Has2fa = true; HasApi = true; HasSso = true; + HasOrganizationDomains = true; HasKeyConnector = true; HasScim = true; HasResetPassword = true; diff --git a/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan2023.cs b/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan2023.cs index 8cd8335425..dce1719a49 100644 --- a/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan2023.cs +++ b/src/Core/Billing/Models/StaticStore/Plans/EnterprisePlan2023.cs @@ -26,6 +26,7 @@ public record Enterprise2023Plan : Plan Has2fa = true; HasApi = true; HasSso = true; + HasOrganizationDomains = true; HasKeyConnector = true; HasScim = true; HasResetPassword = true; diff --git a/src/Core/Billing/Pricing/PlanAdapter.cs b/src/Core/Billing/Pricing/PlanAdapter.cs index c38eb0501d..f719fd1e87 100644 --- a/src/Core/Billing/Pricing/PlanAdapter.cs +++ b/src/Core/Billing/Pricing/PlanAdapter.cs @@ -26,6 +26,7 @@ public record PlanAdapter : Plan Has2fa = HasFeature("2fa"); HasApi = HasFeature("api"); HasSso = HasFeature("sso"); + HasOrganizationDomains = HasFeature("organizationDomains"); HasKeyConnector = HasFeature("keyConnector"); HasScim = HasFeature("scim"); HasResetPassword = HasFeature("resetPassword"); diff --git a/src/Core/Models/Business/OrganizationLicense.cs b/src/Core/Models/Business/OrganizationLicense.cs index a201da3847..eb2f91526b 100644 --- a/src/Core/Models/Business/OrganizationLicense.cs +++ b/src/Core/Models/Business/OrganizationLicense.cs @@ -84,6 +84,7 @@ public class OrganizationLicense : ILicense SmSeats = org.SmSeats; SmServiceAccounts = org.SmServiceAccounts; UseRiskInsights = org.UseRiskInsights; + UseOrganizationDomains = org.UseOrganizationDomains; // Deprecated. Left for backwards compatibility with old license versions. LimitCollectionCreationDeletion = org.LimitCollectionCreation || org.LimitCollectionDeletion; @@ -195,10 +196,10 @@ public class OrganizationLicense : ILicense /// Intentionally set one version behind to allow self hosted users some time to update before /// getting out of date license errors /// - public const int CurrentLicenseFileVersion = 14; + public const int CurrentLicenseFileVersion = 15; private bool ValidLicenseVersion { - get => Version is >= 1 and <= 15; + get => Version is >= 1 and <= 16; } public byte[] GetDataBytes(bool forHash = false) @@ -244,6 +245,8 @@ public class OrganizationLicense : ILicense (Version >= 14 || !p.Name.Equals(nameof(LimitCollectionCreationDeletion))) && // AllowAdminAccessToAllCollectionItems was added in Version 15 (Version >= 15 || !p.Name.Equals(nameof(AllowAdminAccessToAllCollectionItems))) && + // UseOrganizationDomains was added in Version 16 + (Version >= 16 || !p.Name.Equals(nameof(UseOrganizationDomains))) && ( !forHash || ( @@ -583,6 +586,11 @@ public class OrganizationLicense : ILicense * validation. */ + if (valid && Version >= 16) + { + valid = organization.UseOrganizationDomains; + } + return valid; } diff --git a/test/Core.Test/Models/Business/OrganizationLicenseFileFixtures.cs b/test/Core.Test/Models/Business/OrganizationLicenseFileFixtures.cs index 1004cefeca..08771df06a 100644 --- a/test/Core.Test/Models/Business/OrganizationLicenseFileFixtures.cs +++ b/test/Core.Test/Models/Business/OrganizationLicenseFileFixtures.cs @@ -28,7 +28,10 @@ public static class OrganizationLicenseFileFixtures private const string Version15 = "{\n 'LicenseKey': 'myLicenseKey',\n 'InstallationId': '78900000-0000-0000-0000-000000000123',\n 'Id': '12300000-0000-0000-0000-000000000456',\n 'Name': 'myOrg',\n 'BillingEmail': 'myBillingEmail',\n 'BusinessName': 'myBusinessName',\n 'Enabled': true,\n 'Plan': 'myPlan',\n 'PlanType': 11,\n 'Seats': 10,\n 'MaxCollections': 2,\n 'UsePolicies': true,\n 'UseSso': true,\n 'UseKeyConnector': true,\n 'UseScim': true,\n 'UseGroups': true,\n 'UseEvents': true,\n 'UseDirectory': true,\n 'UseTotp': true,\n 'Use2fa': true,\n 'UseApi': true,\n 'UseResetPassword': true,\n 'MaxStorageGb': 100,\n 'SelfHost': true,\n 'UsersGetPremium': true,\n 'UseCustomPermissions': true,\n 'Version': 14,\n 'Issued': '2023-12-14T02:03:33.374297Z',\n 'Refresh': '2023-12-07T22:42:33.970597Z',\n 'Expires': '2023-12-21T02:03:33.374297Z',\n 'ExpirationWithoutGracePeriod': null,\n 'UsePasswordManager': true,\n 'UseSecretsManager': true,\n 'SmSeats': 5,\n 'SmServiceAccounts': 8,\n 'LimitCollectionCreationDeletion': true,\n 'AllowAdminAccessToAllCollectionItems': true,\n 'Trial': true,\n 'LicenseType': 1,\n 'Hash': 'EZl4IvJaa1E5mPmlfp4p5twAtlmaxlF1yoZzVYP4vog=',\n 'Signature': ''\n}"; - private static readonly Dictionary LicenseVersions = new() { { 12, Version12 }, { 13, Version13 }, { 14, Version14 }, { 15, Version15 } }; + private const string Version16 = + "{\n'LicenseKey': 'myLicenseKey',\n'InstallationId': '78900000-0000-0000-0000-000000000123',\n'Id': '12300000-0000-0000-0000-000000000456',\n'Name': 'myOrg',\n'BillingEmail': 'myBillingEmail',\n'BusinessName': 'myBusinessName',\n'Enabled': true,\n'Plan': 'myPlan',\n'PlanType': 11,\n'Seats': 10,\n'MaxCollections': 2,\n'UsePolicies': true,\n'UseSso': true,\n'UseKeyConnector': true,\n'UseScim': true,\n'UseGroups': true,\n'UseEvents': true,\n'UseDirectory': true,\n'UseTotp': true,\n'Use2fa': true,\n'UseApi': true,\n'UseResetPassword': true,\n'MaxStorageGb': 100,\n'SelfHost': true,\n'UsersGetPremium': true,\n'UseCustomPermissions': true,\n'Version': 15,\n'Issued': '2025-05-16T20:50:09.036931Z',\n'Refresh': '2025-05-23T20:50:09.036931Z',\n'Expires': '2025-05-23T20:50:09.036931Z',\n'ExpirationWithoutGracePeriod': null,\n'UsePasswordManager': true,\n'UseSecretsManager': true,\n'SmSeats': 5,\n'SmServiceAccounts': 8,\n'UseRiskInsights': false,\n'LimitCollectionCreationDeletion': true,\n'AllowAdminAccessToAllCollectionItems': true,\n'Trial': true,\n'LicenseType': 1,\n'UseOrganizationDomains': true,\n'UseAdminSponsoredFamilies': false,\n'Hash': 'k3M9SpHKUo0TmuSnNipeZleCHxgcEycKRXYl9BAg30Q=',\n'Signature': '',\n'Token': null\n}"; + + private static readonly Dictionary LicenseVersions = new() { { 12, Version12 }, { 13, Version13 }, { 14, Version14 }, { 15, Version15 }, { 16, Version16 } }; public static OrganizationLicense GetVersion(int licenseVersion) { From 0928e656cc9d691f9dbf5b13ca79962d6138c4b7 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Mon, 19 May 2025 10:40:03 +0000 Subject: [PATCH 073/114] Bumped version to 2025.5.1 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 60d61e5e26..ac814ef8d8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.5.0 + 2025.5.1 Bit.$(MSBuildProjectName) enable From a36db639984e7eb3b55952fe5a7b49c5756f827b Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Mon, 19 May 2025 15:16:35 +0100 Subject: [PATCH 074/114] Check for payment method before subscription (#5631) --- .../Controllers/OrganizationBillingController.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index 3ebae433d8..1ae1f2e655 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -292,15 +292,17 @@ public class OrganizationBillingController( sale.Organization.PlanType = plan.Type; sale.Organization.Plan = plan.Name; sale.SubscriptionSetup.SkipTrial = true; - await organizationBillingService.Finalize(sale); + + if (organizationSignup.PaymentMethodType == null || string.IsNullOrEmpty(organizationSignup.PaymentToken)) + { + return Error.BadRequest("A payment method is required to restart the subscription."); + } var org = await organizationRepository.GetByIdAsync(organizationId); Debug.Assert(org is not null, "This organization has already been found via this same ID, this should be fine."); - if (organizationSignup.PaymentMethodType != null) - { - var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken); - var taxInformation = TaxInformation.From(organizationSignup.TaxInfo); - await organizationBillingService.UpdatePaymentMethod(org, paymentSource, taxInformation); - } + var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken); + var taxInformation = TaxInformation.From(organizationSignup.TaxInfo); + await organizationBillingService.UpdatePaymentMethod(org, paymentSource, taxInformation); + await organizationBillingService.Finalize(sale); return TypedResults.Ok(); } From b2c8c0230f2df6540cc75c41abcb2d856a70f793 Mon Sep 17 00:00:00 2001 From: Conner Turnbull <133619638+cturnbull-bitwarden@users.noreply.github.com> Date: Mon, 19 May 2025 10:54:11 -0400 Subject: [PATCH 075/114] Ignoring newer properties when generating the license's signature (#5832) --- src/Core/Models/Business/OrganizationLicense.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Core/Models/Business/OrganizationLicense.cs b/src/Core/Models/Business/OrganizationLicense.cs index eb2f91526b..e8c04b1277 100644 --- a/src/Core/Models/Business/OrganizationLicense.cs +++ b/src/Core/Models/Business/OrganizationLicense.cs @@ -255,7 +255,10 @@ public class OrganizationLicense : ILicense !p.Name.Equals(nameof(Refresh)) ) ) && - !p.Name.Equals(nameof(UseRiskInsights))) + // any new fields added need to be added here so that they're ignored + !p.Name.Equals(nameof(UseRiskInsights)) && + !p.Name.Equals(nameof(UseAdminSponsoredFamilies)) && + !p.Name.Equals(nameof(UseOrganizationDomains))) .OrderBy(p => p.Name) .Select(p => $"{p.Name}:{Utilities.CoreHelpers.FormatLicenseSignatureValue(p.GetValue(this, null))}") .Aggregate((c, n) => $"{c}|{n}"); From a07cce26f3f6bff49cd1944f14e98b7c7ec1e8b5 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Mon, 19 May 2025 11:59:15 -0400 Subject: [PATCH 076/114] [PM-19444] Emergency access device verification email fix (#5833) * fix: turn off New Device Verification when emergency access takeover is exercised; Also some Docs * test: add tests for EmergencyAccessService --- .../Auth/Enums/EmergencyAccessStatusType.cs | 15 + .../Auth/Services/IEmergencyAccessService.cs | 10 + .../Implementations/EmergencyAccessService.cs | 52 +- .../Services/EmergencyAccessServiceTests.cs | 1416 ++++++++++++++++- 4 files changed, 1440 insertions(+), 53 deletions(-) diff --git a/src/Core/Auth/Enums/EmergencyAccessStatusType.cs b/src/Core/Auth/Enums/EmergencyAccessStatusType.cs index 7faaa11752..d817d6a950 100644 --- a/src/Core/Auth/Enums/EmergencyAccessStatusType.cs +++ b/src/Core/Auth/Enums/EmergencyAccessStatusType.cs @@ -2,9 +2,24 @@ public enum EmergencyAccessStatusType : byte { + /// + /// The user has been invited to be an emergency contact. + /// Invited = 0, + /// + /// The invited user, "grantee", has accepted the request to be an emergency contact. + /// Accepted = 1, + /// + /// The inviting user, "grantor", has approved the grantee's acceptance. + /// Confirmed = 2, + /// + /// The grantee has initiated the recovery process. + /// RecoveryInitiated = 3, + /// + /// The grantee has excercised their emergency access. + /// RecoveryApproved = 4, } diff --git a/src/Core/Auth/Services/IEmergencyAccessService.cs b/src/Core/Auth/Services/IEmergencyAccessService.cs index 2c94632510..6dd17151e6 100644 --- a/src/Core/Auth/Services/IEmergencyAccessService.cs +++ b/src/Core/Auth/Services/IEmergencyAccessService.cs @@ -3,6 +3,7 @@ using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models.Data; using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.Services; using Bit.Core.Vault.Models.Data; @@ -20,6 +21,15 @@ public interface IEmergencyAccessService Task InitiateAsync(Guid id, User initiatingUser); Task ApproveAsync(Guid id, User approvingUser); Task RejectAsync(Guid id, User rejectingUser); + /// + /// This request is made by the Grantee user to fetch the policies for the Grantor User. + /// The Grantor User has to be the owner of the organization. + /// If the Grantor user has OrganizationUserType.Owner then the policies for the _Grantor_ user + /// are returned. + /// + /// EmergencyAccess.Id being acted on + /// User making the request, this is the Grantee + /// null if the GrantorUser is not an organization owner; A list of policies otherwise. Task> GetPoliciesAsync(Guid id, User requestingUser); Task<(EmergencyAccess, User)> TakeoverAsync(Guid id, User initiatingUser); Task PasswordAsync(Guid id, User user, string newMasterPasswordHash, string key); diff --git a/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs b/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs index dda16e29fe..2418830ea7 100644 --- a/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs +++ b/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs @@ -3,7 +3,6 @@ using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Models; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Models.Data; using Bit.Core.Entities; @@ -16,7 +15,6 @@ using Bit.Core.Tokens; using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Repositories; using Bit.Core.Vault.Services; -using Microsoft.AspNetCore.Identity; namespace Bit.Core.Auth.Services; @@ -31,8 +29,6 @@ public class EmergencyAccessService : IEmergencyAccessService private readonly IMailService _mailService; private readonly IUserService _userService; private readonly GlobalSettings _globalSettings; - private readonly IPasswordHasher _passwordHasher; - private readonly IOrganizationService _organizationService; private readonly IDataProtectorTokenFactory _dataProtectorTokenizer; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; @@ -45,9 +41,7 @@ public class EmergencyAccessService : IEmergencyAccessService ICipherService cipherService, IMailService mailService, IUserService userService, - IPasswordHasher passwordHasher, GlobalSettings globalSettings, - IOrganizationService organizationService, IDataProtectorTokenFactory dataProtectorTokenizer, IRemoveOrganizationUserCommand removeOrganizationUserCommand) { @@ -59,9 +53,7 @@ public class EmergencyAccessService : IEmergencyAccessService _cipherService = cipherService; _mailService = mailService; _userService = userService; - _passwordHasher = passwordHasher; _globalSettings = globalSettings; - _organizationService = organizationService; _dataProtectorTokenizer = dataProtectorTokenizer; _removeOrganizationUserCommand = removeOrganizationUserCommand; } @@ -126,7 +118,12 @@ public class EmergencyAccessService : IEmergencyAccessService throw new BadRequestException("Emergency Access not valid."); } - if (!_dataProtectorTokenizer.TryUnprotect(token, out var data) && data.IsValid(emergencyAccessId, user.Email)) + if (!_dataProtectorTokenizer.TryUnprotect(token, out var data)) + { + throw new BadRequestException("Invalid token."); + } + + if (!data.IsValid(emergencyAccessId, user.Email)) { throw new BadRequestException("Invalid token."); } @@ -140,6 +137,8 @@ public class EmergencyAccessService : IEmergencyAccessService throw new BadRequestException("Invitation already accepted."); } + // TODO PM-21687 + // Might not be reachable since the Tokenable.IsValid() does an email comparison if (string.IsNullOrWhiteSpace(emergencyAccess.Email) || !emergencyAccess.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase)) { @@ -163,6 +162,8 @@ public class EmergencyAccessService : IEmergencyAccessService public async Task DeleteAsync(Guid emergencyAccessId, Guid grantorId) { var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); + // TODO PM-19438/PM-21687 + // Not sure why the GrantorId and the GranteeId are supposed to be the same? if (emergencyAccess == null || (emergencyAccess.GrantorId != grantorId && emergencyAccess.GranteeId != grantorId)) { throw new BadRequestException("Emergency Access not valid."); @@ -171,9 +172,9 @@ public class EmergencyAccessService : IEmergencyAccessService await _emergencyAccessRepository.DeleteAsync(emergencyAccess); } - public async Task ConfirmUserAsync(Guid emergencyAcccessId, string key, Guid confirmingUserId) + public async Task ConfirmUserAsync(Guid emergencyAccessId, string key, Guid confirmingUserId) { - var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAcccessId); + var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); if (emergencyAccess == null || emergencyAccess.Status != EmergencyAccessStatusType.Accepted || emergencyAccess.GrantorId != confirmingUserId) { @@ -224,7 +225,6 @@ public class EmergencyAccessService : IEmergencyAccessService public async Task InitiateAsync(Guid id, User initiatingUser) { var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); - if (emergencyAccess == null || emergencyAccess.GranteeId != initiatingUser.Id || emergencyAccess.Status != EmergencyAccessStatusType.Confirmed) { @@ -285,6 +285,9 @@ public class EmergencyAccessService : IEmergencyAccessService public async Task> GetPoliciesAsync(Guid id, User requestingUser) { + // TODO PM-21687 + // Should we look up policies here or just verify the EmergencyAccess is correct + // and handle policy logic else where? Should this be a query/Command? var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.Takeover)) @@ -295,7 +298,9 @@ public class EmergencyAccessService : IEmergencyAccessService var grantor = await _userRepository.GetByIdAsync(emergencyAccess.GrantorId); var grantorOrganizations = await _organizationUserRepository.GetManyByUserAsync(grantor.Id); - var isOrganizationOwner = grantorOrganizations.Any(organization => organization.Type == OrganizationUserType.Owner); + var isOrganizationOwner = grantorOrganizations + .Any(organization => organization.Type == OrganizationUserType.Owner); + var policies = isOrganizationOwner ? await _policyRepository.GetManyByUserIdAsync(grantor.Id) : null; return policies; @@ -311,7 +316,8 @@ public class EmergencyAccessService : IEmergencyAccessService } var grantor = await _userRepository.GetByIdAsync(emergencyAccess.GrantorId); - + // TODO PM-21687 + // Redundant check of the EmergencyAccessType -> checked in IsValidRequest() ln 308 if (emergencyAccess.Type == EmergencyAccessType.Takeover && grantor.UsesKeyConnector) { throw new BadRequestException("You cannot takeover an account that is using Key Connector."); @@ -336,7 +342,9 @@ public class EmergencyAccessService : IEmergencyAccessService grantor.LastPasswordChangeDate = grantor.RevisionDate; grantor.Key = key; // Disable TwoFactor providers since they will otherwise block logins - grantor.SetTwoFactorProviders(new Dictionary()); + grantor.SetTwoFactorProviders([]); + // Disable New Device Verification since it will otherwise block logins + grantor.VerifyDevices = false; await _userRepository.ReplaceAsync(grantor); // Remove grantor from all organizations unless Owner @@ -421,12 +429,22 @@ public class EmergencyAccessService : IEmergencyAccessService await _mailService.SendEmergencyAccessInviteEmailAsync(emergencyAccess, invitingUsersName, token); } - private string NameOrEmail(User user) + private static string NameOrEmail(User user) { return string.IsNullOrWhiteSpace(user.Name) ? user.Email : user.Name; } - private bool IsValidRequest(EmergencyAccess availableAccess, User requestingUser, EmergencyAccessType requestedAccessType) + + /* + * Checks if EmergencyAccess Object is null + * Checks the requesting user is the same as the granteeUser (So we are checking for proper grantee action) + * Status _must_ equal RecoveryApproved (This means the grantor has invited, the grantee has accepted, and the grantor has approved so the shared key exists but hasn't been exercised yet) + * request type must equal the type of access requested (View or Takeover) + */ + private static bool IsValidRequest( + EmergencyAccess availableAccess, + User requestingUser, + EmergencyAccessType requestedAccessType) { return availableAccess != null && availableAccess.GranteeId == requestingUser.Id && diff --git a/test/Core.Test/Auth/Services/EmergencyAccessServiceTests.cs b/test/Core.Test/Auth/Services/EmergencyAccessServiceTests.cs index 6c2352ca00..006515aafd 100644 --- a/test/Core.Test/Auth/Services/EmergencyAccessServiceTests.cs +++ b/test/Core.Test/Auth/Services/EmergencyAccessServiceTests.cs @@ -1,11 +1,17 @@ -using Bit.Core.Auth.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Auth.Entities; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Models; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Services; using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; +using Bit.Core.Tokens; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using NSubstitute; @@ -17,27 +23,21 @@ namespace Bit.Core.Test.Auth.Services; public class EmergencyAccessServiceTests { [Theory, BitAutoData] - public async Task SaveAsync_PremiumCannotUpdate( - SutProvider sutProvider, User savingUser) + public async Task InviteAsync_UserWithOutPremium_ThrowsBadRequest( + SutProvider sutProvider, User invitingUser, string email, int waitTime) { - savingUser.Premium = false; - var emergencyAccess = new EmergencyAccess - { - Type = EmergencyAccessType.Takeover, - GrantorId = savingUser.Id, - }; - - sutProvider.GetDependency().GetUserByIdAsync(savingUser.Id).Returns(savingUser); + sutProvider.GetDependency().CanAccessPremium(invitingUser).Returns(false); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.SaveAsync(emergencyAccess, savingUser)); + () => sutProvider.Sut.InviteAsync(invitingUser, email, EmergencyAccessType.Takeover, waitTime)); Assert.Contains("Not a premium user.", exception.Message); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs().CreateAsync(default); } [Theory, BitAutoData] - public async Task InviteAsync_UserWithKeyConnectorCannotUseTakeover( + public async Task InviteAsync_UserWithKeyConnector_ThrowsBadRequest( SutProvider sutProvider, User invitingUser, string email, int waitTime) { invitingUser.UsesKeyConnector = true; @@ -47,11 +47,461 @@ public class EmergencyAccessServiceTests () => sutProvider.Sut.InviteAsync(invitingUser, email, EmergencyAccessType.Takeover, waitTime)); Assert.Contains("You cannot use Emergency Access Takeover because you are using Key Connector", exception.Message); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs().CreateAsync(default); + } + + [Theory] + [BitAutoData(EmergencyAccessType.Takeover)] + [BitAutoData(EmergencyAccessType.View)] + public async Task InviteAsync_ReturnsEmergencyAccessObject( + EmergencyAccessType accessType, SutProvider sutProvider, User invitingUser, string email, int waitTime) + { + sutProvider.GetDependency().CanAccessPremium(invitingUser).Returns(true); + + var result = await sutProvider.Sut.InviteAsync(invitingUser, email, accessType, waitTime); + + Assert.NotNull(result); + Assert.Equal(accessType, result.Type); + Assert.Equal(invitingUser.Id, result.GrantorId); + Assert.Equal(email, result.Email); + Assert.Equal(EmergencyAccessStatusType.Invited, result.Status); + await sutProvider.GetDependency() + .Received(1) + .CreateAsync(Arg.Any()); + sutProvider.GetDependency>() + .Received(1) + .Protect(Arg.Any()); + await sutProvider.GetDependency() + .Received(1) + .SendEmergencyAccessInviteEmailAsync(Arg.Any(), Arg.Any(), Arg.Any()); } [Theory, BitAutoData] - public async Task ConfirmUserAsync_UserWithKeyConnectorCannotUseTakeover( + public async Task GetAsync_EmergencyAccessNull_ThrowsBadRequest( + SutProvider sutProvider, User user) + { + EmergencyAccessDetails emergencyAccess = null; + sutProvider.GetDependency() + .GetDetailsByIdGrantorIdAsync(Arg.Any(), Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.GetAsync(new Guid(), user.Id)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task ResendInviteAsync_EmergencyAccessNull_ThrowsBadRequest( + SutProvider sutProvider, + User invitingUser, + Guid emergencyAccessId) + { + EmergencyAccess emergencyAccess = null; + + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ResendInviteAsync(invitingUser, emergencyAccessId)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendEmergencyAccessInviteEmailAsync(default, default, default); + } + + [Theory, BitAutoData] + public async Task ResendInviteAsync_InvitingUserIdNotGrantorUserId_ThrowsBadRequest( + SutProvider sutProvider, + User invitingUser, + Guid emergencyAccessId) + { + var emergencyAccess = new EmergencyAccess + { + Status = EmergencyAccessStatusType.Invited, + GrantorId = Guid.NewGuid(), + Type = EmergencyAccessType.Takeover, + }; ; + + sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ResendInviteAsync(invitingUser, emergencyAccessId)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendEmergencyAccessInviteEmailAsync(default, default, default); + } + + [Theory] + [BitAutoData(EmergencyAccessStatusType.Accepted)] + [BitAutoData(EmergencyAccessStatusType.Confirmed)] + [BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)] + [BitAutoData(EmergencyAccessStatusType.RecoveryApproved)] + public async Task ResendInviteAsync_EmergencyAccessStatusInvalid_ThrowsBadRequest( + EmergencyAccessStatusType statusType, + SutProvider sutProvider, + User invitingUser, + Guid emergencyAccessId) + { + var emergencyAccess = new EmergencyAccess + { + Status = statusType, + GrantorId = invitingUser.Id, + Type = EmergencyAccessType.Takeover, + }; + + sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ResendInviteAsync(invitingUser, emergencyAccessId)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendEmergencyAccessInviteEmailAsync(default, default, default); + } + + [Theory, BitAutoData] + public async Task ResendInviteAsync_SendsInviteAsync( + SutProvider sutProvider, + User invitingUser, + Guid emergencyAccessId) + { + var emergencyAccess = new EmergencyAccess + { + Status = EmergencyAccessStatusType.Invited, + GrantorId = invitingUser.Id, + Type = EmergencyAccessType.Takeover, + }; ; + + sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(emergencyAccess); + + await sutProvider.Sut.ResendInviteAsync(invitingUser, emergencyAccessId); + sutProvider.GetDependency>() + .Received(1) + .Protect(Arg.Any()); + await sutProvider.GetDependency() + .Received(1) + .SendEmergencyAccessInviteEmailAsync(emergencyAccess, invitingUser.Name, Arg.Any()); + } + + [Theory, BitAutoData] + public async Task AcceptUserAsync_EmergencyAccessNull_ThrowsBadRequest( + SutProvider sutProvider, User acceptingUser, string token) + { + EmergencyAccess emergencyAccess = null; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.AcceptUserAsync(new Guid(), acceptingUser, token, sutProvider.GetDependency())); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task AcceptUserAsync_CannotUnprotectToken_ThrowsBadRequest( + SutProvider sutProvider, + User acceptingUser, + EmergencyAccess emergencyAccess, + string token) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency>() + .TryUnprotect(token, out Arg.Any()) + .Returns(false); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency())); + + Assert.Contains("Invalid token.", exception.Message); + } + + [Theory, BitAutoData] + public async Task AcceptUserAsync_TokenDataInvalid_ThrowsBadRequest( + SutProvider sutProvider, + User acceptingUser, + EmergencyAccess emergencyAccess, + EmergencyAccess wrongEmergencyAccess, + string token) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency>() + .TryUnprotect(token, out Arg.Any()) + .Returns(callInfo => + { + callInfo[1] = new EmergencyAccessInviteTokenable(wrongEmergencyAccess, 1); + return true; + }); + + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency())); + + Assert.Contains("Invalid token.", exception.Message); + } + + [Theory, BitAutoData] + public async Task AcceptUserAsync_AcceptedStatus_ThrowsBadRequest( + SutProvider sutProvider, + User acceptingUser, + EmergencyAccess emergencyAccess, + string token) + { + emergencyAccess.Status = EmergencyAccessStatusType.Accepted; + emergencyAccess.Email = acceptingUser.Email; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency>() + .TryUnprotect(token, out Arg.Any()) + .Returns(callInfo => + { + callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 1); + return true; + }); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency())); + + Assert.Contains("Invitation already accepted. You will receive an email when the grantor confirms you as an emergency access contact.", exception.Message); + } + + [Theory, BitAutoData] + public async Task AcceptUserAsync_NotInvitedStatus_ThrowsBadRequest( + SutProvider sutProvider, + User acceptingUser, + EmergencyAccess emergencyAccess, + string token) + { + emergencyAccess.Status = EmergencyAccessStatusType.Confirmed; + emergencyAccess.Email = acceptingUser.Email; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency>() + .TryUnprotect(token, out Arg.Any()) + .Returns(callInfo => + { + callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 1); + return true; + }); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency())); + + Assert.Contains("Invitation already accepted.", exception.Message); + } + + [Theory(Skip = "Code not reachable, Tokenable checks email match in IsValid()"), BitAutoData] + public async Task AcceptUserAsync_EmergencyAccessEmailDoesNotMatch_ThrowsBadRequest( + SutProvider sutProvider, + User acceptingUser, + EmergencyAccess emergencyAccess, + string token) + { + emergencyAccess.Status = EmergencyAccessStatusType.Invited; + emergencyAccess.Email = acceptingUser.Email; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency>() + .TryUnprotect(token, out Arg.Any()) + .Returns(callInfo => + { + callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 1); + return true; + }); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency())); + + Assert.Contains("User email does not match invite.", exception.Message); + } + + [Theory, BitAutoData] + public async Task AcceptUserAsync_ReplaceEmergencyAccess_SendsEmail_Success( + SutProvider sutProvider, + User acceptingUser, + User invitingUser, + EmergencyAccess emergencyAccess, + string token) + { + emergencyAccess.Status = EmergencyAccessStatusType.Invited; + emergencyAccess.Email = acceptingUser.Email; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetUserByIdAsync(Arg.Any()) + .Returns(invitingUser); + + sutProvider.GetDependency>() + .TryUnprotect(token, out Arg.Any()) + .Returns(callInfo => + { + callInfo[1] = new EmergencyAccessInviteTokenable(emergencyAccess, 1); + return true; + }); + + await sutProvider.Sut.AcceptUserAsync(emergencyAccess.Id, acceptingUser, token, sutProvider.GetDependency()); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(x => x.Status == EmergencyAccessStatusType.Accepted)); + + await sutProvider.GetDependency() + .Received(1) + .SendEmergencyAccessAcceptedEmailAsync(acceptingUser.Email, invitingUser.Email); + } + + [Theory, BitAutoData] + public async Task DeleteAsync_EmergencyAccessNull_ThrowsBadRequest( + SutProvider sutProvider, + User invitingUser, + EmergencyAccess emergencyAccess) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.DeleteAsync(emergencyAccess.Id, invitingUser.Id)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task DeleteAsync_EmergencyAccessGrantorIdNotEqual_ThrowsBadRequest( + SutProvider sutProvider, + User invitingUser, + EmergencyAccess emergencyAccess) + { + emergencyAccess.GrantorId = Guid.NewGuid(); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.DeleteAsync(emergencyAccess.Id, invitingUser.Id)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task DeleteAsync_EmergencyAccessGranteeIdNotEqual_ThrowsBadRequest( + SutProvider sutProvider, + User invitingUser, + EmergencyAccess emergencyAccess) + { + emergencyAccess.GranteeId = Guid.NewGuid(); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.DeleteAsync(emergencyAccess.Id, invitingUser.Id)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task DeleteAsync_EmergencyAccessIsDeleted_Success( + SutProvider sutProvider, + User user, + EmergencyAccess emergencyAccess) + { + emergencyAccess.GranteeId = user.Id; + emergencyAccess.GrantorId = user.Id; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + await sutProvider.Sut.DeleteAsync(emergencyAccess.Id, user.Id); + + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(emergencyAccess); + } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_EmergencyAccessNull_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + string key, + User grantorUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ConfirmUserAsync(emergencyAccess.Id, key, grantorUser.Id)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_EmergencyAccessStatusIsNotAccepted_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + string key, + User grantorUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated; + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.Id) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ConfirmUserAsync(emergencyAccess.Id, key, grantorUser.Id)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + + } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_EmergencyAccessGrantorIdNotEqualToConfirmingUserId_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + string key, + User grantorUser) + { + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ConfirmUserAsync(emergencyAccess.Id, key, grantorUser.Id)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_UserWithKeyConnectorCannotUseTakeover_ThrowsBadRequest( SutProvider sutProvider, User confirmingUser, string key) { confirmingUser.UsesKeyConnector = true; @@ -62,8 +512,13 @@ public class EmergencyAccessServiceTests Type = EmergencyAccessType.Takeover, }; - sutProvider.GetDependency().GetByIdAsync(confirmingUser.Id).Returns(confirmingUser); - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(emergencyAccess); + sutProvider.GetDependency() + .GetByIdAsync(confirmingUser.Id) + .Returns(confirmingUser); + + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.ConfirmUserAsync(new Guid(), key, confirmingUser.Id)); @@ -73,29 +528,210 @@ public class EmergencyAccessServiceTests } [Theory, BitAutoData] - public async Task SaveAsync_UserWithKeyConnectorCannotUseTakeover( + public async Task ConfirmUserAsync_ConfirmsAndReplacesEmergencyAccess_Success( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + string key, + User grantorUser, + User granteeUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.Accepted; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(grantorUser.Id) + .Returns(grantorUser); + + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.GranteeId.Value) + .Returns(granteeUser); + + await sutProvider.Sut.ConfirmUserAsync(emergencyAccess.Id, key, grantorUser.Id); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(x => x.Status == EmergencyAccessStatusType.Confirmed)); + + await sutProvider.GetDependency() + .Received(1) + .SendEmergencyAccessConfirmedEmailAsync(grantorUser.Name, granteeUser.Email); + } + + [Theory, BitAutoData] + public async Task SaveAsync_PremiumCannotUpdate_ThrowsBadRequest( SutProvider sutProvider, User savingUser) { - savingUser.UsesKeyConnector = true; var emergencyAccess = new EmergencyAccess { Type = EmergencyAccessType.Takeover, GrantorId = savingUser.Id, }; - var userService = sutProvider.GetDependency(); - userService.GetUserByIdAsync(savingUser.Id).Returns(savingUser); - userService.CanAccessPremium(savingUser).Returns(true); + sutProvider.GetDependency() + .CanAccessPremium(savingUser) + .Returns(false); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.SaveAsync(emergencyAccess, savingUser)); + Assert.Contains("Not a premium user.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + } + + [Theory, BitAutoData] + public async Task SaveAsync_EmergencyAccessGrantorIdNotEqualToSavingUserId_ThrowsBadRequest( + SutProvider sutProvider, User savingUser) + { + savingUser.Premium = true; + var emergencyAccess = new EmergencyAccess + { + Type = EmergencyAccessType.Takeover, + GrantorId = new Guid(), + }; + + sutProvider.GetDependency() + .GetUserByIdAsync(savingUser.Id) + .Returns(savingUser); + sutProvider.GetDependency() + .CanAccessPremium(savingUser) + .Returns(true); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(emergencyAccess, savingUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + } + + [Theory, BitAutoData] + public async Task SaveAsync_GrantorUserWithKeyConnectorCannotTakeover_ThrowsBadRequest( + SutProvider sutProvider, User grantorUser) + { + grantorUser.UsesKeyConnector = true; + var emergencyAccess = new EmergencyAccess + { + Type = EmergencyAccessType.Takeover, + GrantorId = grantorUser.Id, + }; + + var userService = sutProvider.GetDependency(); + userService.GetUserByIdAsync(grantorUser.Id).Returns(grantorUser); + userService.CanAccessPremium(grantorUser).Returns(true); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SaveAsync(emergencyAccess, grantorUser)); + Assert.Contains("You cannot use Emergency Access Takeover because you are using Key Connector", exception.Message); await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); } [Theory, BitAutoData] - public async Task InitiateAsync_UserWithKeyConnectorCannotUseTakeover( + public async Task SaveAsync_GrantorUserWithKeyConnectorCanView_SavesEmergencyAccess( + SutProvider sutProvider, User grantorUser) + { + grantorUser.UsesKeyConnector = true; + var emergencyAccess = new EmergencyAccess + { + Type = EmergencyAccessType.View, + GrantorId = grantorUser.Id, + }; + + var userService = sutProvider.GetDependency(); + userService.GetUserByIdAsync(grantorUser.Id).Returns(grantorUser); + userService.CanAccessPremium(grantorUser).Returns(true); + + await sutProvider.Sut.SaveAsync(emergencyAccess, grantorUser); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(emergencyAccess); + } + + [Theory, BitAutoData] + public async Task SaveAsync_ValidRequest_SavesEmergencyAccess( + SutProvider sutProvider, User grantorUser) + { + grantorUser.UsesKeyConnector = false; + var emergencyAccess = new EmergencyAccess + { + Type = EmergencyAccessType.Takeover, + GrantorId = grantorUser.Id, + }; + + var userService = sutProvider.GetDependency(); + userService.GetUserByIdAsync(grantorUser.Id).Returns(grantorUser); + userService.CanAccessPremium(grantorUser).Returns(true); + + await sutProvider.Sut.SaveAsync(emergencyAccess, grantorUser); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(emergencyAccess); + } + + [Theory, BitAutoData] + public async Task InitiateAsync_EmergencyAccessNull_ThrowBadRequest( + SutProvider sutProvider, User initiatingUser) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .ReplaceAsync(default); + } + + [Theory, BitAutoData] + public async Task InitiateAsync_EmergencyAccessGranteeIdNotEqual_ThrowBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User initiatingUser) + { + emergencyAccess.GranteeId = new Guid(); + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.Id) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .ReplaceAsync(default); + } + + [Theory, BitAutoData] + public async Task InitiateAsync_EmergencyAccessStatusIsNotConfirmed_ThrowBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User initiatingUser) + { + emergencyAccess.GranteeId = initiatingUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.Invited; + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.Id) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .ReplaceAsync(default); + } + + [Theory, BitAutoData] + public async Task InitiateAsync_UserWithKeyConnectorCannotUseTakeover_ThrowsBadRequest( SutProvider sutProvider, User initiatingUser, User grantor) { grantor.UsesKeyConnector = true; @@ -107,40 +743,711 @@ public class EmergencyAccessServiceTests Type = EmergencyAccessType.Takeover, }; - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(emergencyAccess); - sutProvider.GetDependency().GetByIdAsync(grantor.Id).Returns(grantor); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + sutProvider.GetDependency() + .GetByIdAsync(grantor.Id) + .Returns(grantor); var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser)); Assert.Contains("You cannot takeover an account that is using Key Connector", exception.Message); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().ReplaceAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .ReplaceAsync(default); } [Theory, BitAutoData] - public async Task TakeoverAsync_UserWithKeyConnectorCannotUseTakeover( - SutProvider sutProvider, User requestingUser, User grantor) + public async Task InitiateAsync_UserWithKeyConnectorCanView_Success( + SutProvider sutProvider, User initiatingUser, User grantor) + { + grantor.UsesKeyConnector = true; + var emergencyAccess = new EmergencyAccess + { + Status = EmergencyAccessStatusType.Confirmed, + GranteeId = initiatingUser.Id, + GrantorId = grantor.Id, + Type = EmergencyAccessType.View, + }; + + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + sutProvider.GetDependency() + .GetByIdAsync(grantor.Id) + .Returns(grantor); + + await sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(x => x.Status == EmergencyAccessStatusType.RecoveryInitiated)); + } + + [Theory, BitAutoData] + public async Task InitiateAsync_RequestIsCorrect_Success( + SutProvider sutProvider, User initiatingUser, User grantor) + { + var emergencyAccess = new EmergencyAccess + { + Status = EmergencyAccessStatusType.Confirmed, + GranteeId = initiatingUser.Id, + GrantorId = grantor.Id, + Type = EmergencyAccessType.Takeover, + }; + + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + sutProvider.GetDependency() + .GetByIdAsync(grantor.Id) + .Returns(grantor); + + await sutProvider.Sut.InitiateAsync(new Guid(), initiatingUser); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(x => x.Status == EmergencyAccessStatusType.RecoveryInitiated)); + } + + [Theory, BitAutoData] + public async Task ApproveAsync_EmergencyAccessNull_ThrowsBadrequest( + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ApproveAsync(new Guid(), null)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task ApproveAsync_EmergencyAccessGrantorIdNotEquatToApproving_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User grantorUser) + { + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ApproveAsync(emergencyAccess.Id, grantorUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory] + [BitAutoData(EmergencyAccessStatusType.Invited)] + [BitAutoData(EmergencyAccessStatusType.Accepted)] + [BitAutoData(EmergencyAccessStatusType.Confirmed)] + [BitAutoData(EmergencyAccessStatusType.RecoveryApproved)] + public async Task ApproveAsync_EmergencyAccessStatusNotRecoveryInitiated_ThrowsBadRequest( + EmergencyAccessStatusType statusType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User grantorUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = statusType; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ApproveAsync(emergencyAccess.Id, grantorUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task ApproveAsync_Success( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User grantorUser, + User granteeUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryInitiated; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(granteeUser); + + await sutProvider.Sut.ApproveAsync(emergencyAccess.Id, grantorUser); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(x => x.Status == EmergencyAccessStatusType.RecoveryApproved)); + } + + [Theory, BitAutoData] + public async Task RejectAsync_EmergencyAccessIdNull_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User GrantorUser) + { + emergencyAccess.GrantorId = GrantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.Accepted; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RejectAsync(emergencyAccess.Id, GrantorUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task RejectAsync_EmergencyAccessGrantorIdNotEqualToRequestUser_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User GrantorUser) + { + emergencyAccess.Status = EmergencyAccessStatusType.Accepted; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RejectAsync(emergencyAccess.Id, GrantorUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory] + [BitAutoData(EmergencyAccessStatusType.Invited)] + [BitAutoData(EmergencyAccessStatusType.Accepted)] + [BitAutoData(EmergencyAccessStatusType.Confirmed)] + public async Task RejectAsync_EmergencyAccessStatusNotValid_ThrowsBadRequest( + EmergencyAccessStatusType statusType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User GrantorUser) + { + emergencyAccess.GrantorId = GrantorUser.Id; + emergencyAccess.Status = statusType; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RejectAsync(emergencyAccess.Id, GrantorUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory] + [BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)] + [BitAutoData(EmergencyAccessStatusType.RecoveryApproved)] + public async Task RejectAsync_Success( + EmergencyAccessStatusType statusType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User GrantorUser, + User GranteeUser) + { + emergencyAccess.GrantorId = GrantorUser.Id; + emergencyAccess.Status = statusType; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(GranteeUser); + + await sutProvider.Sut.RejectAsync(emergencyAccess.Id, GrantorUser); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(x => x.Status == EmergencyAccessStatusType.Confirmed)); + } + + [Theory, BitAutoData] + public async Task GetPoliciesAsync_RequestNotValidEmergencyAccessNull_ThrowsBadRequest( + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.GetPoliciesAsync(default, default)); + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory] + [BitAutoData(EmergencyAccessStatusType.Invited)] + [BitAutoData(EmergencyAccessStatusType.Accepted)] + [BitAutoData(EmergencyAccessStatusType.Confirmed)] + [BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)] + public async Task GetPoliciesAsync_RequestNotValidStatusType_ThrowsBadRequest( + EmergencyAccessStatusType statusType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = statusType; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.GetPoliciesAsync(emergencyAccess.Id, granteeUser)); + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task GetPoliciesAsync_RequestNotValidType_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.View; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.GetPoliciesAsync(emergencyAccess.Id, granteeUser)); + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task GetPoliciesAsync_OrganizationUserTypeNotOwner_ReturnsNull( + OrganizationUserType userType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser, + User grantorUser, + OrganizationUser grantorOrganizationUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.GrantorId) + .Returns(grantorUser); + + grantorOrganizationUser.UserId = grantorUser.Id; + grantorOrganizationUser.Type = userType; + sutProvider.GetDependency() + .GetManyByUserAsync(grantorUser.Id) + .Returns([grantorOrganizationUser]); + + var result = await sutProvider.Sut.GetPoliciesAsync(emergencyAccess.Id, granteeUser); + Assert.Null(result); + } + + [Theory, BitAutoData] + public async Task GetPoliciesAsync_OrganizationUserEmpty_ReturnsNull( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser, + User grantorUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.GrantorId) + .Returns(grantorUser); + + sutProvider.GetDependency() + .GetManyByUserAsync(grantorUser.Id) + .Returns([]); + + + var result = await sutProvider.Sut.GetPoliciesAsync(emergencyAccess.Id, granteeUser); + Assert.Null(result); + } + + [Theory, BitAutoData] + public async Task GetPoliciesAsync_ReturnsNotNull( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser, + User grantorUser, + OrganizationUser grantorOrganizationUser) + { + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.GrantorId) + .Returns(grantorUser); + + grantorOrganizationUser.UserId = grantorUser.Id; + grantorOrganizationUser.Type = OrganizationUserType.Owner; + sutProvider.GetDependency() + .GetManyByUserAsync(grantorUser.Id) + .Returns([grantorOrganizationUser]); + + sutProvider.GetDependency() + .GetManyByUserIdAsync(grantorUser.Id) + .Returns([]); + + var result = await sutProvider.Sut.GetPoliciesAsync(emergencyAccess.Id, granteeUser); + Assert.NotNull(result); + } + + [Theory, BitAutoData] + public async Task TakeoverAsync_RequestNotValid_EmergencyAccessIsNull_ThrowsBadRequest( + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.TakeoverAsync(default, default)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task TakeoverAsync_RequestNotValid_GranteeNotEqualToRequestingUser_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.TakeoverAsync(new Guid(), granteeUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory] + [BitAutoData(EmergencyAccessStatusType.Invited)] + [BitAutoData(EmergencyAccessStatusType.Accepted)] + [BitAutoData(EmergencyAccessStatusType.Confirmed)] + [BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)] + public async Task TakeoverAsync_RequestNotValid_StatusType_ThrowsBadRequest( + EmergencyAccessStatusType statusType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = statusType; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.TakeoverAsync(new Guid(), granteeUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task TakeoverAsync_RequestNotValid_TypeIsView_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.View; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.TakeoverAsync(new Guid(), granteeUser)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task TakeoverAsync_UserWithKeyConnectorCannotUseTakeover_ThrowsBadRequest( + SutProvider sutProvider, + User granteeUser, + User grantor) { grantor.UsesKeyConnector = true; var emergencyAccess = new EmergencyAccess { GrantorId = grantor.Id, - GranteeId = requestingUser.Id, + GranteeId = granteeUser.Id, Status = EmergencyAccessStatusType.RecoveryApproved, Type = EmergencyAccessType.Takeover, }; - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(emergencyAccess); - sutProvider.GetDependency().GetByIdAsync(grantor.Id).Returns(grantor); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(grantor.Id) + .Returns(grantor); var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.TakeoverAsync(new Guid(), requestingUser)); + () => sutProvider.Sut.TakeoverAsync(new Guid(), granteeUser)); Assert.Contains("You cannot takeover an account that is using Key Connector", exception.Message); } [Theory, BitAutoData] - public async Task PasswordAsync_Disables_2FA_Providers_On_The_Grantor( + public async Task TakeoverAsync_Success_ReturnsEmergencyAccessAndGrantorUser( + SutProvider sutProvider, + User granteeUser, + User grantor) + { + grantor.UsesKeyConnector = false; + var emergencyAccess = new EmergencyAccess + { + GrantorId = grantor.Id, + GranteeId = granteeUser.Id, + Status = EmergencyAccessStatusType.RecoveryApproved, + Type = EmergencyAccessType.Takeover, + }; + + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(grantor.Id) + .Returns(grantor); + + var result = await sutProvider.Sut.TakeoverAsync(new Guid(), granteeUser); + + Assert.Equal(result.Item1, emergencyAccess); + Assert.Equal(result.Item2, grantor); + } + + [Theory, BitAutoData] + public async Task PasswordAsync_RequestNotValid_EmergencyAccessIsNull_ThrowsBadRequest( + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns((EmergencyAccess)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PasswordAsync(default, default, default, default)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task PasswordAsync_RequestNotValid_GranteeNotEqualToRequestingUser_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, default, default)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory] + [BitAutoData(EmergencyAccessStatusType.Invited)] + [BitAutoData(EmergencyAccessStatusType.Accepted)] + [BitAutoData(EmergencyAccessStatusType.Confirmed)] + [BitAutoData(EmergencyAccessStatusType.RecoveryInitiated)] + public async Task PasswordAsync_RequestNotValid_StatusType_ThrowsBadRequest( + EmergencyAccessStatusType statusType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = statusType; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, default, default)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task PasswordAsync_RequestNotValid_TypeIsView_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.View; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, default, default)); + + Assert.Contains("Emergency Access not valid.", exception.Message); + } + + [Theory, BitAutoData] + public async Task PasswordAsync_NonOrgUser_Success( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser, + User grantorUser, + string key, + string passwordHash) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.GrantorId) + .Returns(grantorUser); + + await sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, passwordHash, key); + + await sutProvider.GetDependency() + .Received(1) + .UpdatePasswordHash(grantorUser, passwordHash); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(u => u.VerifyDevices == false && u.Key == key)); + } + + [Theory] + [BitAutoData(OrganizationUserType.User)] + [BitAutoData(OrganizationUserType.Admin)] + [BitAutoData(OrganizationUserType.Custom)] + public async Task PasswordAsync_OrgUser_NotOrganizationOwner_RemovedFromOrganization_Success( + OrganizationUserType userType, + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser, + User grantorUser, + OrganizationUser organizationUser, + string key, + string passwordHash) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.GrantorId) + .Returns(grantorUser); + + organizationUser.UserId = grantorUser.Id; + organizationUser.Type = userType; + sutProvider.GetDependency() + .GetManyByUserAsync(grantorUser.Id) + .Returns([organizationUser]); + + await sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, passwordHash, key); + + await sutProvider.GetDependency() + .Received(1) + .UpdatePasswordHash(grantorUser, passwordHash); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(u => u.VerifyDevices == false && u.Key == key)); + await sutProvider.GetDependency() + .Received(1) + .RemoveUserAsync(organizationUser.OrganizationId, organizationUser.UserId.Value); + } + + [Theory, BitAutoData] + public async Task PasswordAsync_OrgUser_IsOrganizationOwner_NotRemovedFromOrganization_Success( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser, + User grantorUser, + OrganizationUser organizationUser, + string key, + string passwordHash) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.GrantorId = grantorUser.Id; + emergencyAccess.Status = EmergencyAccessStatusType.RecoveryApproved; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + sutProvider.GetDependency() + .GetByIdAsync(emergencyAccess.GrantorId) + .Returns(grantorUser); + + organizationUser.UserId = grantorUser.Id; + organizationUser.Type = OrganizationUserType.Owner; + sutProvider.GetDependency() + .GetManyByUserAsync(grantorUser.Id) + .Returns([organizationUser]); + + await sutProvider.Sut.PasswordAsync(emergencyAccess.Id, granteeUser, passwordHash, key); + + await sutProvider.GetDependency() + .Received(1) + .UpdatePasswordHash(grantorUser, passwordHash); + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(u => u.VerifyDevices == false && u.Key == key)); + await sutProvider.GetDependency() + .Received(0) + .RemoveUserAsync(organizationUser.OrganizationId, organizationUser.UserId.Value); + } + + [Theory, BitAutoData] + public async Task PasswordAsync_Disables_NewDeviceVerification_And_TwoFactorProviders_On_The_Grantor( SutProvider sutProvider, User requestingUser, User grantor) { grantor.UsesKeyConnector = true; @@ -160,12 +1467,49 @@ public class EmergencyAccessServiceTests Type = EmergencyAccessType.Takeover, }; - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(emergencyAccess); - sutProvider.GetDependency().GetByIdAsync(grantor.Id).Returns(grantor); + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + sutProvider.GetDependency() + .GetByIdAsync(grantor.Id) + .Returns(grantor); await sutProvider.Sut.PasswordAsync(Guid.NewGuid(), requestingUser, "blablahash", "blablakey"); Assert.Empty(grantor.GetTwoFactorProviders()); + Assert.False(grantor.VerifyDevices); await sutProvider.GetDependency().Received().ReplaceAsync(grantor); } + + [Theory, BitAutoData] + public async Task ViewAsync_EmergencyAccessTypeNotView_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ViewAsync(emergencyAccess.Id, granteeUser)); + } + + [Theory, BitAutoData] + public async Task GetAttachmentDownloadAsync_EmergencyAccessTypeNotView_ThrowsBadRequest( + SutProvider sutProvider, + EmergencyAccess emergencyAccess, + User granteeUser) + { + emergencyAccess.GranteeId = granteeUser.Id; + emergencyAccess.Type = EmergencyAccessType.Takeover; + sutProvider.GetDependency() + .GetByIdAsync(Arg.Any()) + .Returns(emergencyAccess); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.GetAttachmentDownloadAsync(emergencyAccess.Id, default, default, granteeUser)); + } } From 7b3e2a80f4354bcd571062d6b3f32fadb2737667 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Mon, 19 May 2025 14:53:48 -0400 Subject: [PATCH 077/114] [PM-21092] Set tax exemption to reverse charge for non-US business-use customers (#5812) * Set automatic tax to enabled and tax exempt to reverse where applicable when ff is on * Fix and add tests * Run dotnet format * Run dotnet format * PM-21745: Resolve defect * PM-21770: Resolve defect * Run dotnet format' --- .../RemoveOrganizationFromProviderCommand.cs | 30 +- .../Billing/ProviderBillingService.cs | 41 +- ...oveOrganizationFromProviderCommandTests.cs | 118 +++++- .../Billing/ProviderBillingServiceTests.cs | 293 +++++++++++--- .../Controllers/OrganizationsController.cs | 22 - .../Implementations/UpcomingInvoiceHandler.cs | 166 ++++++-- .../Services/IOrganizationService.cs | 2 - .../Implementations/OrganizationService.cs | 21 - src/Core/Billing/Constants/StripeConstants.cs | 16 +- .../Billing/Extensions/CustomerExtensions.cs | 7 +- .../SubscriptionUpdateOptionsExtensions.cs | 2 +- .../UpcomingInvoiceOptionsExtensions.cs | 2 +- .../OrganizationBillingService.cs | 91 ++++- .../PremiumUserBillingService.cs | 29 +- .../Implementations/SubscriberService.cs | 100 +++-- .../BusinessUseAutomaticTaxStrategy.cs | 2 +- .../PersonalUseAutomaticTaxStrategy.cs | 2 +- src/Core/Constants.cs | 2 +- src/Core/Services/IPaymentService.cs | 3 - .../Implementations/StripePaymentService.cs | 379 +++--------------- .../Services/SubscriberServiceTests.cs | 119 +++++- 21 files changed, 846 insertions(+), 601 deletions(-) diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs index 22a2e93642..35a00f4253 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs @@ -8,13 +8,10 @@ using Bit.Core.Billing.Constants; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; -using Bit.Core.Billing.Tax.Services; -using Bit.Core.Billing.Tax.Services.Implementations; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Services; -using Microsoft.Extensions.DependencyInjection; using Stripe; namespace Bit.Commercial.Core.AdminConsole.Providers; @@ -24,7 +21,6 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv private readonly IEventService _eventService; private readonly IMailService _mailService; private readonly IOrganizationRepository _organizationRepository; - private readonly IOrganizationService _organizationService; private readonly IProviderOrganizationRepository _providerOrganizationRepository; private readonly IStripeAdapter _stripeAdapter; private readonly IFeatureService _featureService; @@ -32,26 +28,22 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv private readonly ISubscriberService _subscriberService; private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; private readonly IPricingClient _pricingClient; - private readonly IAutomaticTaxStrategy _automaticTaxStrategy; public RemoveOrganizationFromProviderCommand( IEventService eventService, IMailService mailService, IOrganizationRepository organizationRepository, - IOrganizationService organizationService, IProviderOrganizationRepository providerOrganizationRepository, IStripeAdapter stripeAdapter, IFeatureService featureService, IProviderBillingService providerBillingService, ISubscriberService subscriberService, IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, - IPricingClient pricingClient, - [FromKeyedServices(AutomaticTaxFactory.BusinessUse)] IAutomaticTaxStrategy automaticTaxStrategy) + IPricingClient pricingClient) { _eventService = eventService; _mailService = mailService; _organizationRepository = organizationRepository; - _organizationService = organizationService; _providerOrganizationRepository = providerOrganizationRepository; _stripeAdapter = stripeAdapter; _featureService = featureService; @@ -59,7 +51,6 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv _subscriberService = subscriberService; _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; _pricingClient = pricingClient; - _automaticTaxStrategy = automaticTaxStrategy; } public async Task RemoveOrganizationFromProvider( @@ -77,7 +68,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync( providerOrganization.OrganizationId, - Array.Empty(), + [], includeProvider: false)) { throw new BadRequestException("Organization must have at least one confirmed owner."); @@ -102,7 +93,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv /// /// When a client organization is unlinked from a provider, we have to check if they're Stripe-enabled /// and, if they are, we remove their MSP discount and set their Subscription to `send_invoice`. This is because - /// the provider's payment method will be removed from their Stripe customer causing ensuing charges to fail. Lastly, + /// the provider's payment method will be removed from their Stripe customer, causing ensuing charges to fail. Lastly, /// we email the organization owners letting them know they need to add a new payment method. /// private async Task ResetOrganizationBillingAsync( @@ -142,15 +133,18 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv Items = [new SubscriptionItemOptions { Price = plan.PasswordManager.StripeSeatPlanId, Quantity = organization.Seats }] }; - if (_featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements)) + var setNonUSBusinessUseToReverseCharge = _featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); + + if (setNonUSBusinessUseToReverseCharge) { - _automaticTaxStrategy.SetCreateOptions(subscriptionCreateOptions, customer); + subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; } - else + else if (customer.HasRecognizedTaxLocation()) { - subscriptionCreateOptions.AutomaticTax ??= new SubscriptionAutomaticTaxOptions + subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { - Enabled = true + Enabled = customer.Address.Country == "US" || + customer.TaxIds.Any() }; } @@ -187,7 +181,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv await _mailService.SendProviderUpdatePaymentMethod( organization.Id, organization.Name, - provider.Name, + provider.Name!, organizationOwnerEmails); } } diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs index fe6b8d4617..c8d6505183 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs @@ -18,7 +18,6 @@ using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Contracts; using Bit.Core.Billing.Tax.Models; using Bit.Core.Billing.Tax.Services; -using Bit.Core.Billing.Tax.Services.Implementations; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; @@ -27,7 +26,6 @@ using Bit.Core.Services; using Bit.Core.Settings; using Braintree; using CsvHelper; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Stripe; @@ -52,8 +50,7 @@ public class ProviderBillingService( ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, ISubscriberService subscriberService, - ITaxService taxService, - [FromKeyedServices(AutomaticTaxFactory.BusinessUse)] IAutomaticTaxStrategy automaticTaxStrategy) + ITaxService taxService) : IProviderBillingService { public async Task AddExistingOrganization( @@ -128,7 +125,7 @@ public class ProviderBillingService( /* * We have to scale the provider's seats before the ProviderOrganization - * row is inserted so the added organization's seats don't get double counted. + * row is inserted so the added organization's seats don't get double-counted. */ await ScaleSeats(provider, organization.PlanType, organization.Seats!.Value); @@ -236,7 +233,7 @@ public class ProviderBillingService( var providerCustomer = await subscriberService.GetCustomerOrThrow(provider, new CustomerGetOptions { - Expand = ["tax_ids"] + Expand = ["tax", "tax_ids"] }); var providerTaxId = providerCustomer.TaxIds.FirstOrDefault(); @@ -284,6 +281,13 @@ public class ProviderBillingService( ] }; + var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); + + if (setNonUSBusinessUseToReverseCharge && providerCustomer.Address is not { Country: "US" }) + { + customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse; + } + var customer = await stripeAdapter.CustomerCreateAsync(customerCreateOptions); organization.GatewayCustomerId = customer.Id; @@ -520,6 +524,13 @@ public class ProviderBillingService( } }; + var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); + + if (setNonUSBusinessUseToReverseCharge && taxInfo.BillingAddressCountry != "US") + { + options.TaxExempt = StripeConstants.TaxExempt.Reverse; + } + if (!string.IsNullOrEmpty(taxInfo.TaxIdNumber)) { var taxIdType = taxService.GetStripeTaxCode( @@ -531,6 +542,7 @@ public class ProviderBillingService( logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.", taxInfo.BillingAddressCountry, taxInfo.TaxIdNumber); + throw new BadRequestException("billingTaxIdTypeInferenceError"); } @@ -718,14 +730,21 @@ public class ProviderBillingService( TrialPeriodDays = trialPeriodDays }; - if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements)) - { - automaticTaxStrategy.SetCreateOptions(subscriptionCreateOptions, customer); - } - else + var setNonUSBusinessUseToReverseCharge = + featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); + + if (setNonUSBusinessUseToReverseCharge) { subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; } + else if (customer.HasRecognizedTaxLocation()) + { + subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions + { + Enabled = customer.Address.Country == "US" || + customer.TaxIds.Any() + }; + } try { diff --git a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs index b450bf5d7f..dd40d7d943 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs @@ -1,4 +1,5 @@ using Bit.Commercial.Core.AdminConsole.Providers; +using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; @@ -8,7 +9,6 @@ using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; using Bit.Core.Billing.Services; -using Bit.Core.Billing.Tax.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; @@ -224,31 +224,115 @@ public class RemoveOrganizationFromProviderCommandTests var stripeAdapter = sutProvider.GetDependency(); + stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is(options => + options.Description == string.Empty && + options.Email == organization.BillingEmail && + options.Expand[0] == "tax" && + options.Expand[1] == "tax_ids")).Returns(new Customer + { + Id = "customer_id", + Address = new Address + { + Country = "US" + } + }); + stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(new Subscription { Id = "subscription_id" }); - sutProvider.GetDependency() - .When(x => x.SetCreateOptions( - Arg.Is(options => - options.Customer == organization.GatewayCustomerId && - options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice && - options.DaysUntilDue == 30 && - options.Metadata["organizationId"] == organization.Id.ToString() && - options.OffSession == true && - options.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations && - options.Items.First().Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId && - options.Items.First().Quantity == organization.Seats) - , Arg.Any())) - .Do(x => + await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization); + + await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is(options => + options.Customer == organization.GatewayCustomerId && + options.CollectionMethod == StripeConstants.CollectionMethod.SendInvoice && + options.DaysUntilDue == 30 && + options.AutomaticTax.Enabled == true && + options.Metadata["organizationId"] == organization.Id.ToString() && + options.OffSession == true && + options.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations && + options.Items.First().Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId && + options.Items.First().Quantity == organization.Seats)); + + await sutProvider.GetDependency().Received(1) + .ScaleSeats(provider, organization.PlanType, -organization.Seats ?? 0); + + await organizationRepository.Received(1).ReplaceAsync(Arg.Is( + org => + org.BillingEmail == "a@example.com" && + org.GatewaySubscriptionId == "subscription_id" && + org.Status == OrganizationStatusType.Created)); + + await sutProvider.GetDependency().Received(1) + .DeleteAsync(providerOrganization); + + await sutProvider.GetDependency().Received(1) + .LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed); + + await sutProvider.GetDependency().Received(1) + .SendProviderUpdatePaymentMethod( + organization.Id, + organization.Name, + provider.Name, + Arg.Is>(emails => emails.FirstOrDefault() == "a@example.com")); + } + + [Theory, BitAutoData] + public async Task RemoveOrganizationFromProvider_OrganizationStripeEnabled_ConsolidatedBilling_ReverseCharge_MakesCorrectInvocations( + Provider provider, + ProviderOrganization providerOrganization, + Organization organization, + SutProvider sutProvider) + { + provider.Status = ProviderStatusType.Billable; + + providerOrganization.ProviderId = provider.Id; + + organization.Status = OrganizationStatusType.Managed; + + organization.PlanType = PlanType.TeamsMonthly; + + var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly); + + sutProvider.GetDependency().GetPlanOrThrow(PlanType.TeamsMonthly).Returns(teamsMonthlyPlan); + + sutProvider.GetDependency().HasConfirmedOwnersExceptAsync( + providerOrganization.OrganizationId, + [], + includeProvider: false) + .Returns(true); + + var organizationRepository = sutProvider.GetDependency(); + + organizationRepository.GetOwnerEmailAddressesById(organization.Id).Returns([ + "a@example.com", + "b@example.com" + ]); + + var stripeAdapter = sutProvider.GetDependency(); + + stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, Arg.Is(options => + options.Description == string.Empty && + options.Email == organization.BillingEmail && + options.Expand[0] == "tax" && + options.Expand[1] == "tax_ids")).Returns(new Customer { - x.Arg().AutomaticTax = new SubscriptionAutomaticTaxOptions + Id = "customer_id", + Address = new Address { - Enabled = true - }; + Country = "US" + } }); + stripeAdapter.SubscriptionCreateAsync(Arg.Any()).Returns(new Subscription + { + Id = "subscription_id" + }); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true); + await sutProvider.Sut.RemoveOrganizationFromProvider(provider, providerOrganization, organization); await stripeAdapter.Received(1).SubscriptionCreateAsync(Arg.Is(options => diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs index 2199bc4bfe..92094d026e 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs @@ -262,7 +262,7 @@ public class ProviderBillingServiceTests }; sutProvider.GetDependency().GetCustomerOrThrow(provider, Arg.Is( - options => options.Expand.FirstOrDefault() == "tax_ids")) + options => options.Expand.Contains("tax") && options.Expand.Contains("tax_ids"))) .Returns(providerCustomer); sutProvider.GetDependency().BaseServiceUri @@ -312,6 +312,91 @@ public class ProviderBillingServiceTests org => org.GatewayCustomerId == "customer_id")); } + [Theory, BitAutoData] + public async Task CreateCustomer_ForClientOrg_ReverseCharge_Succeeds( + Provider provider, + Organization organization, + SutProvider sutProvider) + { + organization.GatewayCustomerId = null; + organization.Name = "Name"; + organization.BusinessName = "BusinessName"; + + var providerCustomer = new Customer + { + Address = new Address + { + Country = "CA", + PostalCode = "12345", + Line1 = "123 Main St.", + Line2 = "Unit 4", + City = "Fake Town", + State = "Fake State" + }, + TaxIds = new StripeList + { + Data = + [ + new TaxId { Type = "TYPE", Value = "VALUE" } + ] + } + }; + + sutProvider.GetDependency().GetCustomerOrThrow(provider, Arg.Is( + options => options.Expand.Contains("tax") && options.Expand.Contains("tax_ids"))) + .Returns(providerCustomer); + + sutProvider.GetDependency().BaseServiceUri + .Returns(new Bit.Core.Settings.GlobalSettings.BaseServiceUriSettings(new Bit.Core.Settings.GlobalSettings()) + { + CloudRegion = "US" + }); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true); + + sutProvider.GetDependency().CustomerCreateAsync(Arg.Is( + options => + options.Address.Country == providerCustomer.Address.Country && + options.Address.PostalCode == providerCustomer.Address.PostalCode && + options.Address.Line1 == providerCustomer.Address.Line1 && + options.Address.Line2 == providerCustomer.Address.Line2 && + options.Address.City == providerCustomer.Address.City && + options.Address.State == providerCustomer.Address.State && + options.Name == organization.DisplayName() && + options.Description == $"{provider.Name} Client Organization" && + options.Email == provider.BillingEmail && + options.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Organization" && + options.InvoiceSettings.CustomFields.FirstOrDefault().Value == "Name" && + options.Metadata["region"] == "US" && + options.TaxIdData.FirstOrDefault().Type == providerCustomer.TaxIds.FirstOrDefault().Type && + options.TaxIdData.FirstOrDefault().Value == providerCustomer.TaxIds.FirstOrDefault().Value && + options.TaxExempt == StripeConstants.TaxExempt.Reverse)) + .Returns(new Customer { Id = "customer_id" }); + + await sutProvider.Sut.CreateCustomerForClientOrganization(provider, organization); + + await sutProvider.GetDependency().Received(1).CustomerCreateAsync(Arg.Is( + options => + options.Address.Country == providerCustomer.Address.Country && + options.Address.PostalCode == providerCustomer.Address.PostalCode && + options.Address.Line1 == providerCustomer.Address.Line1 && + options.Address.Line2 == providerCustomer.Address.Line2 && + options.Address.City == providerCustomer.Address.City && + options.Address.State == providerCustomer.Address.State && + options.Name == organization.DisplayName() && + options.Description == $"{provider.Name} Client Organization" && + options.Email == provider.BillingEmail && + options.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Organization" && + options.InvoiceSettings.CustomFields.FirstOrDefault().Value == "Name" && + options.Metadata["region"] == "US" && + options.TaxIdData.FirstOrDefault().Type == providerCustomer.TaxIds.FirstOrDefault().Type && + options.TaxIdData.FirstOrDefault().Value == providerCustomer.TaxIds.FirstOrDefault().Value)); + + await sutProvider.GetDependency().Received(1).ReplaceAsync(Arg.Is( + org => org.GatewayCustomerId == "customer_id")); + } + #endregion #region GenerateClientInvoiceReport @@ -1182,6 +1267,62 @@ public class ProviderBillingServiceTests Assert.Equivalent(expected, actual); } + [Theory, BitAutoData] + public async Task SetupCustomer_WithCard_ReverseCharge_Success( + SutProvider sutProvider, + Provider provider, + TaxInfo taxInfo) + { + provider.Name = "MSP"; + + sutProvider.GetDependency() + .GetStripeTaxCode(Arg.Is( + p => p == taxInfo.BillingAddressCountry), + Arg.Is(p => p == taxInfo.TaxIdNumber)) + .Returns(taxInfo.TaxIdType); + + taxInfo.BillingAddressCountry = "AD"; + + var stripeAdapter = sutProvider.GetDependency(); + + var expected = new Customer + { + Id = "customer_id", + Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } + }; + + var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.Card, "token"); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true); + + stripeAdapter.CustomerCreateAsync(Arg.Is(o => + o.Address.Country == taxInfo.BillingAddressCountry && + o.Address.PostalCode == taxInfo.BillingAddressPostalCode && + o.Address.Line1 == taxInfo.BillingAddressLine1 && + o.Address.Line2 == taxInfo.BillingAddressLine2 && + o.Address.City == taxInfo.BillingAddressCity && + o.Address.State == taxInfo.BillingAddressState && + o.Description == WebUtility.HtmlDecode(provider.BusinessName) && + o.Email == provider.BillingEmail && + o.PaymentMethod == tokenizedPaymentSource.Token && + o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentSource.Token && + o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" && + o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" && + o.Metadata["region"] == "" && + o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType && + o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber && + o.TaxExempt == StripeConstants.TaxExempt.Reverse)) + .Returns(expected); + + var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource); + + Assert.Equivalent(expected, actual); + } + [Theory, BitAutoData] public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid( SutProvider sutProvider, @@ -1307,7 +1448,7 @@ public class ProviderBillingServiceTests .Returns(new Customer { Id = "customer_id", - Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } + Address = new Address { Country = "US" } }); var providerPlans = new List @@ -1359,7 +1500,7 @@ public class ProviderBillingServiceTests var customer = new Customer { Id = "customer_id", - Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } + Address = new Address { Country = "US" } }; sutProvider.GetDependency() .GetCustomerOrThrow( @@ -1399,19 +1540,6 @@ public class ProviderBillingServiceTests var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active }; - sutProvider.GetDependency() - .When(x => x.SetCreateOptions( - Arg.Is(options => - options.Customer == "customer_id") - , Arg.Is(p => p == customer))) - .Do(x => - { - x.Arg().AutomaticTax = new SubscriptionAutomaticTaxOptions - { - Enabled = true - }; - }); - sutProvider.GetDependency().SubscriptionCreateAsync(Arg.Is( sub => sub.AutomaticTax.Enabled == true && @@ -1443,11 +1571,11 @@ public class ProviderBillingServiceTests var customer = new Customer { Id = "customer_id", + Address = new Address { Country = "US" }, InvoiceSettings = new CustomerInvoiceSettings { DefaultPaymentMethodId = "pm_123" - }, - Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } + } }; sutProvider.GetDependency() @@ -1488,19 +1616,6 @@ public class ProviderBillingServiceTests var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active }; - sutProvider.GetDependency() - .When(x => x.SetCreateOptions( - Arg.Is(options => - options.Customer == "customer_id") - , Arg.Is(p => p == customer))) - .Do(x => - { - x.Arg().AutomaticTax = new SubscriptionAutomaticTaxOptions - { - Enabled = true - }; - }); - sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); @@ -1536,9 +1651,9 @@ public class ProviderBillingServiceTests var customer = new Customer { Id = "customer_id", + Address = new Address { Country = "US" }, InvoiceSettings = new CustomerInvoiceSettings(), - Metadata = new Dictionary(), - Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } + Metadata = new Dictionary() }; sutProvider.GetDependency() @@ -1579,19 +1694,6 @@ public class ProviderBillingServiceTests var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active }; - sutProvider.GetDependency() - .When(x => x.SetCreateOptions( - Arg.Is(options => - options.Customer == "customer_id") - , Arg.Is(p => p == customer))) - .Do(x => - { - x.Arg().AutomaticTax = new SubscriptionAutomaticTaxOptions - { - Enabled = true - }; - }); - sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); @@ -1646,12 +1748,15 @@ public class ProviderBillingServiceTests var customer = new Customer { Id = "customer_id", + Address = new Address + { + Country = "US" + }, InvoiceSettings = new CustomerInvoiceSettings(), Metadata = new Dictionary { ["btCustomerId"] = "braintree_customer_id" - }, - Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported } + } }; sutProvider.GetDependency() @@ -1692,22 +1797,92 @@ public class ProviderBillingServiceTests var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active }; - sutProvider.GetDependency() - .When(x => x.SetCreateOptions( - Arg.Is(options => - options.Customer == "customer_id") - , Arg.Is(p => p == customer))) - .Do(x => + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + + sutProvider.GetDependency().SubscriptionCreateAsync(Arg.Is( + sub => + sub.AutomaticTax.Enabled == true && + sub.CollectionMethod == StripeConstants.CollectionMethod.ChargeAutomatically && + sub.Customer == "customer_id" && + sub.DaysUntilDue == null && + sub.Items.Count == 2 && + sub.Items.ElementAt(0).Price == ProviderPriceAdapter.MSP.Active.Teams && + sub.Items.ElementAt(0).Quantity == 100 && + sub.Items.ElementAt(1).Price == ProviderPriceAdapter.MSP.Active.Enterprise && + sub.Items.ElementAt(1).Quantity == 100 && + sub.Metadata["providerId"] == provider.Id.ToString() && + sub.OffSession == true && + sub.ProrationBehavior == StripeConstants.ProrationBehavior.CreateProrations && + sub.TrialPeriodDays == 14)).Returns(expected); + + var actual = await sutProvider.Sut.SetupSubscription(provider); + + Assert.Equivalent(expected, actual); + } + + [Theory, BitAutoData] + public async Task SetupSubscription_ReverseCharge_Succeeds( + SutProvider sutProvider, + Provider provider) + { + provider.Type = ProviderType.Msp; + provider.GatewaySubscriptionId = null; + + var customer = new Customer + { + Id = "customer_id", + Address = new Address { Country = "CA" }, + InvoiceSettings = new CustomerInvoiceSettings { - x.Arg().AutomaticTax = new SubscriptionAutomaticTaxOptions - { - Enabled = true - }; - }); + DefaultPaymentMethodId = "pm_123" + } + }; + + sutProvider.GetDependency() + .GetCustomerOrThrow( + provider, + Arg.Is(p => p.Expand.Contains("tax") || p.Expand.Contains("tax_ids"))).Returns(customer); + + var providerPlans = new List + { + new() + { + Id = Guid.NewGuid(), + ProviderId = provider.Id, + PlanType = PlanType.TeamsMonthly, + SeatMinimum = 100, + PurchasedSeats = 0, + AllocatedSeats = 0 + }, + new() + { + Id = Guid.NewGuid(), + ProviderId = provider.Id, + PlanType = PlanType.EnterpriseMonthly, + SeatMinimum = 100, + PurchasedSeats = 0, + AllocatedSeats = 0 + } + }; + + foreach (var plan in providerPlans) + { + sutProvider.GetDependency().GetPlanOrThrow(plan.PlanType) + .Returns(StaticStore.GetPlan(plan.PlanType)); + } + + sutProvider.GetDependency().GetByProviderId(provider.Id) + .Returns(providerPlans); + + var expected = new Subscription { Id = "subscription_id", Status = StripeConstants.SubscriptionStatus.Active }; sutProvider.GetDependency() .IsEnabled(FeatureFlagKeys.PM19956_RequireProviderPaymentMethodDuringSetup).Returns(true); + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true); + sutProvider.GetDependency().SubscriptionCreateAsync(Arg.Is( sub => sub.AutomaticTax.Enabled == true && diff --git a/src/Api/Billing/Controllers/OrganizationsController.cs b/src/Api/Billing/Controllers/OrganizationsController.cs index 510f6c2835..bd5ab8cef4 100644 --- a/src/Api/Billing/Controllers/OrganizationsController.cs +++ b/src/Api/Billing/Controllers/OrganizationsController.cs @@ -109,28 +109,6 @@ public class OrganizationsController( return license; } - [HttpPost("{id:guid}/payment")] - [SelfHosted(NotSelfHostedOnly = true)] - public async Task PostPayment(Guid id, [FromBody] PaymentRequestModel model) - { - if (!await currentContext.EditPaymentMethods(id)) - { - throw new NotFoundException(); - } - - await organizationService.ReplacePaymentMethodAsync(id, model.PaymentToken, - model.PaymentMethodType.Value, new TaxInfo - { - BillingAddressLine1 = model.Line1, - BillingAddressLine2 = model.Line2, - BillingAddressState = model.State, - BillingAddressCity = model.City, - BillingAddressPostalCode = model.PostalCode, - BillingAddressCountry = model.Country, - TaxIdNumber = model.TaxId, - }); - } - [HttpPost("{id:guid}/upgrade")] [SelfHosted(NotSelfHostedOnly = true)] public async Task PostUpgrade(Guid id, [FromBody] OrganizationUpgradeRequestModel model) diff --git a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs index 4c27098f38..e31d1dceb7 100644 --- a/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs +++ b/src/Billing/Services/Implementations/UpcomingInvoiceHandler.cs @@ -1,11 +1,11 @@ using Bit.Core; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services.Contracts; -using Bit.Core.Billing.Tax.Services; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; @@ -25,8 +25,7 @@ public class UpcomingInvoiceHandler( IStripeEventService stripeEventService, IStripeEventUtilityService stripeEventUtilityService, IUserRepository userRepository, - IValidateSponsorshipCommand validateSponsorshipCommand, - IAutomaticTaxFactory automaticTaxFactory) + IValidateSponsorshipCommand validateSponsorshipCommand) : IUpcomingInvoiceHandler { public async Task HandleAsync(Event parsedEvent) @@ -46,6 +45,8 @@ public class UpcomingInvoiceHandler( var (organizationId, userId, providerId) = stripeEventUtilityService.GetIdsFromMetadata(subscription.Metadata); + var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); + if (organizationId.HasValue) { var organization = await organizationRepository.GetByIdAsync(organizationId.Value); @@ -55,7 +56,7 @@ public class UpcomingInvoiceHandler( return; } - await TryEnableAutomaticTaxAsync(subscription); + await AlignOrganizationTaxConcernsAsync(organization, subscription, parsedEvent.Id, setNonUSBusinessUseToReverseCharge); var plan = await pricingClient.GetPlanOrThrow(organization.PlanType); @@ -100,7 +101,25 @@ public class UpcomingInvoiceHandler( return; } - await TryEnableAutomaticTaxAsync(subscription); + if (!subscription.AutomaticTax.Enabled && subscription.Customer.HasRecognizedTaxLocation()) + { + try + { + await stripeFacade.UpdateSubscription(subscription.Id, + new SubscriptionUpdateOptions + { + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } + }); + } + catch (Exception exception) + { + logger.LogError( + exception, + "Failed to set user's ({UserID}) subscription to automatic tax while processing event with ID {EventID}", + user.Id, + parsedEvent.Id); + } + } if (user.Premium) { @@ -116,7 +135,7 @@ public class UpcomingInvoiceHandler( return; } - await TryEnableAutomaticTaxAsync(subscription); + await AlignProviderTaxConcernsAsync(provider, subscription, parsedEvent.Id, setNonUSBusinessUseToReverseCharge); await SendUpcomingInvoiceEmailsAsync(new List { provider.BillingEmail }, invoice); } @@ -139,50 +158,123 @@ public class UpcomingInvoiceHandler( } } - private async Task TryEnableAutomaticTaxAsync(Subscription subscription) + private async Task AlignOrganizationTaxConcernsAsync( + Organization organization, + Subscription subscription, + string eventId, + bool setNonUSBusinessUseToReverseCharge) { - if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements)) - { - var automaticTaxParameters = new AutomaticTaxFactoryParameters(subscription.Items.Select(x => x.Price.Id)); - var automaticTaxStrategy = await automaticTaxFactory.CreateAsync(automaticTaxParameters); - var updateOptions = automaticTaxStrategy.GetUpdateOptions(subscription); + var nonUSBusinessUse = + organization.PlanType.GetProductTier() != ProductTierType.Families && + subscription.Customer.Address.Country != "US"; - if (updateOptions == null) + bool setAutomaticTaxToEnabled; + + if (setNonUSBusinessUseToReverseCharge) + { + if (nonUSBusinessUse && subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse) { - return; + try + { + await stripeFacade.UpdateCustomer(subscription.CustomerId, + new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse }); + } + catch (Exception exception) + { + logger.LogError( + exception, + "Failed to set organization's ({OrganizationID}) to reverse tax exemption while processing event with ID {EventID}", + organization.Id, + eventId); + } } - await stripeFacade.UpdateSubscription(subscription.Id, updateOptions); - return; + setAutomaticTaxToEnabled = true; } - - if (subscription.AutomaticTax.Enabled || - !subscription.Customer.HasBillingLocation() || - await IsNonTaxableNonUSBusinessUseSubscription(subscription)) + else { - return; + setAutomaticTaxToEnabled = + subscription.Customer.HasRecognizedTaxLocation() && + (subscription.Customer.Address.Country == "US" || + (nonUSBusinessUse && subscription.Customer.TaxIds.Any())); } - await stripeFacade.UpdateSubscription(subscription.Id, - new SubscriptionUpdateOptions + if (!subscription.AutomaticTax.Enabled && setAutomaticTaxToEnabled) + { + try { - DefaultTaxRates = [], - AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } - }); + await stripeFacade.UpdateSubscription(subscription.Id, + new SubscriptionUpdateOptions + { + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } + }); + } + catch (Exception exception) + { + logger.LogError( + exception, + "Failed to set organization's ({OrganizationID}) subscription to automatic tax while processing event with ID {EventID}", + organization.Id, + eventId); + } + } + } - return; + private async Task AlignProviderTaxConcernsAsync( + Provider provider, + Subscription subscription, + string eventId, + bool setNonUSBusinessUseToReverseCharge) + { + bool setAutomaticTaxToEnabled; - async Task IsNonTaxableNonUSBusinessUseSubscription(Subscription localSubscription) + if (setNonUSBusinessUseToReverseCharge) { - var familyPriceIds = (await Task.WhenAll( - pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019), - pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually))) - .Select(plan => plan.PasswordManager.StripePlanId); + if (subscription.Customer.Address.Country != "US" && subscription.Customer.TaxExempt != StripeConstants.TaxExempt.Reverse) + { + try + { + await stripeFacade.UpdateCustomer(subscription.CustomerId, + new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse }); + } + catch (Exception exception) + { + logger.LogError( + exception, + "Failed to set provider's ({ProviderID}) to reverse tax exemption while processing event with ID {EventID}", + provider.Id, + eventId); + } + } - return localSubscription.Customer.Address.Country != "US" && - localSubscription.Metadata.ContainsKey(StripeConstants.MetadataKeys.OrganizationId) && - !localSubscription.Items.Select(item => item.Price.Id).Intersect(familyPriceIds).Any() && - !localSubscription.Customer.TaxIds.Any(); + setAutomaticTaxToEnabled = true; + } + else + { + setAutomaticTaxToEnabled = + subscription.Customer.HasRecognizedTaxLocation() && + (subscription.Customer.Address.Country == "US" || + subscription.Customer.TaxIds.Any()); + } + + if (!subscription.AutomaticTax.Enabled && setAutomaticTaxToEnabled) + { + try + { + await stripeFacade.UpdateSubscription(subscription.Id, + new SubscriptionUpdateOptions + { + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } + }); + } + catch (Exception exception) + { + logger.LogError( + exception, + "Failed to set provider's ({ProviderID}) subscription to automatic tax while processing event with ID {EventID}", + provider.Id, + eventId); + } } } } diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index 1e53be734e..8baad23f65 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -11,8 +11,6 @@ namespace Bit.Core.Services; public interface IOrganizationService { - Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, PaymentMethodType paymentMethodType, - TaxInfo taxInfo); Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null); Task ReinstateSubscriptionAsync(Guid organizationId); Task AdjustStorageAsync(Guid organizationId, short storageAdjustmentGb); diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 1ced923b45..4e9d9bdb8a 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -144,27 +144,6 @@ public class OrganizationService : IOrganizationService _sendOrganizationInvitesCommand = sendOrganizationInvitesCommand; } - public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken, - PaymentMethodType paymentMethodType, TaxInfo taxInfo) - { - var organization = await GetOrgById(organizationId); - if (organization == null) - { - throw new NotFoundException(); - } - - await _paymentService.SaveTaxInfoAsync(organization, taxInfo); - var updated = await _paymentService.UpdatePaymentMethodAsync( - organization, - paymentMethodType, - paymentToken, - taxInfo); - if (updated) - { - await ReplaceAndUpdateCacheAsync(organization); - } - } - public async Task CancelSubscriptionAsync(Guid organizationId, bool? endOfPeriod = null) { var organization = await GetOrgById(organizationId); diff --git a/src/Core/Billing/Constants/StripeConstants.cs b/src/Core/Billing/Constants/StripeConstants.cs index c3e3ec6c30..28f4dea4b2 100644 --- a/src/Core/Billing/Constants/StripeConstants.cs +++ b/src/Core/Billing/Constants/StripeConstants.cs @@ -2,10 +2,6 @@ public static class StripeConstants { - public static class Prices - { - public const string StoragePlanPersonal = "personal-storage-gb-annually"; - } public static class AutomaticTaxStatus { public const string Failed = "failed"; @@ -69,6 +65,11 @@ public static class StripeConstants public const string USBankAccount = "us_bank_account"; } + public static class Prices + { + public const string StoragePlanPersonal = "personal-storage-gb-annually"; + } + public static class ProrationBehavior { public const string AlwaysInvoice = "always_invoice"; @@ -88,6 +89,13 @@ public static class StripeConstants public const string Paused = "paused"; } + public static class TaxExempt + { + public const string Exempt = "exempt"; + public const string None = "none"; + public const string Reverse = "reverse"; + } + public static class ValidateTaxLocationTiming { public const string Deferred = "deferred"; diff --git a/src/Core/Billing/Extensions/CustomerExtensions.cs b/src/Core/Billing/Extensions/CustomerExtensions.cs index 3e0c1ea0fb..aa22331f7c 100644 --- a/src/Core/Billing/Extensions/CustomerExtensions.cs +++ b/src/Core/Billing/Extensions/CustomerExtensions.cs @@ -15,12 +15,7 @@ public static class CustomerExtensions } }; - /// - /// Determines if a Stripe customer supports automatic tax - /// - /// - /// - public static bool HasTaxLocationVerified(this Customer customer) => + public static bool HasRecognizedTaxLocation(this Customer customer) => customer?.Tax?.AutomaticTax != StripeConstants.AutomaticTaxStatus.UnrecognizedLocation; public static decimal GetBillingBalance(this Customer customer) diff --git a/src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs b/src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs index d70af78fa8..22a715733b 100644 --- a/src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs +++ b/src/Core/Billing/Extensions/SubscriptionUpdateOptionsExtensions.cs @@ -22,7 +22,7 @@ public static class SubscriptionUpdateOptionsExtensions } // We might only need to check the automatic tax status. - if (!customer.HasTaxLocationVerified() && string.IsNullOrWhiteSpace(customer.Address?.Country)) + if (!customer.HasRecognizedTaxLocation() && string.IsNullOrWhiteSpace(customer.Address?.Country)) { return false; } diff --git a/src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs b/src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs index 88df5638c9..d00b5b46a4 100644 --- a/src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs +++ b/src/Core/Billing/Extensions/UpcomingInvoiceOptionsExtensions.cs @@ -22,7 +22,7 @@ public static class UpcomingInvoiceOptionsExtensions } // We might only need to check the automatic tax status. - if (!customer.HasTaxLocationVerified() && string.IsNullOrWhiteSpace(customer.Address?.Country)) + if (!customer.HasRecognizedTaxLocation() && string.IsNullOrWhiteSpace(customer.Address?.Country)) { return false; } diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs index 20f6105c2a..95df34dfd4 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs @@ -1,11 +1,11 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services.Contracts; using Bit.Core.Billing.Tax.Models; using Bit.Core.Billing.Tax.Services; using Bit.Core.Enums; @@ -35,16 +35,15 @@ public class OrganizationBillingService( ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, ISubscriberService subscriberService, - ITaxService taxService, - IAutomaticTaxFactory automaticTaxFactory) : IOrganizationBillingService + ITaxService taxService) : IOrganizationBillingService { public async Task Finalize(OrganizationSale sale) { var (organization, customerSetup, subscriptionSetup) = sale; var customer = string.IsNullOrEmpty(organization.GatewayCustomerId) && customerSetup != null - ? await CreateCustomerAsync(organization, customerSetup) - : await subscriberService.GetCustomerOrThrow(organization, new CustomerGetOptions { Expand = ["tax", "tax_ids"] }); + ? await CreateCustomerAsync(organization, customerSetup, subscriptionSetup.PlanType) + : await GetCustomerWhileEnsuringCorrectTaxExemptionAsync(organization, subscriptionSetup); var subscription = await CreateSubscriptionAsync(organization.Id, customer, subscriptionSetup); @@ -121,7 +120,8 @@ public class OrganizationBillingService( subscription.CurrentPeriodEnd); } - public async Task UpdatePaymentMethod( + public async Task + UpdatePaymentMethod( Organization organization, TokenizedPaymentSource tokenizedPaymentSource, TaxInformation taxInformation) @@ -151,8 +151,11 @@ public class OrganizationBillingService( private async Task CreateCustomerAsync( Organization organization, - CustomerSetup customerSetup) + CustomerSetup customerSetup, + PlanType? updatedPlanType = null) { + var planType = updatedPlanType ?? organization.PlanType; + var displayName = organization.DisplayName(); var customerCreateOptions = new CustomerCreateOptions @@ -212,13 +215,24 @@ public class OrganizationBillingService( City = customerSetup.TaxInformation.City, PostalCode = customerSetup.TaxInformation.PostalCode, State = customerSetup.TaxInformation.State, - Country = customerSetup.TaxInformation.Country, + Country = customerSetup.TaxInformation.Country }; + customerCreateOptions.Tax = new CustomerTaxOptions { ValidateLocation = StripeConstants.ValidateTaxLocationTiming.Immediately }; + var setNonUSBusinessUseToReverseCharge = + featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); + + if (setNonUSBusinessUseToReverseCharge && + planType.GetProductTier() is not ProductTierType.Free and not ProductTierType.Families && + customerSetup.TaxInformation.Country != "US") + { + customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse; + } + if (!string.IsNullOrEmpty(customerSetup.TaxInformation.TaxId)) { var taxIdType = taxService.GetStripeTaxCode(customerSetup.TaxInformation.Country, @@ -399,21 +413,68 @@ public class OrganizationBillingService( TrialPeriodDays = subscriptionSetup.SkipTrial ? 0 : plan.TrialPeriodDays }; - if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements)) + var setNonUSBusinessUseToReverseCharge = + featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); + + if (setNonUSBusinessUseToReverseCharge) { - var automaticTaxParameters = new AutomaticTaxFactoryParameters(subscriptionSetup.PlanType); - var automaticTaxStrategy = await automaticTaxFactory.CreateAsync(automaticTaxParameters); - automaticTaxStrategy.SetCreateOptions(subscriptionCreateOptions, customer); + subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; } - else + else if (customer.HasRecognizedTaxLocation()) { - subscriptionCreateOptions.AutomaticTax ??= new SubscriptionAutomaticTaxOptions(); - subscriptionCreateOptions.AutomaticTax.Enabled = customer.HasBillingLocation(); + subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions + { + Enabled = + subscriptionSetup.PlanType.GetProductTier() == ProductTierType.Families || + customer.Address.Country == "US" || + customer.TaxIds.Any() + }; } return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); } + private async Task GetCustomerWhileEnsuringCorrectTaxExemptionAsync( + Organization organization, + SubscriptionSetup subscriptionSetup) + { + var customer = await subscriberService.GetCustomerOrThrow(organization, + new CustomerGetOptions { Expand = ["tax", "tax_ids"] }); + + var setNonUSBusinessUseToReverseCharge = featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); + + if (!setNonUSBusinessUseToReverseCharge || subscriptionSetup.PlanType.GetProductTier() is + not (ProductTierType.Teams or + ProductTierType.TeamsStarter or + ProductTierType.Enterprise)) + { + return customer; + } + + List expansions = ["tax", "tax_ids"]; + + customer = customer switch + { + { Address.Country: not "US", TaxExempt: not StripeConstants.TaxExempt.Reverse } => await + stripeAdapter.CustomerUpdateAsync(customer.Id, + new CustomerUpdateOptions + { + Expand = expansions, + TaxExempt = StripeConstants.TaxExempt.Reverse + }), + { Address.Country: "US", TaxExempt: StripeConstants.TaxExempt.Reverse } => await + stripeAdapter.CustomerUpdateAsync(customer.Id, + new CustomerUpdateOptions + { + Expand = expansions, + TaxExempt = StripeConstants.TaxExempt.None + }), + _ => customer + }; + + return customer; + } + private async Task IsEligibleForSelfHostAsync( Organization organization) { diff --git a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs index 1b845e93f1..7496157aaa 100644 --- a/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs +++ b/src/Core/Billing/Services/Implementations/PremiumUserBillingService.cs @@ -3,8 +3,6 @@ using Bit.Core.Billing.Constants; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Tax.Models; -using Bit.Core.Billing.Tax.Services; -using Bit.Core.Billing.Tax.Services.Implementations; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; @@ -12,7 +10,6 @@ using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; using Braintree; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Stripe; using Customer = Stripe.Customer; @@ -24,20 +21,18 @@ using static Utilities; public class PremiumUserBillingService( IBraintreeGateway braintreeGateway, - IFeatureService featureService, IGlobalSettings globalSettings, ILogger logger, ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, ISubscriberService subscriberService, - IUserRepository userRepository, - [FromKeyedServices(AutomaticTaxFactory.PersonalUse)] IAutomaticTaxStrategy automaticTaxStrategy) : IPremiumUserBillingService + IUserRepository userRepository) : IPremiumUserBillingService { public async Task Credit(User user, decimal amount) { var customer = await subscriberService.GetCustomer(user); - // Negative credit represents a balance and all Stripe denomination is in cents. + // Negative credit represents a balance, and all Stripe denomination is in cents. var credit = (long)(amount * -100); if (customer == null) @@ -184,7 +179,7 @@ public class PremiumUserBillingService( City = customerSetup.TaxInformation.City, PostalCode = customerSetup.TaxInformation.PostalCode, State = customerSetup.TaxInformation.State, - Country = customerSetup.TaxInformation.Country, + Country = customerSetup.TaxInformation.Country }, Description = user.Name, Email = user.Email, @@ -324,6 +319,10 @@ public class PremiumUserBillingService( var subscriptionCreateOptions = new SubscriptionCreateOptions { + AutomaticTax = new SubscriptionAutomaticTaxOptions + { + Enabled = true + }, CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically, Customer = customer.Id, Items = subscriptionItemOptionsList, @@ -337,18 +336,6 @@ public class PremiumUserBillingService( OffSession = true }; - if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements)) - { - automaticTaxStrategy.SetCreateOptions(subscriptionCreateOptions, customer); - } - else - { - subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions - { - Enabled = customer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported, - }; - } - var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); if (usingPayPal) @@ -380,7 +367,7 @@ public class PremiumUserBillingService( City = taxInformation.City, PostalCode = taxInformation.PostalCode, State = taxInformation.State, - Country = taxInformation.Country, + Country = taxInformation.Country }, Expand = ["tax"], Tax = new CustomerTaxOptions diff --git a/src/Core/Billing/Services/Implementations/SubscriberService.cs b/src/Core/Billing/Services/Implementations/SubscriberService.cs index 10247cdf92..75a1bf76ec 100644 --- a/src/Core/Billing/Services/Implementations/SubscriberService.cs +++ b/src/Core/Billing/Services/Implementations/SubscriberService.cs @@ -1,7 +1,10 @@ -using Bit.Core.Billing.Caches; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; +using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; -using Bit.Core.Billing.Services.Contracts; using Bit.Core.Billing.Tax.Models; using Bit.Core.Billing.Tax.Services; using Bit.Core.Entities; @@ -28,8 +31,7 @@ public class SubscriberService( ILogger logger, ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, - ITaxService taxService, - IAutomaticTaxFactory automaticTaxFactory) : ISubscriberService + ITaxService taxService) : ISubscriberService { public async Task CancelSubscription( ISubscriber subscriber, @@ -128,7 +130,7 @@ public class SubscriberService( [subscriber.BraintreeCloudRegionField()] = globalSettings.BaseServiceUri.CloudRegion }, Email = subscriber.BillingEmailAddress(), - PaymentMethodNonce = paymentMethodNonce, + PaymentMethodNonce = paymentMethodNonce }); if (customerResult.IsSuccess()) @@ -482,7 +484,7 @@ public class SubscriberService( var matchingSetupIntent = setupIntentsForUpdatedPaymentMethod.First(); - // Find the customer's existing setup intents that should be cancelled. + // Find the customer's existing setup intents that should be canceled. var existingSetupIntentsForCustomer = (await getExistingSetupIntentsForCustomer) .Where(si => si.Status is "requires_payment_method" or "requires_confirmation" or "requires_action"); @@ -519,7 +521,7 @@ public class SubscriberService( await stripeAdapter.PaymentMethodAttachAsync(token, new PaymentMethodAttachOptions { Customer = subscriber.GatewayCustomerId }); - // Find the customer's existing setup intents that should be cancelled. + // Find the customer's existing setup intents that should be canceled. var existingSetupIntentsForCustomer = (await getExistingSetupIntentsForCustomer) .Where(si => si.Status is "requires_payment_method" or "requires_confirmation" or "requires_action"); @@ -637,7 +639,8 @@ public class SubscriberService( logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.", taxInformation.Country, taxInformation.TaxId); - throw new Exceptions.BadRequestException("billingTaxIdTypeInferenceError"); + + throw new BadRequestException("billingTaxIdTypeInferenceError"); } } @@ -654,53 +657,84 @@ public class SubscriberService( logger.LogWarning("Invalid tax ID '{TaxID}' for country '{Country}'.", taxInformation.TaxId, taxInformation.Country); - throw new Exceptions.BadRequestException("billingInvalidTaxIdError"); + + throw new BadRequestException("billingInvalidTaxIdError"); + default: logger.LogError(e, "Error creating tax ID '{TaxId}' in country '{Country}' for customer '{CustomerID}'.", taxInformation.TaxId, taxInformation.Country, customer.Id); - throw new Exceptions.BadRequestException("billingTaxIdCreationError"); + + throw new BadRequestException("billingTaxIdCreationError"); } } } - if (featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements)) + var subscription = + customer.Subscriptions.First(subscription => subscription.Id == subscriber.GatewaySubscriptionId); + + var isBusinessUseSubscriber = subscriber switch { - if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId)) + Organization organization => organization.PlanType.GetProductTier() is not ProductTierType.Free and not ProductTierType.Families, + Provider => true, + _ => false + }; + + var setNonUSBusinessUseToReverseCharge = + featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); + + if (setNonUSBusinessUseToReverseCharge && isBusinessUseSubscriber) + { + switch (customer) { - var subscriptionGetOptions = new SubscriptionGetOptions + case { - Expand = ["customer.tax", "customer.tax_ids"] - }; - var subscription = await stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId, subscriptionGetOptions); - var automaticTaxParameters = new AutomaticTaxFactoryParameters(subscriber, subscription.Items.Select(x => x.Price.Id)); - var automaticTaxStrategy = await automaticTaxFactory.CreateAsync(automaticTaxParameters); - var automaticTaxOptions = automaticTaxStrategy.GetUpdateOptions(subscription); - if (automaticTaxOptions?.AutomaticTax?.Enabled != null) + Address.Country: not "US", + TaxExempt: not StripeConstants.TaxExempt.Reverse + }: + await stripeAdapter.CustomerUpdateAsync(customer.Id, + new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse }); + break; + case { - await stripeAdapter.SubscriptionUpdateAsync(subscriber.GatewaySubscriptionId, automaticTaxOptions); - } + Address.Country: "US", + TaxExempt: StripeConstants.TaxExempt.Reverse + }: + await stripeAdapter.CustomerUpdateAsync(customer.Id, + new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.None }); + break; } - } - else - { - if (SubscriberIsEligibleForAutomaticTax(subscriber, customer)) + + if (!subscription.AutomaticTax.Enabled) { - await stripeAdapter.SubscriptionUpdateAsync(subscriber.GatewaySubscriptionId, + await stripeAdapter.SubscriptionUpdateAsync(subscription.Id, new SubscriptionUpdateOptions { AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } }); } + } + else + { + var automaticTaxShouldBeEnabled = subscriber switch + { + User => true, + Organization organization => organization.PlanType.GetProductTier() == ProductTierType.Families || + customer.Address.Country == "US" || (customer.TaxIds?.Any() ?? false), + Provider => customer.Address.Country == "US" || (customer.TaxIds?.Any() ?? false), + _ => false + }; - return; - - bool SubscriberIsEligibleForAutomaticTax(ISubscriber localSubscriber, Customer localCustomer) - => !string.IsNullOrEmpty(localSubscriber.GatewaySubscriptionId) && - (localCustomer.Subscriptions?.Any(sub => sub.Id == localSubscriber.GatewaySubscriptionId && !sub.AutomaticTax.Enabled) ?? false) && - localCustomer.Tax?.AutomaticTax == StripeConstants.AutomaticTaxStatus.Supported; + if (automaticTaxShouldBeEnabled && !subscription.AutomaticTax.Enabled) + { + await stripeAdapter.SubscriptionUpdateAsync(subscription.Id, + new SubscriptionUpdateOptions + { + AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true } + }); + } } } diff --git a/src/Core/Billing/Tax/Services/Implementations/BusinessUseAutomaticTaxStrategy.cs b/src/Core/Billing/Tax/Services/Implementations/BusinessUseAutomaticTaxStrategy.cs index 310aced130..6affc57354 100644 --- a/src/Core/Billing/Tax/Services/Implementations/BusinessUseAutomaticTaxStrategy.cs +++ b/src/Core/Billing/Tax/Services/Implementations/BusinessUseAutomaticTaxStrategy.cs @@ -76,7 +76,7 @@ public class BusinessUseAutomaticTaxStrategy(IFeatureService featureService) : I private bool ShouldBeEnabled(Customer customer) { - if (!customer.HasTaxLocationVerified()) + if (!customer.HasRecognizedTaxLocation()) { return false; } diff --git a/src/Core/Billing/Tax/Services/Implementations/PersonalUseAutomaticTaxStrategy.cs b/src/Core/Billing/Tax/Services/Implementations/PersonalUseAutomaticTaxStrategy.cs index e89fc6a3b3..615222259e 100644 --- a/src/Core/Billing/Tax/Services/Implementations/PersonalUseAutomaticTaxStrategy.cs +++ b/src/Core/Billing/Tax/Services/Implementations/PersonalUseAutomaticTaxStrategy.cs @@ -59,6 +59,6 @@ public class PersonalUseAutomaticTaxStrategy(IFeatureService featureService) : I private static bool ShouldBeEnabled(Customer customer) { - return customer.HasTaxLocationVerified(); + return customer.HasRecognizedTaxLocation(); } } diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 707001ddcc..694521c14e 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -143,13 +143,13 @@ public static class FeatureFlagKeys public const string UsePricingService = "use-pricing-service"; public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features"; public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method"; - public const string PM19147_AutomaticTaxImprovements = "pm-19147-automatic-tax-improvements"; public const string PM19422_AllowAutomaticTaxUpdates = "pm-19422-allow-automatic-tax-updates"; public const string PM18770_EnableOrganizationBusinessUnitConversion = "pm-18770-enable-organization-business-unit-conversion"; public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically"; public const string PM19956_RequireProviderPaymentMethodDuringSetup = "pm-19956-require-provider-payment-method-during-setup"; public const string UseOrganizationWarningsService = "use-organization-warnings-service"; public const string PM20322_AllowTrialLength0 = "pm-20322-allow-trial-length-0"; + public const string PM21092_SetNonUSBusinessUseToReverseCharge = "pm-21092-set-non-us-business-use-to-reverse-charge"; /* Data Insights and Reporting Team */ public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application"; diff --git a/src/Core/Services/IPaymentService.cs b/src/Core/Services/IPaymentService.cs index 3fdb829cf4..af96b88ee6 100644 --- a/src/Core/Services/IPaymentService.cs +++ b/src/Core/Services/IPaymentService.cs @@ -4,7 +4,6 @@ using Bit.Core.Billing.Models; using Bit.Core.Billing.Tax.Requests; using Bit.Core.Billing.Tax.Responses; using Bit.Core.Entities; -using Bit.Core.Enums; using Bit.Core.Models.Business; using Bit.Core.Models.StaticStore; @@ -30,8 +29,6 @@ public interface IPaymentService Task AdjustServiceAccountsAsync(Organization organization, Plan plan, int additionalServiceAccounts); Task CancelSubscriptionAsync(ISubscriber subscriber, bool endOfPeriod = false); Task ReinstateSubscriptionAsync(ISubscriber subscriber); - Task UpdatePaymentMethodAsync(ISubscriber subscriber, PaymentMethodType paymentMethodType, - string paymentToken, TaxInfo taxInfo = null); Task CreditAccountAsync(ISubscriber subscriber, decimal creditAmount); Task GetBillingAsync(ISubscriber subscriber); Task GetBillingHistoryAsync(ISubscriber subscriber); diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 65c0525535..34be6d59c5 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -1,13 +1,13 @@ using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Models.Business; using Bit.Core.Billing.Constants; +using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Business; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services; using Bit.Core.Billing.Services.Contracts; -using Bit.Core.Billing.Tax.Models; using Bit.Core.Billing.Tax.Requests; using Bit.Core.Billing.Tax.Responses; using Bit.Core.Billing.Tax.Services; @@ -38,7 +38,6 @@ public class StripePaymentService : IPaymentService private readonly IGlobalSettings _globalSettings; private readonly IFeatureService _featureService; private readonly ITaxService _taxService; - private readonly ISubscriberService _subscriberService; private readonly IPricingClient _pricingClient; private readonly IAutomaticTaxFactory _automaticTaxFactory; private readonly IAutomaticTaxStrategy _personalUseTaxStrategy; @@ -51,7 +50,6 @@ public class StripePaymentService : IPaymentService IGlobalSettings globalSettings, IFeatureService featureService, ITaxService taxService, - ISubscriberService subscriberService, IPricingClient pricingClient, IAutomaticTaxFactory automaticTaxFactory, [FromKeyedServices(AutomaticTaxFactory.PersonalUse)] IAutomaticTaxStrategy personalUseTaxStrategy) @@ -63,7 +61,6 @@ public class StripePaymentService : IPaymentService _globalSettings = globalSettings; _featureService = featureService; _taxService = taxService; - _subscriberService = subscriberService; _pricingClient = pricingClient; _automaticTaxFactory = automaticTaxFactory; _personalUseTaxStrategy = personalUseTaxStrategy; @@ -136,15 +133,68 @@ public class StripePaymentService : IPaymentService if (subscriptionUpdate is CompleteSubscriptionUpdate) { - if (_featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements)) + var setNonUSBusinessUseToReverseCharge = + _featureService.IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge); + + if (setNonUSBusinessUseToReverseCharge) { - var automaticTaxParameters = new AutomaticTaxFactoryParameters(subscriber, updatedItemOptions.Select(x => x.Plan ?? x.Price)); - var automaticTaxStrategy = await _automaticTaxFactory.CreateAsync(automaticTaxParameters); - automaticTaxStrategy.SetUpdateOptions(subUpdateOptions, sub); + if (sub.Customer is + { + Address.Country: not "US", + TaxExempt: not StripeConstants.TaxExempt.Reverse + }) + { + await _stripeAdapter.CustomerUpdateAsync(sub.CustomerId, + new CustomerUpdateOptions { TaxExempt = StripeConstants.TaxExempt.Reverse }); + } + + subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; } - else + else if (sub.Customer.HasRecognizedTaxLocation()) { - subUpdateOptions.EnableAutomaticTax(sub.Customer, sub); + switch (subscriber) + { + case User: + { + subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; + break; + } + case Organization: + { + if (sub.Customer.Address.Country == "US") + { + subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }; + } + else + { + var familyPriceIds = (await Task.WhenAll( + _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually2019), + _pricingClient.GetPlanOrThrow(PlanType.FamiliesAnnually))) + .Select(plan => plan.PasswordManager.StripePlanId); + + var updateIsForPersonalUse = updatedItemOptions + .Select(option => option.Price) + .Intersect(familyPriceIds) + .Any(); + + subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions + { + Enabled = updateIsForPersonalUse || sub.Customer.TaxIds.Any() + }; + } + + break; + } + case Provider: + { + subUpdateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions + { + Enabled = sub.Customer.Address.Country == "US" || + sub.Customer.TaxIds.Any() + }; + break; + } + } } } @@ -202,7 +252,7 @@ public class StripePaymentService : IPaymentService } else if (!invoice.Paid) { - // Pay invoice with no charge to customer this completes the invoice immediately without waiting the scheduled 1h + // Pay invoice with no charge to the customer this completes the invoice immediately without waiting the scheduled 1h invoice = await _stripeAdapter.InvoicePayAsync(subResponse.LatestInvoiceId); paymentIntentClientSecret = null; } @@ -585,309 +635,6 @@ public class StripePaymentService : IPaymentService } } - public async Task UpdatePaymentMethodAsync(ISubscriber subscriber, PaymentMethodType paymentMethodType, - string paymentToken, TaxInfo taxInfo = null) - { - if (subscriber == null) - { - throw new ArgumentNullException(nameof(subscriber)); - } - - if (subscriber.Gateway.HasValue && subscriber.Gateway.Value != GatewayType.Stripe) - { - throw new GatewayException("Switching from one payment type to another is not supported. " + - "Contact us for assistance."); - } - - var createdCustomer = false; - Braintree.Customer braintreeCustomer = null; - string stipeCustomerSourceToken = null; - string stipeCustomerPaymentMethodId = null; - var stripeCustomerMetadata = new Dictionary - { - { "region", _globalSettings.BaseServiceUri.CloudRegion } - }; - var stripePaymentMethod = paymentMethodType is PaymentMethodType.Card or PaymentMethodType.BankAccount; - - Customer customer = null; - - if (!string.IsNullOrWhiteSpace(subscriber.GatewayCustomerId)) - { - var options = new CustomerGetOptions { Expand = ["sources", "tax", "subscriptions"] }; - customer = await _stripeAdapter.CustomerGetAsync(subscriber.GatewayCustomerId, options); - if (customer.Metadata?.Any() ?? false) - { - stripeCustomerMetadata = customer.Metadata; - } - } - - var hadBtCustomer = stripeCustomerMetadata.ContainsKey("btCustomerId"); - if (stripePaymentMethod) - { - if (paymentToken.StartsWith("pm_")) - { - stipeCustomerPaymentMethodId = paymentToken; - } - else - { - stipeCustomerSourceToken = paymentToken; - } - } - else if (paymentMethodType == PaymentMethodType.PayPal) - { - if (hadBtCustomer) - { - var pmResult = await _btGateway.PaymentMethod.CreateAsync(new Braintree.PaymentMethodRequest - { - CustomerId = stripeCustomerMetadata["btCustomerId"], - PaymentMethodNonce = paymentToken - }); - - if (pmResult.IsSuccess()) - { - var customerResult = await _btGateway.Customer.UpdateAsync( - stripeCustomerMetadata["btCustomerId"], new Braintree.CustomerRequest - { - DefaultPaymentMethodToken = pmResult.Target.Token - }); - - if (customerResult.IsSuccess() && customerResult.Target.PaymentMethods.Length > 0) - { - braintreeCustomer = customerResult.Target; - } - else - { - await _btGateway.PaymentMethod.DeleteAsync(pmResult.Target.Token); - hadBtCustomer = false; - } - } - else - { - hadBtCustomer = false; - } - } - - if (!hadBtCustomer) - { - var customerResult = await _btGateway.Customer.CreateAsync(new Braintree.CustomerRequest - { - PaymentMethodNonce = paymentToken, - Email = subscriber.BillingEmailAddress(), - Id = subscriber.BraintreeCustomerIdPrefix() + subscriber.Id.ToString("N").ToLower() + - Utilities.CoreHelpers.RandomString(3, upper: false, numeric: false), - CustomFields = new Dictionary - { - [subscriber.BraintreeIdField()] = subscriber.Id.ToString(), - [subscriber.BraintreeCloudRegionField()] = _globalSettings.BaseServiceUri.CloudRegion - } - }); - - if (!customerResult.IsSuccess() || customerResult.Target.PaymentMethods.Length == 0) - { - throw new GatewayException("Failed to create PayPal customer record."); - } - - braintreeCustomer = customerResult.Target; - } - } - else - { - throw new GatewayException("Payment method is not supported at this time."); - } - - if (stripeCustomerMetadata.ContainsKey("btCustomerId")) - { - if (braintreeCustomer?.Id != stripeCustomerMetadata["btCustomerId"]) - { - stripeCustomerMetadata["btCustomerId_old"] = stripeCustomerMetadata["btCustomerId"]; - } - - stripeCustomerMetadata["btCustomerId"] = braintreeCustomer?.Id; - } - else if (!string.IsNullOrWhiteSpace(braintreeCustomer?.Id)) - { - stripeCustomerMetadata.Add("btCustomerId", braintreeCustomer.Id); - } - - try - { - if (!string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber)) - { - taxInfo.TaxIdType = taxInfo.TaxIdType ?? - _taxService.GetStripeTaxCode(taxInfo.BillingAddressCountry, taxInfo.TaxIdNumber); - } - - if (customer == null) - { - customer = await _stripeAdapter.CustomerCreateAsync(new CustomerCreateOptions - { - Description = subscriber.BillingName(), - Email = subscriber.BillingEmailAddress(), - Metadata = stripeCustomerMetadata, - Source = stipeCustomerSourceToken, - PaymentMethod = stipeCustomerPaymentMethodId, - InvoiceSettings = new CustomerInvoiceSettingsOptions - { - DefaultPaymentMethod = stipeCustomerPaymentMethodId, - CustomFields = - [ - new CustomerInvoiceSettingsCustomFieldOptions() - { - Name = subscriber.SubscriberType(), - Value = subscriber.GetFormattedInvoiceName() - } - - ] - }, - Address = taxInfo == null ? null : new AddressOptions - { - Country = taxInfo.BillingAddressCountry, - PostalCode = taxInfo.BillingAddressPostalCode, - Line1 = taxInfo.BillingAddressLine1 ?? string.Empty, - Line2 = taxInfo.BillingAddressLine2, - City = taxInfo.BillingAddressCity, - State = taxInfo.BillingAddressState - }, - TaxIdData = string.IsNullOrWhiteSpace(taxInfo.TaxIdNumber) - ? [] - : [ - new CustomerTaxIdDataOptions - { - Type = taxInfo.TaxIdType, - Value = taxInfo.TaxIdNumber - } - ], - Expand = ["sources", "tax", "subscriptions"], - }); - - subscriber.Gateway = GatewayType.Stripe; - subscriber.GatewayCustomerId = customer.Id; - createdCustomer = true; - } - - if (!createdCustomer) - { - string defaultSourceId = null; - string defaultPaymentMethodId = null; - if (stripePaymentMethod) - { - if (!string.IsNullOrWhiteSpace(stipeCustomerSourceToken) && paymentToken.StartsWith("btok_")) - { - var bankAccount = await _stripeAdapter.BankAccountCreateAsync(customer.Id, new BankAccountCreateOptions - { - Source = paymentToken - }); - defaultSourceId = bankAccount.Id; - } - else if (!string.IsNullOrWhiteSpace(stipeCustomerPaymentMethodId)) - { - await _stripeAdapter.PaymentMethodAttachAsync(stipeCustomerPaymentMethodId, - new PaymentMethodAttachOptions { Customer = customer.Id }); - defaultPaymentMethodId = stipeCustomerPaymentMethodId; - } - } - - if (customer.Sources != null) - { - foreach (var source in customer.Sources.Where(s => s.Id != defaultSourceId)) - { - if (source is BankAccount) - { - await _stripeAdapter.BankAccountDeleteAsync(customer.Id, source.Id); - } - else if (source is Card) - { - await _stripeAdapter.CardDeleteAsync(customer.Id, source.Id); - } - } - } - - var cardPaymentMethods = _stripeAdapter.PaymentMethodListAutoPaging(new PaymentMethodListOptions - { - Customer = customer.Id, - Type = "card" - }); - foreach (var cardMethod in cardPaymentMethods.Where(m => m.Id != defaultPaymentMethodId)) - { - await _stripeAdapter.PaymentMethodDetachAsync(cardMethod.Id, new PaymentMethodDetachOptions()); - } - - await _subscriberService.UpdateTaxInformation(subscriber, TaxInformation.From(taxInfo)); - - customer = await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions - { - Metadata = stripeCustomerMetadata, - DefaultSource = defaultSourceId, - InvoiceSettings = new CustomerInvoiceSettingsOptions - { - DefaultPaymentMethod = defaultPaymentMethodId, - CustomFields = - [ - new CustomerInvoiceSettingsCustomFieldOptions() - { - Name = subscriber.SubscriberType(), - Value = subscriber.GetFormattedInvoiceName() - } - ] - }, - Expand = ["tax", "subscriptions"] - }); - } - - if (_featureService.IsEnabled(FeatureFlagKeys.PM19147_AutomaticTaxImprovements)) - { - if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId)) - { - var subscriptionGetOptions = new SubscriptionGetOptions - { - Expand = ["customer.tax", "customer.tax_ids"] - }; - var subscription = await _stripeAdapter.SubscriptionGetAsync(subscriber.GatewaySubscriptionId, subscriptionGetOptions); - - var automaticTaxParameters = new AutomaticTaxFactoryParameters(subscriber, subscription.Items.Select(x => x.Price.Id)); - var automaticTaxStrategy = await _automaticTaxFactory.CreateAsync(automaticTaxParameters); - var subscriptionUpdateOptions = automaticTaxStrategy.GetUpdateOptions(subscription); - - if (subscriptionUpdateOptions != null) - { - _ = await _stripeAdapter.SubscriptionUpdateAsync( - subscriber.GatewaySubscriptionId, - subscriptionUpdateOptions); - } - } - } - else - { - if (!string.IsNullOrEmpty(subscriber.GatewaySubscriptionId) && - customer.Subscriptions.Any(sub => - sub.Id == subscriber.GatewaySubscriptionId && - !sub.AutomaticTax.Enabled) && - customer.HasTaxLocationVerified()) - { - var subscriptionUpdateOptions = new SubscriptionUpdateOptions - { - AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }, - DefaultTaxRates = [] - }; - - _ = await _stripeAdapter.SubscriptionUpdateAsync( - subscriber.GatewaySubscriptionId, - subscriptionUpdateOptions); - } - } - } - catch - { - if (braintreeCustomer != null && !hadBtCustomer) - { - await _btGateway.Customer.DeleteAsync(braintreeCustomer.Id); - } - throw; - } - - return createdCustomer; - } - public async Task CreditAccountAsync(ISubscriber subscriber, decimal creditAmount) { Customer customer = null; @@ -1018,7 +765,7 @@ public class StripePaymentService : IPaymentService var address = customer.Address; var taxId = customer.TaxIds?.FirstOrDefault(); - // Line1 is required, so if missing we're using the subscriber name + // Line1 is required, so if missing we're using the subscriber name, // see: https://stripe.com/docs/api/customers/create#create_customer-address-line1 if (address != null && string.IsNullOrWhiteSpace(address.Line1)) { diff --git a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs index b1f78ed987..3fb134fda8 100644 --- a/test/Core.Test/Billing/Services/SubscriberServiceTests.cs +++ b/test/Core.Test/Billing/Services/SubscriberServiceTests.cs @@ -3,14 +3,11 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Models; -using Bit.Core.Billing.Services.Contracts; using Bit.Core.Billing.Services.Implementations; using Bit.Core.Billing.Tax.Models; -using Bit.Core.Billing.Tax.Services; using Bit.Core.Enums; using Bit.Core.Services; using Bit.Core.Settings; -using Bit.Core.Test.Billing.Tax.Services; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Braintree; @@ -195,7 +192,7 @@ public class SubscriberServiceTests await stripeAdapter .DidNotReceiveWithAnyArgs() - .SubscriptionCancelAsync(Arg.Any(), Arg.Any()); ; + .SubscriptionCancelAsync(Arg.Any(), Arg.Any()); } #endregion @@ -1029,7 +1026,7 @@ public class SubscriberServiceTests stripeAdapter .PaymentMethodListAutoPagingAsync(Arg.Any()) - .Returns(GetPaymentMethodsAsync(new List())); + .Returns(GetPaymentMethodsAsync(new List())); await sutProvider.Sut.RemovePaymentSource(organization); @@ -1061,7 +1058,7 @@ public class SubscriberServiceTests stripeAdapter .PaymentMethodListAutoPagingAsync(Arg.Any()) - .Returns(GetPaymentMethodsAsync(new List + .Returns(GetPaymentMethodsAsync(new List { new () { @@ -1086,8 +1083,8 @@ public class SubscriberServiceTests .PaymentMethodDetachAsync(cardId); } - private static async IAsyncEnumerable GetPaymentMethodsAsync( - IEnumerable paymentMethods) + private static async IAsyncEnumerable GetPaymentMethodsAsync( + IEnumerable paymentMethods) { foreach (var paymentMethod in paymentMethods) { @@ -1598,14 +1595,22 @@ public class SubscriberServiceTests City = "Example Town", State = "NY" }, - TaxIds = new StripeList { Data = [new TaxId { Id = "tax_id_1", Type = "us_ein" }] } + TaxIds = new StripeList { Data = [new TaxId { Id = "tax_id_1", Type = "us_ein" }] }, + Subscriptions = new StripeList + { + Data = [ + new Subscription + { + Id = provider.GatewaySubscriptionId, + AutomaticTax = new SubscriptionAutomaticTax { Enabled = false } + } + ] + } }); var subscription = new Subscription { Items = new StripeList() }; sutProvider.GetDependency().SubscriptionGetAsync(Arg.Any()) .Returns(subscription); - sutProvider.GetDependency().CreateAsync(Arg.Any()) - .Returns(new FakeAutomaticTaxStrategy(true)); await sutProvider.Sut.UpdateTaxInformation(provider, taxInformation); @@ -1623,6 +1628,98 @@ public class SubscriberServiceTests await stripeAdapter.Received(1).TaxIdCreateAsync(provider.GatewayCustomerId, Arg.Is( options => options.Type == "us_ein" && options.Value == taxInformation.TaxId)); + + await stripeAdapter.Received(1).SubscriptionUpdateAsync(provider.GatewaySubscriptionId, + Arg.Is(options => options.AutomaticTax.Enabled == true)); + } + + [Theory, BitAutoData] + public async Task UpdateTaxInformation_NonUser_ReverseCharge_MakesCorrectInvocations( + Provider provider, + SutProvider sutProvider) + { + var stripeAdapter = sutProvider.GetDependency(); + + var customer = new Customer { Id = provider.GatewayCustomerId, TaxIds = new StripeList { Data = [new TaxId { Id = "tax_id_1", Type = "us_ein" }] } }; + + stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId, Arg.Is( + options => options.Expand.Contains("tax_ids"))).Returns(customer); + + var taxInformation = new TaxInformation( + "CA", + "12345", + "123456789", + "us_ein", + "123 Example St.", + null, + "Example Town", + "NY"); + + sutProvider.GetDependency() + .CustomerUpdateAsync( + Arg.Is(p => p == provider.GatewayCustomerId), + Arg.Is(options => + options.Address.Country == "CA" && + options.Address.PostalCode == "12345" && + options.Address.Line1 == "123 Example St." && + options.Address.Line2 == null && + options.Address.City == "Example Town" && + options.Address.State == "NY")) + .Returns(new Customer + { + Id = provider.GatewayCustomerId, + Address = new Address + { + Country = "CA", + PostalCode = "12345", + Line1 = "123 Example St.", + Line2 = null, + City = "Example Town", + State = "NY" + }, + TaxIds = new StripeList { Data = [new TaxId { Id = "tax_id_1", Type = "us_ein" }] }, + Subscriptions = new StripeList + { + Data = [ + new Subscription + { + Id = provider.GatewaySubscriptionId, + CustomerId = provider.GatewayCustomerId, + AutomaticTax = new SubscriptionAutomaticTax { Enabled = false } + } + ] + } + }); + + var subscription = new Subscription { Items = new StripeList() }; + sutProvider.GetDependency().SubscriptionGetAsync(Arg.Any()) + .Returns(subscription); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PM21092_SetNonUSBusinessUseToReverseCharge).Returns(true); + + await sutProvider.Sut.UpdateTaxInformation(provider, taxInformation); + + await stripeAdapter.Received(1).CustomerUpdateAsync(provider.GatewayCustomerId, Arg.Is( + options => + options.Address.Country == taxInformation.Country && + options.Address.PostalCode == taxInformation.PostalCode && + options.Address.Line1 == taxInformation.Line1 && + options.Address.Line2 == taxInformation.Line2 && + options.Address.City == taxInformation.City && + options.Address.State == taxInformation.State)); + + await stripeAdapter.Received(1).TaxIdDeleteAsync(provider.GatewayCustomerId, "tax_id_1"); + + await stripeAdapter.Received(1).TaxIdCreateAsync(provider.GatewayCustomerId, Arg.Is( + options => options.Type == "us_ein" && + options.Value == taxInformation.TaxId)); + + await stripeAdapter.Received(1).CustomerUpdateAsync(provider.GatewayCustomerId, + Arg.Is(options => options.TaxExempt == StripeConstants.TaxExempt.Reverse)); + + await stripeAdapter.Received(1).SubscriptionUpdateAsync(provider.GatewaySubscriptionId, + Arg.Is(options => options.AutomaticTax.Enabled == true)); } #endregion From 818934487f03062723152f771ffd3c38e6360ceb Mon Sep 17 00:00:00 2001 From: Graham Walker Date: Mon, 19 May 2025 22:59:30 -0500 Subject: [PATCH 078/114] PM-18939 refactoring send service to 'cqrs' (#5652) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * PM-18939 refactoring send service to 'cqrs' * PM-18939 fixing import issue with sendValidationService * PM-18939 fixing code based on PR comments * PM-18339 reverting to previous code in test * PM-18939 adding XMLdocs to services * PM-18939 reverting send validation methods * PM-18939 updating code to match main * PM-18939 reverting validateUserCanSaveAsync to match main * PM-18939 fill our param and return sections of XMLdocs * PM-18939 updating XMLdocs based on PR comments * Update src/Core/Tools/SendFeatures/Commands/Interfaces/IAnonymousSendCommand.cs Co-authored-by: ✨ Audrey ✨ * Update src/Core/Tools/SendFeatures/Commands/Interfaces/INonAnonymousSendCommand.cs Co-authored-by: ✨ Audrey ✨ * Update src/Core/Tools/SendFeatures/Commands/Interfaces/INonAnonymousSendCommand.cs Co-authored-by: ✨ Audrey ✨ * Update src/Core/Tools/SendFeatures/Services/Interfaces/ISendStorageService.cs Co-authored-by: ✨ Audrey ✨ * PM-18939 adding commits to change tuple to enum type * PM-18939 resetting stream position to 0 when uploading file * PM-18939 updating XMLdocs based on PR comments * PM-18939 updating XMLdocs * PM-18939 removing circular dependency * PM-18939 fixing based on comments * PM-18939 updating method name and documentation --------- Co-authored-by: ✨ Audrey ✨ --- src/Admin/Tools/Jobs/DeleteSendsJob.cs | 6 +- .../Validators/SendRotationValidator.cs | 10 +- src/Api/Startup.cs | 2 + src/Api/Tools/Controllers/SendsController.cs | 129 +- .../Tools/Models/Request/SendRequestModel.cs | 16 +- .../Tools/Models/Data/SendAccessResult.cs | 19 + .../Commands/AnonymousSendCommand.cs | 52 + .../Interfaces/IAnonymousSendCommand.cs | 21 + .../Interfaces/INonAnonymousSendCommand.cs | 53 + .../Commands/NonAnonymousSendCommand.cs | 180 +++ .../SendServiceCollectionExtension.cs | 18 + .../Services}/AzureSendFileStorageService.cs | 0 .../Interfaces/ISendAuthorizationService.cs | 28 + .../Interfaces/ISendCoreHelperService.cs | 17 + .../Interfaces/ISendStorageService.cs | 71 ++ .../Interfaces/ISendValidationService.cs | 35 + .../Services}/LocalSendStorageService.cs | 0 .../Services/SendAuthorizationService.cs | 101 ++ .../Services/SendCoreHelperService.cs | 12 + .../Services/SendFileSettingHelper.cs | 26 + .../Services/SendValidationService.cs | 142 +++ src/Core/Tools/Services/ISendService.cs | 16 - .../Tools/Services/ISendStorageService.cs | 16 - .../Services/Implementations/SendService.cs | 383 ------ .../Utilities/ServiceCollectionExtensions.cs | 4 +- .../Validators/SendRotationValidatorTests.cs | 12 +- .../Tools/Controllers/SendsControllerTests.cs | 17 +- .../Models/Request/SendRequestModelTests.cs | 6 +- .../Services/AnonymousSendCommandTests.cs | 118 ++ .../Services/NonAnonymousSendCommandTests.cs | 1111 +++++++++++++++++ .../Services/SendAuthorizationServiceTests.cs | 175 +++ .../Tools/Services/SendServiceTests.cs | 867 ------------- 32 files changed, 2295 insertions(+), 1368 deletions(-) create mode 100644 src/Core/Tools/Models/Data/SendAccessResult.cs create mode 100644 src/Core/Tools/SendFeatures/Commands/AnonymousSendCommand.cs create mode 100644 src/Core/Tools/SendFeatures/Commands/Interfaces/IAnonymousSendCommand.cs create mode 100644 src/Core/Tools/SendFeatures/Commands/Interfaces/INonAnonymousSendCommand.cs create mode 100644 src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs create mode 100644 src/Core/Tools/SendFeatures/SendServiceCollectionExtension.cs rename src/Core/Tools/{Services/Implementations => SendFeatures/Services}/AzureSendFileStorageService.cs (100%) create mode 100644 src/Core/Tools/SendFeatures/Services/Interfaces/ISendAuthorizationService.cs create mode 100644 src/Core/Tools/SendFeatures/Services/Interfaces/ISendCoreHelperService.cs create mode 100644 src/Core/Tools/SendFeatures/Services/Interfaces/ISendStorageService.cs create mode 100644 src/Core/Tools/SendFeatures/Services/Interfaces/ISendValidationService.cs rename src/Core/Tools/{Services/Implementations => SendFeatures/Services}/LocalSendStorageService.cs (100%) create mode 100644 src/Core/Tools/SendFeatures/Services/SendAuthorizationService.cs create mode 100644 src/Core/Tools/SendFeatures/Services/SendCoreHelperService.cs create mode 100644 src/Core/Tools/SendFeatures/Services/SendFileSettingHelper.cs create mode 100644 src/Core/Tools/SendFeatures/Services/SendValidationService.cs delete mode 100644 src/Core/Tools/Services/ISendService.cs delete mode 100644 src/Core/Tools/Services/ISendStorageService.cs delete mode 100644 src/Core/Tools/Services/Implementations/SendService.cs create mode 100644 test/Core.Test/Tools/Services/AnonymousSendCommandTests.cs create mode 100644 test/Core.Test/Tools/Services/NonAnonymousSendCommandTests.cs create mode 100644 test/Core.Test/Tools/Services/SendAuthorizationServiceTests.cs delete mode 100644 test/Core.Test/Tools/Services/SendServiceTests.cs diff --git a/src/Admin/Tools/Jobs/DeleteSendsJob.cs b/src/Admin/Tools/Jobs/DeleteSendsJob.cs index dafce03994..7449d2ea01 100644 --- a/src/Admin/Tools/Jobs/DeleteSendsJob.cs +++ b/src/Admin/Tools/Jobs/DeleteSendsJob.cs @@ -2,7 +2,7 @@ using Bit.Core; using Bit.Core.Jobs; using Bit.Core.Tools.Repositories; -using Bit.Core.Tools.Services; +using Bit.Core.Tools.SendFeatures.Commands.Interfaces; using Quartz; namespace Bit.Admin.Tools.Jobs; @@ -32,10 +32,10 @@ public class DeleteSendsJob : BaseJob } using (var scope = _serviceProvider.CreateScope()) { - var sendService = scope.ServiceProvider.GetRequiredService(); + var nonAnonymousSendCommand = scope.ServiceProvider.GetRequiredService(); foreach (var send in sends) { - await sendService.DeleteSendAsync(send); + await nonAnonymousSendCommand.DeleteSendAsync(send); } } } diff --git a/src/Api/KeyManagement/Validators/SendRotationValidator.cs b/src/Api/KeyManagement/Validators/SendRotationValidator.cs index c39f563b51..10a5d996b7 100644 --- a/src/Api/KeyManagement/Validators/SendRotationValidator.cs +++ b/src/Api/KeyManagement/Validators/SendRotationValidator.cs @@ -12,17 +12,17 @@ namespace Bit.Api.KeyManagement.Validators; ///
    public class SendRotationValidator : IRotationValidator, IReadOnlyList> { - private readonly ISendService _sendService; + private readonly ISendAuthorizationService _sendAuthorizationService; private readonly ISendRepository _sendRepository; /// /// Instantiates a new /// - /// Enables conversion of to + /// Enables conversion of to /// Retrieves all user s - public SendRotationValidator(ISendService sendService, ISendRepository sendRepository) + public SendRotationValidator(ISendAuthorizationService sendAuthorizationService, ISendRepository sendRepository) { - _sendService = sendService; + _sendAuthorizationService = sendAuthorizationService; _sendRepository = sendRepository; } @@ -44,7 +44,7 @@ public class SendRotationValidator : IRotationValidator _logger; private readonly GlobalSettings _globalSettings; private readonly ICurrentContext _currentContext; @@ -34,7 +38,9 @@ public class SendsController : Controller public SendsController( ISendRepository sendRepository, IUserService userService, - ISendService sendService, + ISendAuthorizationService sendAuthorizationService, + IAnonymousSendCommand anonymousSendCommand, + INonAnonymousSendCommand nonAnonymousSendCommand, ISendFileStorageService sendFileStorageService, ILogger logger, GlobalSettings globalSettings, @@ -42,13 +48,16 @@ public class SendsController : Controller { _sendRepository = sendRepository; _userService = userService; - _sendService = sendService; + _sendAuthorizationService = sendAuthorizationService; + _anonymousSendCommand = anonymousSendCommand; + _nonAnonymousSendCommand = nonAnonymousSendCommand; _sendFileStorageService = sendFileStorageService; _logger = logger; _globalSettings = globalSettings; _currentContext = currentContext; } + #region Anonymous endpoints [AllowAnonymous] [HttpPost("access/{id}")] public async Task Access(string id, [FromBody] SendAccessRequestModel model) @@ -61,18 +70,19 @@ public class SendsController : Controller //} var guid = new Guid(CoreHelpers.Base64UrlDecode(id)); - var (send, passwordRequired, passwordInvalid) = - await _sendService.AccessAsync(guid, model.Password); - if (passwordRequired) + var send = await _sendRepository.GetByIdAsync(guid); + SendAccessResult sendAuthResult = + await _sendAuthorizationService.AccessAsync(send, model.Password); + if (sendAuthResult.Equals(SendAccessResult.PasswordRequired)) { return new UnauthorizedResult(); } - if (passwordInvalid) + if (sendAuthResult.Equals(SendAccessResult.PasswordInvalid)) { await Task.Delay(2000); throw new BadRequestException("Invalid password."); } - if (send == null) + if (sendAuthResult.Equals(SendAccessResult.Denied)) { throw new NotFoundException(); } @@ -106,19 +116,19 @@ public class SendsController : Controller throw new BadRequestException("Could not locate send"); } - var (url, passwordRequired, passwordInvalid) = await _sendService.GetSendFileDownloadUrlAsync(send, fileId, + var (url, result) = await _anonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId, model.Password); - if (passwordRequired) + if (result.Equals(SendAccessResult.PasswordRequired)) { return new UnauthorizedResult(); } - if (passwordInvalid) + if (result.Equals(SendAccessResult.PasswordInvalid)) { await Task.Delay(2000); throw new BadRequestException("Invalid password."); } - if (send == null) + if (result.Equals(SendAccessResult.Denied)) { throw new NotFoundException(); } @@ -130,6 +140,45 @@ public class SendsController : Controller }); } + [AllowAnonymous] + [HttpPost("file/validate/azure")] + public async Task AzureValidateFile() + { + return await ApiHelpers.HandleAzureEvents(Request, new Dictionary> + { + { + "Microsoft.Storage.BlobCreated", async (eventGridEvent) => + { + try + { + var blobName = eventGridEvent.Subject.Split($"{AzureSendFileStorageService.FilesContainerName}/blobs/")[1]; + var sendId = AzureSendFileStorageService.SendIdFromBlobName(blobName); + var send = await _sendRepository.GetByIdAsync(new Guid(sendId)); + if (send == null) + { + if (_sendFileStorageService is AzureSendFileStorageService azureSendFileStorageService) + { + await azureSendFileStorageService.DeleteBlobAsync(blobName); + } + return; + } + + await _nonAnonymousSendCommand.ConfirmFileSize(send); + } + catch (Exception e) + { + _logger.LogError(e, $"Uncaught exception occurred while handling event grid event: {JsonSerializer.Serialize(eventGridEvent)}"); + return; + } + } + } + }); + } + + #endregion + + #region Non-anonymous endpoints + [HttpGet("{id}")] public async Task Get(string id) { @@ -157,8 +206,8 @@ public class SendsController : Controller { model.ValidateCreation(); var userId = _userService.GetProperUserId(User).Value; - var send = model.ToSend(userId, _sendService); - await _sendService.SaveSendAsync(send); + var send = model.ToSend(userId, _sendAuthorizationService); + await _nonAnonymousSendCommand.SaveSendAsync(send); return new SendResponseModel(send, _globalSettings); } @@ -175,15 +224,15 @@ public class SendsController : Controller throw new BadRequestException("Invalid content. File size hint is required."); } - if (model.FileLength.Value > SendService.MAX_FILE_SIZE) + if (model.FileLength.Value > Constants.FileSize501mb) { - throw new BadRequestException($"Max file size is {SendService.MAX_FILE_SIZE_READABLE}."); + throw new BadRequestException($"Max file size is {SendFileSettingHelper.MAX_FILE_SIZE_READABLE}."); } model.ValidateCreation(); var userId = _userService.GetProperUserId(User).Value; - var (send, data) = model.ToSend(userId, model.File.FileName, _sendService); - var uploadUrl = await _sendService.SaveFileSendAsync(send, data, model.FileLength.Value); + var (send, data) = model.ToSend(userId, model.File.FileName, _sendAuthorizationService); + var uploadUrl = await _nonAnonymousSendCommand.SaveFileSendAsync(send, data, model.FileLength.Value); return new SendFileUploadDataResponseModel { Url = uploadUrl, @@ -230,41 +279,7 @@ public class SendsController : Controller var send = await _sendRepository.GetByIdAsync(new Guid(id)); await Request.GetFileAsync(async (stream) => { - await _sendService.UploadFileToExistingSendAsync(stream, send); - }); - } - - [AllowAnonymous] - [HttpPost("file/validate/azure")] - public async Task AzureValidateFile() - { - return await ApiHelpers.HandleAzureEvents(Request, new Dictionary> - { - { - "Microsoft.Storage.BlobCreated", async (eventGridEvent) => - { - try - { - var blobName = eventGridEvent.Subject.Split($"{AzureSendFileStorageService.FilesContainerName}/blobs/")[1]; - var sendId = AzureSendFileStorageService.SendIdFromBlobName(blobName); - var send = await _sendRepository.GetByIdAsync(new Guid(sendId)); - if (send == null) - { - if (_sendFileStorageService is AzureSendFileStorageService azureSendFileStorageService) - { - await azureSendFileStorageService.DeleteBlobAsync(blobName); - } - return; - } - await _sendService.ValidateSendFile(send); - } - catch (Exception e) - { - _logger.LogError(e, $"Uncaught exception occurred while handling event grid event: {JsonSerializer.Serialize(eventGridEvent)}"); - return; - } - } - } + await _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send); }); } @@ -279,7 +294,7 @@ public class SendsController : Controller throw new NotFoundException(); } - await _sendService.SaveSendAsync(model.ToSend(send, _sendService)); + await _nonAnonymousSendCommand.SaveSendAsync(model.ToSend(send, _sendAuthorizationService)); return new SendResponseModel(send, _globalSettings); } @@ -294,7 +309,7 @@ public class SendsController : Controller } send.Password = null; - await _sendService.SaveSendAsync(send); + await _nonAnonymousSendCommand.SaveSendAsync(send); return new SendResponseModel(send, _globalSettings); } @@ -308,6 +323,8 @@ public class SendsController : Controller throw new NotFoundException(); } - await _sendService.DeleteSendAsync(send); + await _nonAnonymousSendCommand.DeleteSendAsync(send); } + + #endregion } diff --git a/src/Api/Tools/Models/Request/SendRequestModel.cs b/src/Api/Tools/Models/Request/SendRequestModel.cs index 660ff41e3a..5b3fd7ba31 100644 --- a/src/Api/Tools/Models/Request/SendRequestModel.cs +++ b/src/Api/Tools/Models/Request/SendRequestModel.cs @@ -36,31 +36,31 @@ public class SendRequestModel public bool? Disabled { get; set; } public bool? HideEmail { get; set; } - public Send ToSend(Guid userId, ISendService sendService) + public Send ToSend(Guid userId, ISendAuthorizationService sendAuthorizationService) { var send = new Send { Type = Type, UserId = (Guid?)userId }; - ToSend(send, sendService); + ToSend(send, sendAuthorizationService); return send; } - public (Send, SendFileData) ToSend(Guid userId, string fileName, ISendService sendService) + public (Send, SendFileData) ToSend(Guid userId, string fileName, ISendAuthorizationService sendAuthorizationService) { var send = ToSendBase(new Send { Type = Type, UserId = (Guid?)userId - }, sendService); + }, sendAuthorizationService); var data = new SendFileData(Name, Notes, fileName); return (send, data); } - public Send ToSend(Send existingSend, ISendService sendService) + public Send ToSend(Send existingSend, ISendAuthorizationService sendAuthorizationService) { - existingSend = ToSendBase(existingSend, sendService); + existingSend = ToSendBase(existingSend, sendAuthorizationService); switch (existingSend.Type) { case SendType.File: @@ -125,7 +125,7 @@ public class SendRequestModel } } - private Send ToSendBase(Send existingSend, ISendService sendService) + private Send ToSendBase(Send existingSend, ISendAuthorizationService authorizationService) { existingSend.Key = Key; existingSend.ExpirationDate = ExpirationDate; @@ -133,7 +133,7 @@ public class SendRequestModel existingSend.MaxAccessCount = MaxAccessCount; if (!string.IsNullOrWhiteSpace(Password)) { - existingSend.Password = sendService.HashPassword(Password); + existingSend.Password = authorizationService.HashPassword(Password); } existingSend.Disabled = Disabled.GetValueOrDefault(); existingSend.HideEmail = HideEmail.GetValueOrDefault(); diff --git a/src/Core/Tools/Models/Data/SendAccessResult.cs b/src/Core/Tools/Models/Data/SendAccessResult.cs new file mode 100644 index 0000000000..4516f0d9a2 --- /dev/null +++ b/src/Core/Tools/Models/Data/SendAccessResult.cs @@ -0,0 +1,19 @@ +using Bit.Core.Tools.Entities; + +namespace Bit.Core.Tools.Models.Data; + +/// +/// This enum represents the possible results when attempting to access a . +/// +/// name="Granted">Access is granted for the . +/// name="PasswordRequired">Access is denied, but a password is required to access the . +/// +/// name="PasswordInvalid">Access is denied due to an invalid password. +/// name="Denied">Access is denied for the . +public enum SendAccessResult +{ + Granted, + PasswordRequired, + PasswordInvalid, + Denied +} diff --git a/src/Core/Tools/SendFeatures/Commands/AnonymousSendCommand.cs b/src/Core/Tools/SendFeatures/Commands/AnonymousSendCommand.cs new file mode 100644 index 0000000000..f41c62f409 --- /dev/null +++ b/src/Core/Tools/SendFeatures/Commands/AnonymousSendCommand.cs @@ -0,0 +1,52 @@ +using Bit.Core.Exceptions; +using Bit.Core.Platform.Push; +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.Repositories; +using Bit.Core.Tools.SendFeatures.Commands.Interfaces; +using Bit.Core.Tools.Services; + +namespace Bit.Core.Tools.SendFeatures.Commands; + +public class AnonymousSendCommand : IAnonymousSendCommand +{ + private readonly ISendRepository _sendRepository; + private readonly ISendFileStorageService _sendFileStorageService; + private readonly IPushNotificationService _pushNotificationService; + private readonly ISendAuthorizationService _sendAuthorizationService; + + public AnonymousSendCommand( + ISendRepository sendRepository, + ISendFileStorageService sendFileStorageService, + IPushNotificationService pushNotificationService, + ISendAuthorizationService sendAuthorizationService + ) + { + _sendRepository = sendRepository; + _sendFileStorageService = sendFileStorageService; + _pushNotificationService = pushNotificationService; + _sendAuthorizationService = sendAuthorizationService; + } + + // Response: Send, password required, password invalid + public async Task<(string, SendAccessResult)> GetSendFileDownloadUrlAsync(Send send, string fileId, string password) + { + if (send.Type != SendType.File) + { + throw new BadRequestException("Can only get a download URL for a file type of Send"); + } + + var result = _sendAuthorizationService.SendCanBeAccessed(send, password); + + if (!result.Equals(SendAccessResult.Granted)) + { + return (null, result); + } + + send.AccessCount++; + await _sendRepository.ReplaceAsync(send); + await _pushNotificationService.PushSyncSendUpdateAsync(send); + return (await _sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId), result); + } +} diff --git a/src/Core/Tools/SendFeatures/Commands/Interfaces/IAnonymousSendCommand.cs b/src/Core/Tools/SendFeatures/Commands/Interfaces/IAnonymousSendCommand.cs new file mode 100644 index 0000000000..ad23d85170 --- /dev/null +++ b/src/Core/Tools/SendFeatures/Commands/Interfaces/IAnonymousSendCommand.cs @@ -0,0 +1,21 @@ +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Models.Data; + +namespace Bit.Core.Tools.SendFeatures.Commands.Interfaces; + +/// +/// AnonymousSendCommand interface provides methods for managing anonymous Sends. +/// +public interface IAnonymousSendCommand +{ + /// + /// Gets the Send file download URL for a Send object. + /// + /// used to help get file download url and validate file + /// FileId get file download url + /// A hashed and base64-encoded password. This is compared with the send's password to authorize access. + /// Async Task object with Tuple containing the string of download url and + /// to determine if the user can access send. + /// + Task<(string, SendAccessResult)> GetSendFileDownloadUrlAsync(Send send, string fileId, string password); +} diff --git a/src/Core/Tools/SendFeatures/Commands/Interfaces/INonAnonymousSendCommand.cs b/src/Core/Tools/SendFeatures/Commands/Interfaces/INonAnonymousSendCommand.cs new file mode 100644 index 0000000000..58693e619c --- /dev/null +++ b/src/Core/Tools/SendFeatures/Commands/Interfaces/INonAnonymousSendCommand.cs @@ -0,0 +1,53 @@ +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Models.Data; + +namespace Bit.Core.Tools.SendFeatures.Commands.Interfaces; + +/// +/// NonAnonymousSendCommand interface provides methods for managing non-anonymous Sends. +/// +public interface INonAnonymousSendCommand +{ + /// + /// Saves a to the database. + /// + /// that will save to database + /// Task completes as saves to the database + Task SaveSendAsync(Send send); + + /// + /// Saves the and to the database. + /// + /// that will save to the database + /// that will save to file storage + /// Length of file help with saving to file storage + /// Task object for async operations with file upload url + Task SaveFileSendAsync(Send send, SendFileData data, long fileLength); + + /// + /// Upload a file to an existing . + /// + /// of file to be uploaded. The position + /// will be set to 0 before uploading the file. + /// used to help with uploading file + /// Task completes after saving and metadata to the file storage + Task UploadFileToExistingSendAsync(Stream stream, Send send); + + /// + /// Deletes a from the database and file storage. + /// + /// is used to delete from database and file storage + /// Task completes once has been deleted from database and file storage. + Task DeleteSendAsync(Send send); + + /// + /// Stores the confirmed file size of a send; when the file size cannot be confirmed, the send is deleted. + /// + /// The this command acts upon + /// when the file is confirmed, otherwise + /// + /// When a file size cannot be confirmed, we assume we're working with a rogue client. The send is deleted out of + /// an abundance of caution. + /// + Task ConfirmFileSize(Send send); +} diff --git a/src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs b/src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs new file mode 100644 index 0000000000..00da0a911f --- /dev/null +++ b/src/Core/Tools/SendFeatures/Commands/NonAnonymousSendCommand.cs @@ -0,0 +1,180 @@ +using System.Text.Json; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Platform.Push; +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Business; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.Repositories; +using Bit.Core.Tools.SendFeatures.Commands.Interfaces; +using Bit.Core.Tools.Services; +using Bit.Core.Utilities; + +namespace Bit.Core.Tools.SendFeatures.Commands; + +public class NonAnonymousSendCommand : INonAnonymousSendCommand +{ + private readonly ISendRepository _sendRepository; + private readonly ISendFileStorageService _sendFileStorageService; + private readonly IPushNotificationService _pushNotificationService; + private readonly ISendValidationService _sendValidationService; + private readonly IReferenceEventService _referenceEventService; + private readonly ICurrentContext _currentContext; + private readonly ISendCoreHelperService _sendCoreHelperService; + + public NonAnonymousSendCommand(ISendRepository sendRepository, + ISendFileStorageService sendFileStorageService, + IPushNotificationService pushNotificationService, + ISendAuthorizationService sendAuthorizationService, + ISendValidationService sendValidationService, + IReferenceEventService referenceEventService, + ICurrentContext currentContext, + ISendCoreHelperService sendCoreHelperService) + { + _sendRepository = sendRepository; + _sendFileStorageService = sendFileStorageService; + _pushNotificationService = pushNotificationService; + _sendValidationService = sendValidationService; + _referenceEventService = referenceEventService; + _currentContext = currentContext; + _sendCoreHelperService = sendCoreHelperService; + } + + public async Task SaveSendAsync(Send send) + { + // Make sure user can save Sends + await _sendValidationService.ValidateUserCanSaveAsync(send.UserId, send); + + if (send.Id == default(Guid)) + { + await _sendRepository.CreateAsync(send); + await _pushNotificationService.PushSyncSendCreateAsync(send); + await _referenceEventService.RaiseEventAsync(new ReferenceEvent + { + Id = send.UserId ?? default, + Type = ReferenceEventType.SendCreated, + Source = ReferenceEventSource.User, + SendType = send.Type, + MaxAccessCount = send.MaxAccessCount, + HasPassword = !string.IsNullOrWhiteSpace(send.Password), + SendHasNotes = send.Data?.Contains("Notes"), + ClientId = _currentContext.ClientId, + ClientVersion = _currentContext.ClientVersion + }); + } + else + { + send.RevisionDate = DateTime.UtcNow; + await _sendRepository.UpsertAsync(send); + await _pushNotificationService.PushSyncSendUpdateAsync(send); + } + } + + public async Task SaveFileSendAsync(Send send, SendFileData data, long fileLength) + { + if (send.Type != SendType.File) + { + throw new BadRequestException("Send is not of type \"file\"."); + } + + if (fileLength < 1) + { + throw new BadRequestException("No file data."); + } + + var storageBytesRemaining = await _sendValidationService.StorageRemainingForSendAsync(send); + + if (storageBytesRemaining < fileLength) + { + throw new BadRequestException("Not enough storage available."); + } + + var fileId = _sendCoreHelperService.SecureRandomString(32, useUpperCase: false, useSpecial: false); + + try + { + data.Id = fileId; + data.Size = fileLength; + data.Validated = false; + send.Data = JsonSerializer.Serialize(data, + JsonHelpers.IgnoreWritingNull); + await SaveSendAsync(send); + return await _sendFileStorageService.GetSendFileUploadUrlAsync(send, fileId); + } + catch + { + // Clean up since this is not transactional + await _sendFileStorageService.DeleteFileAsync(send, fileId); + throw; + } + } + public async Task UploadFileToExistingSendAsync(Stream stream, Send send) + { + if (stream.Position > 0) + { + stream.Position = 0; + } + + if (send?.Data == null) + { + throw new BadRequestException("Send does not have file data"); + } + + if (send.Type != SendType.File) + { + throw new BadRequestException("Not a File Type Send."); + } + + var data = JsonSerializer.Deserialize(send.Data); + + if (data.Validated) + { + throw new BadRequestException("File has already been uploaded."); + } + + await _sendFileStorageService.UploadNewFileAsync(stream, send, data.Id); + + if (!await ConfirmFileSize(send)) + { + throw new BadRequestException("File received does not match expected file length."); + } + } + public async Task DeleteSendAsync(Send send) + { + await _sendRepository.DeleteAsync(send); + if (send.Type == Enums.SendType.File) + { + var data = JsonSerializer.Deserialize(send.Data); + await _sendFileStorageService.DeleteFileAsync(send, data.Id); + } + await _pushNotificationService.PushSyncSendDeleteAsync(send); + } + + public async Task ConfirmFileSize(Send send) + { + var fileData = JsonSerializer.Deserialize(send.Data); + + var (valid, realSize) = await _sendFileStorageService.ValidateFileAsync(send, fileData.Id, fileData.Size, SendFileSettingHelper.FILE_SIZE_LEEWAY); + + if (!valid || realSize > SendFileSettingHelper.FILE_SIZE_LEEWAY) + { + // File reported differs in size from that promised. Must be a rogue client. Delete Send + await DeleteSendAsync(send); + return false; + } + + // Update Send data if necessary + if (realSize != fileData.Size) + { + fileData.Size = realSize.Value; + } + fileData.Validated = true; + send.Data = JsonSerializer.Serialize(fileData, + JsonHelpers.IgnoreWritingNull); + await SaveSendAsync(send); + + return valid; + } + +} diff --git a/src/Core/Tools/SendFeatures/SendServiceCollectionExtension.cs b/src/Core/Tools/SendFeatures/SendServiceCollectionExtension.cs new file mode 100644 index 0000000000..02327adaac --- /dev/null +++ b/src/Core/Tools/SendFeatures/SendServiceCollectionExtension.cs @@ -0,0 +1,18 @@ +using Bit.Core.Tools.SendFeatures.Commands; +using Bit.Core.Tools.SendFeatures.Commands.Interfaces; +using Bit.Core.Tools.Services; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.Tools.SendFeatures; + +public static class SendServiceCollectionExtension +{ + public static void AddSendServices(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } +} diff --git a/src/Core/Tools/Services/Implementations/AzureSendFileStorageService.cs b/src/Core/Tools/SendFeatures/Services/AzureSendFileStorageService.cs similarity index 100% rename from src/Core/Tools/Services/Implementations/AzureSendFileStorageService.cs rename to src/Core/Tools/SendFeatures/Services/AzureSendFileStorageService.cs diff --git a/src/Core/Tools/SendFeatures/Services/Interfaces/ISendAuthorizationService.cs b/src/Core/Tools/SendFeatures/Services/Interfaces/ISendAuthorizationService.cs new file mode 100644 index 0000000000..9acf987ac5 --- /dev/null +++ b/src/Core/Tools/SendFeatures/Services/Interfaces/ISendAuthorizationService.cs @@ -0,0 +1,28 @@ +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Models.Data; + +namespace Bit.Core.Tools.Services; + +/// +/// Send Authorization service is responsible for checking if a Send can be accessed. +/// +public interface ISendAuthorizationService +{ + /// + /// Checks if a can be accessed while updating the , pushing a notification, and sending a reference event. + /// + /// used to determine access + /// A hashed and base64-encoded password. This is compared with the send's password to authorize access. + /// will be returned to determine if the user can access send. + /// + Task AccessAsync(Send send, string password); + SendAccessResult SendCanBeAccessed(Send send, + string password); + + /// + /// Hashes the password using the password hasher. + /// + /// Password to be hashed + /// Hashed password of the password given + string HashPassword(string password); +} diff --git a/src/Core/Tools/SendFeatures/Services/Interfaces/ISendCoreHelperService.cs b/src/Core/Tools/SendFeatures/Services/Interfaces/ISendCoreHelperService.cs new file mode 100644 index 0000000000..a09d7c3c60 --- /dev/null +++ b/src/Core/Tools/SendFeatures/Services/Interfaces/ISendCoreHelperService.cs @@ -0,0 +1,17 @@ +namespace Bit.Core.Tools.Services; + +/// +/// This interface provides helper methods for generating secure random strings. Making +/// it easier to mock the service in unit tests. +/// +public interface ISendCoreHelperService +{ + /// + /// Securely generates a random string of the specified length. + /// + /// Desired string length to be returned + /// Desired casing for the string + /// Determines if special characters will be used in string + /// A secure random string with the desired parameters + string SecureRandomString(int length, bool useUpperCase, bool useSpecial); +} diff --git a/src/Core/Tools/SendFeatures/Services/Interfaces/ISendStorageService.cs b/src/Core/Tools/SendFeatures/Services/Interfaces/ISendStorageService.cs new file mode 100644 index 0000000000..29bc0c6a6a --- /dev/null +++ b/src/Core/Tools/SendFeatures/Services/Interfaces/ISendStorageService.cs @@ -0,0 +1,71 @@ +using Bit.Core.Enums; +using Bit.Core.Tools.Entities; + +namespace Bit.Core.Tools.Services; + +/// +/// Send File Storage Service is responsible for uploading, deleting, and validating files +/// whether they are in local storage or in cloud storage. +/// +public interface ISendFileStorageService +{ + FileUploadType FileUploadType { get; } + /// + /// Uploads a new file to the storage. + /// + /// of the file + /// for the file + /// File id + /// Task completes once and have been saved to the database + Task UploadNewFileAsync(Stream stream, Send send, string fileId); + /// + /// Deletes a file from the storage. + /// + /// used to delete file + /// File id of file to be deleted + /// Task completes once has been deleted to the database + Task DeleteFileAsync(Send send, string fileId); + /// + /// Deletes all files for a specific organization. + /// + /// used to delete all files pertaining to organization + /// Task completes after running code to delete files by organization id + Task DeleteFilesForOrganizationAsync(Guid organizationId); + /// + /// Deletes all files for a specific user. + /// + /// used to delete all files pertaining to user + /// Task completes after running code to delete files by user id + Task DeleteFilesForUserAsync(Guid userId); + /// + /// Gets the download URL for a file. + /// + /// used to help get download url for file + /// File id to help get download url for file + /// Download url as a string + Task GetSendFileDownloadUrlAsync(Send send, string fileId); + /// + /// Gets the upload URL for a file. + /// + /// used to help get upload url for file + /// File id to help get upload url for file + /// File upload url as string + Task GetSendFileUploadUrlAsync(Send send, string fileId); + /// + /// Validates the file size of a file in the storage. + /// + /// used to help validate file + /// File id to identify which file to validate + /// Expected file size of the file + /// + /// Send file size tolerance in bytes. When an uploaded file's `expectedFileSize` + /// is outside of the leeway, the storage operation fails. + /// + /// + /// ❌ Fill this in with an explanation of the error thrown when `leeway` is incorrect + /// + /// Task object for async operations with Tuple of boolean that determines if file was valid and long that + /// the actual file size of the file. + /// + Task<(bool, long?)> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway); +} diff --git a/src/Core/Tools/SendFeatures/Services/Interfaces/ISendValidationService.cs b/src/Core/Tools/SendFeatures/Services/Interfaces/ISendValidationService.cs new file mode 100644 index 0000000000..24d31c5cfe --- /dev/null +++ b/src/Core/Tools/SendFeatures/Services/Interfaces/ISendValidationService.cs @@ -0,0 +1,35 @@ +using Bit.Core.Tools.Entities; + +namespace Bit.Core.Tools.Services; + +public interface ISendValidationService +{ + /// + /// Validates a file can be saved by specified user. + /// + /// needed to validate file for specific user + /// needed to help validate file + /// Task completes when a conditional statement has been met it will return out of the method or + /// throw a BadRequestException. + /// + Task ValidateUserCanSaveAsync(Guid? userId, Send send); + + /// + /// Validates a file can be saved by specified user with different policy based on feature flag + /// + /// needed to validate file for specific user + /// needed to help validate file + /// Task completes when a conditional statement has been met it will return out of the method or + /// throw a BadRequestException. + /// + Task ValidateUserCanSaveAsync_vNext(Guid? userId, Send send); + + /// + /// Calculates the remaining storage for a Send. + /// + /// needed to help calculate remaining storage + /// Long with the remaining bytes for storage or will throw a BadRequestException if user cannot access + /// file or email is not verified. + /// + Task StorageRemainingForSendAsync(Send send); +} diff --git a/src/Core/Tools/Services/Implementations/LocalSendStorageService.cs b/src/Core/Tools/SendFeatures/Services/LocalSendStorageService.cs similarity index 100% rename from src/Core/Tools/Services/Implementations/LocalSendStorageService.cs rename to src/Core/Tools/SendFeatures/Services/LocalSendStorageService.cs diff --git a/src/Core/Tools/SendFeatures/Services/SendAuthorizationService.cs b/src/Core/Tools/SendFeatures/Services/SendAuthorizationService.cs new file mode 100644 index 0000000000..101a33754e --- /dev/null +++ b/src/Core/Tools/SendFeatures/Services/SendAuthorizationService.cs @@ -0,0 +1,101 @@ +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Platform.Push; +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Business; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.Repositories; +using Microsoft.AspNetCore.Identity; + +namespace Bit.Core.Tools.Services; + +public class SendAuthorizationService : ISendAuthorizationService +{ + private readonly ISendRepository _sendRepository; + private readonly IPasswordHasher _passwordHasher; + private readonly IPushNotificationService _pushNotificationService; + private readonly IReferenceEventService _referenceEventService; + private readonly ICurrentContext _currentContext; + + public SendAuthorizationService( + ISendRepository sendRepository, + IPasswordHasher passwordHasher, + IPushNotificationService pushNotificationService, + IReferenceEventService referenceEventService, + ICurrentContext currentContext) + { + _sendRepository = sendRepository; + _passwordHasher = passwordHasher; + _pushNotificationService = pushNotificationService; + _referenceEventService = referenceEventService; + _currentContext = currentContext; + } + + public SendAccessResult SendCanBeAccessed(Send send, + string password) + { + var now = DateTime.UtcNow; + if (send == null || send.MaxAccessCount.GetValueOrDefault(int.MaxValue) <= send.AccessCount || + send.ExpirationDate.GetValueOrDefault(DateTime.MaxValue) < now || send.Disabled || + send.DeletionDate < now) + { + return SendAccessResult.Denied; + } + if (!string.IsNullOrWhiteSpace(send.Password)) + { + if (string.IsNullOrWhiteSpace(password)) + { + return SendAccessResult.PasswordRequired; + } + var passwordResult = _passwordHasher.VerifyHashedPassword(new User(), send.Password, password); + if (passwordResult == PasswordVerificationResult.SuccessRehashNeeded) + { + send.Password = HashPassword(password); + } + if (passwordResult == PasswordVerificationResult.Failed) + { + return SendAccessResult.PasswordInvalid; + } + } + + return SendAccessResult.Granted; + } + + public async Task AccessAsync(Send sendToBeAccessed, string password) + { + var accessResult = SendCanBeAccessed(sendToBeAccessed, password); + + if (!accessResult.Equals(SendAccessResult.Granted)) + { + return accessResult; + } + + if (sendToBeAccessed.Type != SendType.File) + { + // File sends are incremented during file download + sendToBeAccessed.AccessCount++; + } + + await _sendRepository.ReplaceAsync(sendToBeAccessed); + await _pushNotificationService.PushSyncSendUpdateAsync(sendToBeAccessed); + await _referenceEventService.RaiseEventAsync(new ReferenceEvent + { + Id = sendToBeAccessed.UserId ?? default, + Type = ReferenceEventType.SendAccessed, + Source = ReferenceEventSource.User, + SendType = sendToBeAccessed.Type, + MaxAccessCount = sendToBeAccessed.MaxAccessCount, + HasPassword = !string.IsNullOrWhiteSpace(sendToBeAccessed.Password), + SendHasNotes = sendToBeAccessed.Data?.Contains("Notes"), + ClientId = _currentContext.ClientId, + ClientVersion = _currentContext.ClientVersion + }); + return accessResult; + } + + public string HashPassword(string password) + { + return _passwordHasher.HashPassword(new User(), password); + } +} diff --git a/src/Core/Tools/SendFeatures/Services/SendCoreHelperService.cs b/src/Core/Tools/SendFeatures/Services/SendCoreHelperService.cs new file mode 100644 index 0000000000..122759f8f0 --- /dev/null +++ b/src/Core/Tools/SendFeatures/Services/SendCoreHelperService.cs @@ -0,0 +1,12 @@ +using Bit.Core.Utilities; + +namespace Bit.Core.Tools.Services; + +public class SendCoreHelperService : ISendCoreHelperService +{ + public string SecureRandomString(int length, bool useUpperCase, bool useSpecial) + { + return CoreHelpers.SecureRandomString(length, upper: useUpperCase, special: useSpecial); + } + +} diff --git a/src/Core/Tools/SendFeatures/Services/SendFileSettingHelper.cs b/src/Core/Tools/SendFeatures/Services/SendFileSettingHelper.cs new file mode 100644 index 0000000000..ef3f210ff8 --- /dev/null +++ b/src/Core/Tools/SendFeatures/Services/SendFileSettingHelper.cs @@ -0,0 +1,26 @@ +using Bit.Core.Tools.Entities; + +namespace Bit.Core.Tools.SendFeatures; + +/// +/// SendFileSettingHelper is a static class that provides constants and helper methods (if needed) for managing file +/// settings. +/// +public static class SendFileSettingHelper +{ + /// + /// The leeway for the file size. This is the calculated 1 megabyte of cushion when doing comparisons of file sizes + /// within the system. + /// + public const long FILE_SIZE_LEEWAY = 1024L * 1024L; // 1MB + /// + /// The maximum file size for a file uploaded in a . Units are calculated in bytes but + /// represent 501 megabytes. 1 megabyte is added for cushion to account for file size. + /// + public const long MAX_FILE_SIZE = Constants.FileSize501mb; + + /// + /// String of the expected file size and to be used when needing to communicate the file size to the client/user. + /// + public const string MAX_FILE_SIZE_READABLE = "500 MB"; +} diff --git a/src/Core/Tools/SendFeatures/Services/SendValidationService.cs b/src/Core/Tools/SendFeatures/Services/SendValidationService.cs new file mode 100644 index 0000000000..f1e8855def --- /dev/null +++ b/src/Core/Tools/SendFeatures/Services/SendValidationService.cs @@ -0,0 +1,142 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.AdminConsole.Services; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Core.Tools.Entities; +using Bit.Core.Utilities; + +namespace Bit.Core.Tools.Services; + +public class SendValidationService : ISendValidationService +{ + + private readonly IUserRepository _userRepository; + private readonly IOrganizationRepository _organizationRepository; + private readonly IPolicyService _policyService; + private readonly IFeatureService _featureService; + private readonly IUserService _userService; + private readonly GlobalSettings _globalSettings; + private readonly ICurrentContext _currentContext; + private readonly IPolicyRequirementQuery _policyRequirementQuery; + + + + public SendValidationService( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IPolicyService policyService, + IFeatureService featureService, + IUserService userService, + IPolicyRequirementQuery policyRequirementQuery, + GlobalSettings globalSettings, + + ICurrentContext currentContext) + { + _userRepository = userRepository; + _organizationRepository = organizationRepository; + _policyService = policyService; + _featureService = featureService; + _userService = userService; + _policyRequirementQuery = policyRequirementQuery; + _globalSettings = globalSettings; + _currentContext = currentContext; + } + + public async Task ValidateUserCanSaveAsync(Guid? userId, Send send) + { + if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) + { + await ValidateUserCanSaveAsync_vNext(userId, send); + return; + } + + if (!userId.HasValue || (!_currentContext.Organizations?.Any() ?? true)) + { + return; + } + + var anyDisableSendPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId.Value, + PolicyType.DisableSend); + if (anyDisableSendPolicies) + { + throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send."); + } + + if (send.HideEmail.GetValueOrDefault()) + { + var sendOptionsPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId.Value, PolicyType.SendOptions); + if (sendOptionsPolicies.Any(p => CoreHelpers.LoadClassFromJsonData(p.PolicyData)?.DisableHideEmail ?? false)) + { + throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send."); + } + } + } + + public async Task ValidateUserCanSaveAsync_vNext(Guid? userId, Send send) + { + if (!userId.HasValue) + { + return; + } + + var disableSendRequirement = await _policyRequirementQuery.GetAsync(userId.Value); + if (disableSendRequirement.DisableSend) + { + throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send."); + } + + var sendOptionsRequirement = await _policyRequirementQuery.GetAsync(userId.Value); + if (sendOptionsRequirement.DisableHideEmail && send.HideEmail.GetValueOrDefault()) + { + throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send."); + } + } + + public async Task StorageRemainingForSendAsync(Send send) + { + var storageBytesRemaining = 0L; + if (send.UserId.HasValue) + { + var user = await _userRepository.GetByIdAsync(send.UserId.Value); + if (!await _userService.CanAccessPremium(user)) + { + throw new BadRequestException("You must have premium status to use file Sends."); + } + + if (!user.EmailVerified) + { + throw new BadRequestException("You must confirm your email to use file Sends."); + } + + if (user.Premium) + { + storageBytesRemaining = user.StorageBytesRemaining(); + } + else + { + // Users that get access to file storage/premium from their organization get the default + // 1 GB max storage. + short limit = _globalSettings.SelfHosted ? (short)10240 : (short)1; + storageBytesRemaining = user.StorageBytesRemaining(limit); + } + } + else if (send.OrganizationId.HasValue) + { + var org = await _organizationRepository.GetByIdAsync(send.OrganizationId.Value); + if (!org.MaxStorageGb.HasValue) + { + throw new BadRequestException("This organization cannot use file sends."); + } + + storageBytesRemaining = org.StorageBytesRemaining(); + } + + return storageBytesRemaining; + } +} diff --git a/src/Core/Tools/Services/ISendService.cs b/src/Core/Tools/Services/ISendService.cs deleted file mode 100644 index 2c20851ce8..0000000000 --- a/src/Core/Tools/Services/ISendService.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Bit.Core.Tools.Entities; -using Bit.Core.Tools.Models.Data; - -namespace Bit.Core.Tools.Services; - -public interface ISendService -{ - Task DeleteSendAsync(Send send); - Task SaveSendAsync(Send send); - Task SaveFileSendAsync(Send send, SendFileData data, long fileLength); - Task UploadFileToExistingSendAsync(Stream stream, Send send); - Task<(Send, bool, bool)> AccessAsync(Guid sendId, string password); - string HashPassword(string password); - Task<(string, bool, bool)> GetSendFileDownloadUrlAsync(Send send, string fileId, string password); - Task ValidateSendFile(Send send); -} diff --git a/src/Core/Tools/Services/ISendStorageService.cs b/src/Core/Tools/Services/ISendStorageService.cs deleted file mode 100644 index 4bf2aa3892..0000000000 --- a/src/Core/Tools/Services/ISendStorageService.cs +++ /dev/null @@ -1,16 +0,0 @@ -using Bit.Core.Enums; -using Bit.Core.Tools.Entities; - -namespace Bit.Core.Tools.Services; - -public interface ISendFileStorageService -{ - FileUploadType FileUploadType { get; } - Task UploadNewFileAsync(Stream stream, Send send, string fileId); - Task DeleteFileAsync(Send send, string fileId); - Task DeleteFilesForOrganizationAsync(Guid organizationId); - Task DeleteFilesForUserAsync(Guid userId); - Task GetSendFileDownloadUrlAsync(Send send, string fileId); - Task GetSendFileUploadUrlAsync(Send send, string fileId); - Task<(bool, long?)> ValidateFileAsync(Send send, string fileId, long expectedFileSize, long leeway); -} diff --git a/src/Core/Tools/Services/Implementations/SendService.cs b/src/Core/Tools/Services/Implementations/SendService.cs deleted file mode 100644 index e09787d7eb..0000000000 --- a/src/Core/Tools/Services/Implementations/SendService.cs +++ /dev/null @@ -1,383 +0,0 @@ -using System.Text.Json; -using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; -using Bit.Core.AdminConsole.Services; -using Bit.Core.Context; -using Bit.Core.Entities; -using Bit.Core.Exceptions; -using Bit.Core.Platform.Push; -using Bit.Core.Repositories; -using Bit.Core.Services; -using Bit.Core.Settings; -using Bit.Core.Tools.Entities; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Business; -using Bit.Core.Tools.Models.Data; -using Bit.Core.Tools.Repositories; -using Bit.Core.Utilities; -using Microsoft.AspNetCore.Identity; - -namespace Bit.Core.Tools.Services; - -public class SendService : ISendService -{ - public const long MAX_FILE_SIZE = Constants.FileSize501mb; - public const string MAX_FILE_SIZE_READABLE = "500 MB"; - private readonly ISendRepository _sendRepository; - private readonly IUserRepository _userRepository; - private readonly IPolicyService _policyService; - private readonly IUserService _userService; - private readonly IOrganizationRepository _organizationRepository; - private readonly ISendFileStorageService _sendFileStorageService; - private readonly IPasswordHasher _passwordHasher; - private readonly IPushNotificationService _pushService; - private readonly IReferenceEventService _referenceEventService; - private readonly GlobalSettings _globalSettings; - private readonly ICurrentContext _currentContext; - private readonly IPolicyRequirementQuery _policyRequirementQuery; - private readonly IFeatureService _featureService; - - private const long _fileSizeLeeway = 1024L * 1024L; // 1MB - - public SendService( - ISendRepository sendRepository, - IUserRepository userRepository, - IUserService userService, - IOrganizationRepository organizationRepository, - ISendFileStorageService sendFileStorageService, - IPasswordHasher passwordHasher, - IPushNotificationService pushService, - IReferenceEventService referenceEventService, - GlobalSettings globalSettings, - IPolicyService policyService, - ICurrentContext currentContext, - IPolicyRequirementQuery policyRequirementQuery, - IFeatureService featureService) - { - _sendRepository = sendRepository; - _userRepository = userRepository; - _userService = userService; - _policyService = policyService; - _organizationRepository = organizationRepository; - _sendFileStorageService = sendFileStorageService; - _passwordHasher = passwordHasher; - _pushService = pushService; - _referenceEventService = referenceEventService; - _globalSettings = globalSettings; - _currentContext = currentContext; - _policyRequirementQuery = policyRequirementQuery; - _featureService = featureService; - } - - public async Task SaveSendAsync(Send send) - { - // Make sure user can save Sends - await ValidateUserCanSaveAsync(send.UserId, send); - - if (send.Id == default(Guid)) - { - await _sendRepository.CreateAsync(send); - await _pushService.PushSyncSendCreateAsync(send); - await RaiseReferenceEventAsync(send, ReferenceEventType.SendCreated); - } - else - { - send.RevisionDate = DateTime.UtcNow; - await _sendRepository.UpsertAsync(send); - await _pushService.PushSyncSendUpdateAsync(send); - } - } - - public async Task SaveFileSendAsync(Send send, SendFileData data, long fileLength) - { - if (send.Type != SendType.File) - { - throw new BadRequestException("Send is not of type \"file\"."); - } - - if (fileLength < 1) - { - throw new BadRequestException("No file data."); - } - - var storageBytesRemaining = await StorageRemainingForSendAsync(send); - - if (storageBytesRemaining < fileLength) - { - throw new BadRequestException("Not enough storage available."); - } - - var fileId = Utilities.CoreHelpers.SecureRandomString(32, upper: false, special: false); - - try - { - data.Id = fileId; - data.Size = fileLength; - data.Validated = false; - send.Data = JsonSerializer.Serialize(data, - JsonHelpers.IgnoreWritingNull); - await SaveSendAsync(send); - return await _sendFileStorageService.GetSendFileUploadUrlAsync(send, fileId); - } - catch - { - // Clean up since this is not transactional - await _sendFileStorageService.DeleteFileAsync(send, fileId); - throw; - } - } - - public async Task UploadFileToExistingSendAsync(Stream stream, Send send) - { - if (send?.Data == null) - { - throw new BadRequestException("Send does not have file data"); - } - - if (send.Type != SendType.File) - { - throw new BadRequestException("Not a File Type Send."); - } - - var data = JsonSerializer.Deserialize(send.Data); - - if (data.Validated) - { - throw new BadRequestException("File has already been uploaded."); - } - - await _sendFileStorageService.UploadNewFileAsync(stream, send, data.Id); - - if (!await ValidateSendFile(send)) - { - throw new BadRequestException("File received does not match expected file length."); - } - } - - public async Task ValidateSendFile(Send send) - { - var fileData = JsonSerializer.Deserialize(send.Data); - - var (valid, realSize) = await _sendFileStorageService.ValidateFileAsync(send, fileData.Id, fileData.Size, _fileSizeLeeway); - - if (!valid || realSize > MAX_FILE_SIZE) - { - // File reported differs in size from that promised. Must be a rogue client. Delete Send - await DeleteSendAsync(send); - return false; - } - - // Update Send data if necessary - if (realSize != fileData.Size) - { - fileData.Size = realSize.Value; - } - fileData.Validated = true; - send.Data = JsonSerializer.Serialize(fileData, - JsonHelpers.IgnoreWritingNull); - await SaveSendAsync(send); - - return valid; - } - - public async Task DeleteSendAsync(Send send) - { - await _sendRepository.DeleteAsync(send); - if (send.Type == Enums.SendType.File) - { - var data = JsonSerializer.Deserialize(send.Data); - await _sendFileStorageService.DeleteFileAsync(send, data.Id); - } - await _pushService.PushSyncSendDeleteAsync(send); - } - - public (bool grant, bool passwordRequiredError, bool passwordInvalidError) SendCanBeAccessed(Send send, - string password) - { - var now = DateTime.UtcNow; - if (send == null || send.MaxAccessCount.GetValueOrDefault(int.MaxValue) <= send.AccessCount || - send.ExpirationDate.GetValueOrDefault(DateTime.MaxValue) < now || send.Disabled || - send.DeletionDate < now) - { - return (false, false, false); - } - if (!string.IsNullOrWhiteSpace(send.Password)) - { - if (string.IsNullOrWhiteSpace(password)) - { - return (false, true, false); - } - var passwordResult = _passwordHasher.VerifyHashedPassword(new User(), send.Password, password); - if (passwordResult == PasswordVerificationResult.SuccessRehashNeeded) - { - send.Password = HashPassword(password); - } - if (passwordResult == PasswordVerificationResult.Failed) - { - return (false, false, true); - } - } - - return (true, false, false); - } - - // Response: Send, password required, password invalid - public async Task<(string, bool, bool)> GetSendFileDownloadUrlAsync(Send send, string fileId, string password) - { - if (send.Type != SendType.File) - { - throw new BadRequestException("Can only get a download URL for a file type of Send"); - } - - var (grantAccess, passwordRequired, passwordInvalid) = SendCanBeAccessed(send, password); - - if (!grantAccess) - { - return (null, passwordRequired, passwordInvalid); - } - - send.AccessCount++; - await _sendRepository.ReplaceAsync(send); - await _pushService.PushSyncSendUpdateAsync(send); - return (await _sendFileStorageService.GetSendFileDownloadUrlAsync(send, fileId), false, false); - } - - // Response: Send, password required, password invalid - public async Task<(Send, bool, bool)> AccessAsync(Guid sendId, string password) - { - var send = await _sendRepository.GetByIdAsync(sendId); - var (grantAccess, passwordRequired, passwordInvalid) = SendCanBeAccessed(send, password); - - if (!grantAccess) - { - return (null, passwordRequired, passwordInvalid); - } - - // TODO: maybe move this to a simple ++ sproc? - if (send.Type != SendType.File) - { - // File sends are incremented during file download - send.AccessCount++; - } - - await _sendRepository.ReplaceAsync(send); - await _pushService.PushSyncSendUpdateAsync(send); - await RaiseReferenceEventAsync(send, ReferenceEventType.SendAccessed); - return (send, false, false); - } - - private async Task RaiseReferenceEventAsync(Send send, ReferenceEventType eventType) - { - await _referenceEventService.RaiseEventAsync(new ReferenceEvent - { - Id = send.UserId ?? default, - Type = eventType, - Source = ReferenceEventSource.User, - SendType = send.Type, - MaxAccessCount = send.MaxAccessCount, - HasPassword = !string.IsNullOrWhiteSpace(send.Password), - SendHasNotes = send.Data?.Contains("Notes"), - ClientId = _currentContext.ClientId, - ClientVersion = _currentContext.ClientVersion - }); - } - - public string HashPassword(string password) - { - return _passwordHasher.HashPassword(new User(), password); - } - - private async Task ValidateUserCanSaveAsync(Guid? userId, Send send) - { - if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) - { - await ValidateUserCanSaveAsync_vNext(userId, send); - return; - } - - if (!userId.HasValue || (!_currentContext.Organizations?.Any() ?? true)) - { - return; - } - - var anyDisableSendPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId.Value, - PolicyType.DisableSend); - if (anyDisableSendPolicies) - { - throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send."); - } - - if (send.HideEmail.GetValueOrDefault()) - { - var sendOptionsPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId.Value, PolicyType.SendOptions); - if (sendOptionsPolicies.Any(p => CoreHelpers.LoadClassFromJsonData(p.PolicyData)?.DisableHideEmail ?? false)) - { - throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send."); - } - } - } - - private async Task ValidateUserCanSaveAsync_vNext(Guid? userId, Send send) - { - if (!userId.HasValue) - { - return; - } - - var disableSendRequirement = await _policyRequirementQuery.GetAsync(userId.Value); - if (disableSendRequirement.DisableSend) - { - throw new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send."); - } - - var sendOptionsRequirement = await _policyRequirementQuery.GetAsync(userId.Value); - if (sendOptionsRequirement.DisableHideEmail && send.HideEmail.GetValueOrDefault()) - { - throw new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send."); - } - } - - private async Task StorageRemainingForSendAsync(Send send) - { - var storageBytesRemaining = 0L; - if (send.UserId.HasValue) - { - var user = await _userRepository.GetByIdAsync(send.UserId.Value); - if (!await _userService.CanAccessPremium(user)) - { - throw new BadRequestException("You must have premium status to use file Sends."); - } - - if (!user.EmailVerified) - { - throw new BadRequestException("You must confirm your email to use file Sends."); - } - - if (user.Premium) - { - storageBytesRemaining = user.StorageBytesRemaining(); - } - else - { - // Users that get access to file storage/premium from their organization get the default - // 1 GB max storage. - storageBytesRemaining = user.StorageBytesRemaining( - _globalSettings.SelfHosted ? (short)10240 : (short)1); - } - } - else if (send.OrganizationId.HasValue) - { - var org = await _organizationRepository.GetByIdAsync(send.OrganizationId.Value); - if (!org.MaxStorageGb.HasValue) - { - throw new BadRequestException("This organization cannot use file sends."); - } - - storageBytesRemaining = org.StorageBytesRemaining(); - } - - return storageBytesRemaining; - } -} diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 9fbc14444f..598d93b177 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -43,6 +43,7 @@ using Bit.Core.Settings; using Bit.Core.Tokens; using Bit.Core.Tools.ImportFeatures; using Bit.Core.Tools.ReportFeatures; +using Bit.Core.Tools.SendFeatures; using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Bit.Core.Vault; @@ -123,7 +124,7 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddScoped(); services.AddLoginServices(); services.AddScoped(); services.AddVaultServices(); @@ -132,6 +133,7 @@ public static class ServiceCollectionExtensions services.AddNotificationCenterServices(); services.AddPlatformServices(); services.AddImportServices(); + services.AddSendServices(); } public static void AddTokenizers(this IServiceCollection services) diff --git a/test/Api.Test/KeyManagement/Validators/SendRotationValidatorTests.cs b/test/Api.Test/KeyManagement/Validators/SendRotationValidatorTests.cs index 842343ba33..7bab587cf0 100644 --- a/test/Api.Test/KeyManagement/Validators/SendRotationValidatorTests.cs +++ b/test/Api.Test/KeyManagement/Validators/SendRotationValidatorTests.cs @@ -23,11 +23,11 @@ public class SendRotationValidatorTests public async Task ValidateAsync_Success() { // Arrange - var sendService = Substitute.For(); + var sendAuthorizationService = Substitute.For(); var sendRepository = Substitute.For(); var sut = new SendRotationValidator( - sendService, + sendAuthorizationService, sendRepository ); @@ -52,11 +52,11 @@ public class SendRotationValidatorTests public async Task ValidateAsync_SendNotReturnedFromRepository_NotIncludedInOutput() { // Arrange - var sendService = Substitute.For(); + var sendAuthorizationService = Substitute.For(); var sendRepository = Substitute.For(); var sut = new SendRotationValidator( - sendService, + sendAuthorizationService, sendRepository ); @@ -76,11 +76,11 @@ public class SendRotationValidatorTests public async Task ValidateAsync_InputMissingUserSend_Throws() { // Arrange - var sendService = Substitute.For(); + var sendAuthorizationService = Substitute.For(); var sendRepository = Substitute.For(); var sut = new SendRotationValidator( - sendService, + sendAuthorizationService, sendRepository ); diff --git a/test/Api.Test/Tools/Controllers/SendsControllerTests.cs b/test/Api.Test/Tools/Controllers/SendsControllerTests.cs index f784448e50..b1fa5c9260 100644 --- a/test/Api.Test/Tools/Controllers/SendsControllerTests.cs +++ b/test/Api.Test/Tools/Controllers/SendsControllerTests.cs @@ -10,7 +10,9 @@ using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Tools.Entities; using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Data; using Bit.Core.Tools.Repositories; +using Bit.Core.Tools.SendFeatures.Commands.Interfaces; using Bit.Core.Tools.Services; using Bit.Core.Utilities; using Microsoft.AspNetCore.Mvc; @@ -26,7 +28,9 @@ public class SendsControllerTests : IDisposable private readonly GlobalSettings _globalSettings; private readonly IUserService _userService; private readonly ISendRepository _sendRepository; - private readonly ISendService _sendService; + private readonly INonAnonymousSendCommand _nonAnonymousSendCommand; + private readonly IAnonymousSendCommand _anonymousSendCommand; + private readonly ISendAuthorizationService _sendAuthorizationService; private readonly ISendFileStorageService _sendFileStorageService; private readonly ILogger _logger; private readonly ICurrentContext _currentContext; @@ -35,7 +39,9 @@ public class SendsControllerTests : IDisposable { _userService = Substitute.For(); _sendRepository = Substitute.For(); - _sendService = Substitute.For(); + _nonAnonymousSendCommand = Substitute.For(); + _anonymousSendCommand = Substitute.For(); + _sendAuthorizationService = Substitute.For(); _sendFileStorageService = Substitute.For(); _globalSettings = new GlobalSettings(); _logger = Substitute.For>(); @@ -44,7 +50,9 @@ public class SendsControllerTests : IDisposable _sut = new SendsController( _sendRepository, _userService, - _sendService, + _sendAuthorizationService, + _anonymousSendCommand, + _nonAnonymousSendCommand, _sendFileStorageService, _logger, _globalSettings, @@ -68,7 +76,8 @@ public class SendsControllerTests : IDisposable send.Data = JsonSerializer.Serialize(new Dictionary()); send.HideEmail = true; - _sendService.AccessAsync(id, null).Returns((send, false, false)); + _sendRepository.GetByIdAsync(Arg.Any()).Returns(send); + _sendAuthorizationService.AccessAsync(send, null).Returns(SendAccessResult.Granted); _userService.GetUserByIdAsync(Arg.Any()).Returns(user); var request = new SendAccessRequestModel(); diff --git a/test/Api.Test/Tools/Models/Request/SendRequestModelTests.cs b/test/Api.Test/Tools/Models/Request/SendRequestModelTests.cs index 59fb35d32e..8049667011 100644 --- a/test/Api.Test/Tools/Models/Request/SendRequestModelTests.cs +++ b/test/Api.Test/Tools/Models/Request/SendRequestModelTests.cs @@ -34,11 +34,11 @@ public class SendRequestModelTests Type = SendType.Text, }; - var sendService = Substitute.For(); - sendService.HashPassword(Arg.Any()) + var sendAuthorizationService = Substitute.For(); + sendAuthorizationService.HashPassword(Arg.Any()) .Returns((info) => $"hashed_{(string)info[0]}"); - var send = sendRequest.ToSend(Guid.NewGuid(), sendService); + var send = sendRequest.ToSend(Guid.NewGuid(), sendAuthorizationService); Assert.Equal(deletionDate, send.DeletionDate); Assert.False(send.Disabled); diff --git a/test/Core.Test/Tools/Services/AnonymousSendCommandTests.cs b/test/Core.Test/Tools/Services/AnonymousSendCommandTests.cs new file mode 100644 index 0000000000..3101273225 --- /dev/null +++ b/test/Core.Test/Tools/Services/AnonymousSendCommandTests.cs @@ -0,0 +1,118 @@ +using System.Text.Json; +using Bit.Core.Exceptions; +using Bit.Core.Platform.Push; +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.Repositories; +using Bit.Core.Tools.SendFeatures.Commands; +using Bit.Core.Tools.Services; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Tools.Services; + +public class AnonymousSendCommandTests +{ + private readonly ISendRepository _sendRepository; + private readonly ISendFileStorageService _sendFileStorageService; + private readonly IPushNotificationService _pushNotificationService; + private readonly ISendAuthorizationService _sendAuthorizationService; + private readonly AnonymousSendCommand _anonymousSendCommand; + + public AnonymousSendCommandTests() + { + _sendRepository = Substitute.For(); + _sendFileStorageService = Substitute.For(); + _pushNotificationService = Substitute.For(); + _sendAuthorizationService = Substitute.For(); + + _anonymousSendCommand = new AnonymousSendCommand( + _sendRepository, + _sendFileStorageService, + _pushNotificationService, + _sendAuthorizationService); + } + + [Fact] + public async Task GetSendFileDownloadUrlAsync_Success_ReturnsDownloadUrl() + { + // Arrange + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + AccessCount = 0, + Data = JsonSerializer.Serialize(new { Id = "fileId123" }) + }; + var fileId = "fileId123"; + var password = "testPassword"; + var expectedUrl = "https://example.com/download"; + + _sendAuthorizationService + .SendCanBeAccessed(send, password) + .Returns(SendAccessResult.Granted); + + _sendFileStorageService + .GetSendFileDownloadUrlAsync(send, fileId) + .Returns(expectedUrl); + + // Act + var result = + await _anonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId, password); + + // Assert + Assert.Equal(expectedUrl, result.Item1); + Assert.Equal(1, send.AccessCount); + + await _sendRepository.Received(1).ReplaceAsync(send); + await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send); + } + + [Fact] + public async Task GetSendFileDownloadUrlAsync_AccessDenied_ReturnsNullWithReasons() + { + // Arrange + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + AccessCount = 0 + }; + var fileId = "fileId123"; + var password = "wrongPassword"; + + _sendAuthorizationService + .SendCanBeAccessed(send, password) + .Returns(SendAccessResult.Denied); + + // Act + var result = + await _anonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId, password); + + // Assert + Assert.Null(result.Item1); + Assert.Equal(SendAccessResult.Denied, result.Item2); + Assert.Equal(0, send.AccessCount); + + await _sendRepository.DidNotReceiveWithAnyArgs().ReplaceAsync(default); + await _pushNotificationService.DidNotReceiveWithAnyArgs().PushSyncSendUpdateAsync(default); + } + + [Fact] + public async Task GetSendFileDownloadUrlAsync_NotFileSend_ThrowsBadRequestException() + { + // Arrange + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.Text + }; + var fileId = "fileId123"; + var password = "testPassword"; + + // Act & Assert + await Assert.ThrowsAsync(() => + _anonymousSendCommand.GetSendFileDownloadUrlAsync(send, fileId, password)); + } +} diff --git a/test/Core.Test/Tools/Services/NonAnonymousSendCommandTests.cs b/test/Core.Test/Tools/Services/NonAnonymousSendCommandTests.cs new file mode 100644 index 0000000000..15e7d57651 --- /dev/null +++ b/test/Core.Test/Tools/Services/NonAnonymousSendCommandTests.cs @@ -0,0 +1,1111 @@ +using System.Text.Json; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Platform.Push; +using Bit.Core.Services; +using Bit.Core.Test.AutoFixture.CurrentContextFixtures; +using Bit.Core.Test.Tools.AutoFixture.SendFixtures; +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Business; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.Repositories; +using Bit.Core.Tools.SendFeatures; +using Bit.Core.Tools.SendFeatures.Commands; +using Bit.Core.Tools.Services; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using Xunit; + +namespace Bit.Core.Test.Tools.Services; + +[SutProviderCustomize] +[CurrentContextCustomize] +[UserSendCustomize] +public class NonAnonymousSendCommandTests +{ + private readonly ISendRepository _sendRepository; + private readonly ISendFileStorageService _sendFileStorageService; + private readonly IPushNotificationService _pushNotificationService; + private readonly ISendAuthorizationService _sendAuthorizationService; + private readonly ISendValidationService _sendValidationService; + private readonly IFeatureService _featureService; + private readonly IReferenceEventService _referenceEventService; + private readonly ICurrentContext _currentContext; + private readonly ISendCoreHelperService _sendCoreHelperService; + private readonly NonAnonymousSendCommand _nonAnonymousSendCommand; + + public NonAnonymousSendCommandTests() + { + _sendRepository = Substitute.For(); + _sendFileStorageService = Substitute.For(); + _pushNotificationService = Substitute.For(); + _sendAuthorizationService = Substitute.For(); + _featureService = Substitute.For(); + _sendValidationService = Substitute.For(); + _referenceEventService = Substitute.For(); + _currentContext = Substitute.For(); + _sendCoreHelperService = Substitute.For(); + + _nonAnonymousSendCommand = new NonAnonymousSendCommand( + _sendRepository, + _sendFileStorageService, + _pushNotificationService, + _sendAuthorizationService, + _sendValidationService, + _referenceEventService, + _currentContext, + _sendCoreHelperService + ); + } + + // Disable Send policy check + [Theory] + [InlineData(SendType.File)] + [InlineData(SendType.Text)] + public async Task SaveSendAsync_DisableSend_Applies_throws(SendType sendType) + { + // Arrange + var send = new Send + { + Id = default, + Type = sendType, + UserId = Guid.NewGuid() + }; + + var user = new User + { + Id = send.UserId.Value, + Email = "test@example.com" + }; + + // Configure validation service to throw when DisableSend policy applies + _sendValidationService.ValidateUserCanSaveAsync(send.UserId.Value, send) + .Throws(new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send.")); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveSendAsync(send)); + + Assert.Contains("Enterprise Policy", exception.Message); + + // Verify the validation service was called + await _sendValidationService.Received(1).ValidateUserCanSaveAsync(send.UserId.Value, send); + + // Verify repository was not called since exception was thrown + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + } + + [Theory] + [InlineData(true)] // New Send (Id is default) + [InlineData(false)] // Existing Send (Id is not default) + public async Task SaveSendAsync_DisableSend_DoesntApply_success(bool isNewSend) + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = isNewSend ? default : Guid.NewGuid(), + Type = SendType.Text, + UserId = userId, + Data = "Text with Notes" + }; + + var initialDate = DateTime.UtcNow.AddMinutes(-5); + send.RevisionDate = initialDate; + + // Configure validation service to NOT throw (policy doesn't apply) + _sendValidationService.ValidateUserCanSaveAsync(userId, send).Returns(Task.CompletedTask); + + // Set up context for reference event + _currentContext.ClientId.Returns("test-client"); + _currentContext.ClientVersion.Returns(Version.Parse("1.0.0")); + + // Act + await _nonAnonymousSendCommand.SaveSendAsync(send); + + // Assert + // Verify validation was checked + await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); + + if (isNewSend) + { + // For new Sends + await _sendRepository.Received(1).CreateAsync(send); + await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send); + await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is(e => + e.Id == userId && + e.Type == ReferenceEventType.SendCreated && + e.Source == ReferenceEventSource.User && + e.SendType == send.Type && + e.SendHasNotes == true && + e.ClientId == "test-client" && + e.ClientVersion == Version.Parse("1.0.0"))); + } + else + { + // For existing Sends + await _sendRepository.Received(1).UpsertAsync(send); + Assert.NotEqual(initialDate, send.RevisionDate); + await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send); + await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); + } + } + + [Theory] + [InlineData(true)] // New Send (Id is default) + [InlineData(false)] // Existing Send (Id is not default) + public async Task SaveSendAsync_DisableHideEmail_Applies_throws(bool isNewSend) + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = isNewSend ? default : Guid.NewGuid(), + Type = SendType.Text, + UserId = userId, + HideEmail = true + }; + + // Configure validation service to throw when HideEmail policy applies + _sendValidationService.ValidateUserCanSaveAsync(userId, send) + .Throws(new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send.")); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveSendAsync(send)); + + Assert.Contains("hide your email address", exception.Message); + + // Verify validation was called + await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); + + // Verify repository was not called (exception prevented save) + if (isNewSend) + { + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + } + else + { + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + } + + // Verify push notification wasn't sent + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + } + + [Theory] + [InlineData(true)] // New Send (Id is default) + [InlineData(false)] // Existing Send (Id is not default) + public async Task SaveSendAsync_DisableHideEmail_DoesntApply_success(bool isNewSend) + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = isNewSend ? default : Guid.NewGuid(), + Type = SendType.Text, + UserId = userId, + HideEmail = true // Setting HideEmail to true + }; + + var initialDate = DateTime.UtcNow.AddMinutes(-5); + send.RevisionDate = initialDate; + + // Configure validation service to NOT throw (policy doesn't apply) + _sendValidationService.ValidateUserCanSaveAsync(userId, send).Returns(Task.CompletedTask); + + // Set up context for reference event + _currentContext.ClientId.Returns("test-client"); + _currentContext.ClientVersion.Returns(Version.Parse("1.0.0")); + + // Act + await _nonAnonymousSendCommand.SaveSendAsync(send); + + // Assert + // Verify validation was checked + await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); + + if (isNewSend) + { + // For new Sends + await _sendRepository.Received(1).CreateAsync(send); + await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send); + await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is(e => + e.Id == userId && + e.Type == ReferenceEventType.SendCreated && + e.Source == ReferenceEventSource.User && + e.SendType == send.Type && + e.HasPassword == false && + e.ClientId == "test-client" && + e.ClientVersion == Version.Parse("1.0.0"))); + } + else + { + // For existing Sends + await _sendRepository.Received(1).UpsertAsync(send); + Assert.NotEqual(initialDate, send.RevisionDate); + await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send); + await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); + } + } + + [Theory] + [InlineData(SendType.File)] + [InlineData(SendType.Text)] + public async Task SaveSendAsync_DisableSend_Applies_Throws_vNext(SendType sendType) + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = default, + Type = sendType, + UserId = userId + }; + + // Configure validation service to throw when DisableSend policy applies in vNext implementation + _sendValidationService.ValidateUserCanSaveAsync(userId, send) + .Returns(Task.FromException(new BadRequestException("Due to an Enterprise Policy, you are only able to delete an existing Send."))); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveSendAsync(send)); + + Assert.Contains("Enterprise Policy", exception.Message); + + // Verify validation service was called + await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); + + // Verify repository and notification methods were not called + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); + } + + [Theory] + [InlineData(true)] // New Send (Id is default) + [InlineData(false)] // Existing Send (Id is not default) + public async Task SaveSendAsync_DisableSend_DoesntApply_Success_vNext(bool isNewSend) + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = isNewSend ? default : Guid.NewGuid(), + Type = SendType.Text, + UserId = userId, + Data = "Text with Notes" + }; + + var initialDate = DateTime.UtcNow.AddMinutes(-5); + send.RevisionDate = initialDate; + + // Configure validation service to return success for vNext implementation + _sendValidationService.ValidateUserCanSaveAsync(userId, send).Returns(Task.CompletedTask); + + // Set up context for reference event + _currentContext.ClientId.Returns("test-client"); + _currentContext.ClientVersion.Returns(Version.Parse("1.0.0")); + + // Enable feature flag for policy requirements (vNext path) + _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + + // Act + await _nonAnonymousSendCommand.SaveSendAsync(send); + + // Assert + // Verify validation was checked with vNext path + await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); + + if (isNewSend) + { + // For new Sends + await _sendRepository.Received(1).CreateAsync(send); + await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send); + await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is(e => + e.Id == userId && + e.Type == ReferenceEventType.SendCreated && + e.Source == ReferenceEventSource.User && + e.SendType == send.Type && + e.SendHasNotes == true && + e.ClientId == "test-client" && + e.ClientVersion == Version.Parse("1.0.0"))); + } + else + { + // For existing Sends + await _sendRepository.Received(1).UpsertAsync(send); + Assert.NotEqual(initialDate, send.RevisionDate); + await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send); + await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); + } + } + + // Send Options Policy - Disable Hide Email check + [Theory] + [InlineData(true)] // New Send (Id is default) + [InlineData(false)] // Existing Send (Id is not default) + public async Task SaveSendAsync_DisableHideEmail_Applies_Throws_vNext(bool isNewSend) + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = isNewSend ? default : Guid.NewGuid(), + Type = SendType.Text, + UserId = userId, + HideEmail = true + }; + + // Enable feature flag for policy requirements (vNext path) + _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + + // Configure validation service to throw when DisableHideEmail policy applies in vNext implementation + _sendValidationService.ValidateUserCanSaveAsync(userId, send) + .Throws(new BadRequestException("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send.")); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveSendAsync(send)); + + Assert.Contains("hide your email address", exception.Message); + + // Verify validation was called + await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); + + // Verify repository was not called (exception prevented save) + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + + // Verify push notification wasn't sent + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + + // Verify reference event service wasn't called + await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); + } + + [Theory] + [InlineData(true)] // New Send (Id is default) + [InlineData(false)] // Existing Send (Id is not default) + public async Task SaveSendAsync_DisableHideEmail_Applies_ButEmailNotHidden_Success_vNext(bool isNewSend) + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = isNewSend ? default : Guid.NewGuid(), + Type = SendType.Text, + UserId = userId, + HideEmail = false // Email is not hidden, so policy doesn't block + }; + + var initialDate = DateTime.UtcNow.AddMinutes(-5); + send.RevisionDate = initialDate; + + // Enable feature flag for policy requirements (vNext path) + _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + + // Configure validation service to allow saves when HideEmail is false + _sendValidationService.ValidateUserCanSaveAsync(userId, send).Returns(Task.CompletedTask); + + // Set up context for reference event + _currentContext.ClientId.Returns("test-client"); + _currentContext.ClientVersion.Returns(Version.Parse("1.0.0")); + + // Act + await _nonAnonymousSendCommand.SaveSendAsync(send); + + // Assert + // Verify validation was called with vNext path + await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); + + if (isNewSend) + { + // For new Sends + await _sendRepository.Received(1).CreateAsync(send); + await _pushNotificationService.Received(1).PushSyncSendCreateAsync(send); + await _referenceEventService.Received(1).RaiseEventAsync(Arg.Is(e => + e.Id == userId && + e.Type == ReferenceEventType.SendCreated && + e.Source == ReferenceEventSource.User && + e.SendType == send.Type && + e.ClientId == "test-client" && + e.ClientVersion == Version.Parse("1.0.0"))); + } + else + { + // For existing Sends + await _sendRepository.Received(1).UpsertAsync(send); + Assert.NotEqual(initialDate, send.RevisionDate); + await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send); + await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); + } + } + + [Fact] + public async Task SaveSendAsync_ExistingSend_Updates() + { + // Arrange + var userId = Guid.NewGuid(); + var sendId = Guid.NewGuid(); + var send = new Send + { + Id = sendId, + Type = SendType.Text, + UserId = userId, + Data = "Some text data" + }; + + var initialDate = DateTime.UtcNow.AddMinutes(-5); + send.RevisionDate = initialDate; + + // Act + await _nonAnonymousSendCommand.SaveSendAsync(send); + + // Assert + // Verify validation was called + await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); + + // Verify repository was called with updated send + await _sendRepository.Received(1).UpsertAsync(send); + + // Check that the revision date was updated + Assert.NotEqual(initialDate, send.RevisionDate); + + // Verify push notification was sent for the update + await _pushNotificationService.Received(1).PushSyncSendUpdateAsync(send); + + // Verify no reference event was raised (only happens for new sends) + await _referenceEventService.DidNotReceive().RaiseEventAsync(Arg.Any()); + } + + [Fact] + public async Task SaveFileSendAsync_TextType_ThrowsBadRequest() + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.Text, // Text type instead of File + UserId = userId + }; + var fileData = new SendFileData(); + var fileLength = 1024L; // 1KB + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + Assert.Contains("not of type \"file\"", exception.Message); + + // Verify no further methods were called + await _sendValidationService.DidNotReceive().StorageRemainingForSendAsync(Arg.Any()); + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + } + + [Fact] + public async Task SaveFileSendAsync_EmptyFile_ThrowsBadRequest() + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = userId + }; + var fileData = new SendFileData(); + var fileLength = 0L; // Empty file + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + Assert.Contains("No file data", exception.Message); + + // Verify no methods were called after validation failed + await _sendValidationService.DidNotReceive().StorageRemainingForSendAsync(Arg.Any()); + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + } + + [Fact] + public async Task SaveFileSendAsync_UserCannotAccessPremium_ThrowsBadRequest() + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = userId + }; + var fileData = new SendFileData(); + var fileLength = 1024L; // 1KB + + // Configure validation service to throw when checking storage + _sendValidationService.StorageRemainingForSendAsync(send) + .Throws(new BadRequestException("You must have premium status to use file Sends.")); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + Assert.Contains("premium status", exception.Message); + + // Verify storage validation was called + await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); + + // Verify no further methods were called + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + } + + [Fact] + public async Task SaveFileSendAsync_UserHasUnconfirmedEmail_ThrowsBadRequest() + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = userId + }; + var fileData = new SendFileData(); + var fileLength = 1024L; // 1KB + + // Configure validation service to pass storage check + _sendValidationService.StorageRemainingForSendAsync(send).Returns(10240L); // 10KB remaining + + // Configure validation service to throw when checking user can save + _sendValidationService.When(x => x.ValidateUserCanSaveAsync(userId, send)) + .Throw(new BadRequestException("You must confirm your email before creating a Send.")); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + Assert.Contains("confirm your email", exception.Message); + + // Verify storage validation was called + await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); + + // Verify SaveSendAsync attempted to be called, triggering email validation + await _sendValidationService.Received(1).ValidateUserCanSaveAsync(userId, send); + + // Verify no repository or notification methods were called after validation failed + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + } + + [Fact] + public async Task SaveFileSendAsync_UserCanAccessPremium_HasNoStorage_ThrowsBadRequest() + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = userId + }; + var fileData = new SendFileData(); + var fileLength = 1024L; // 1KB + + // Configure validation service to return 0 storage remaining + _sendValidationService.StorageRemainingForSendAsync(send).Returns(0L); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + Assert.Contains("Not enough storage available", exception.Message); + + // Verify storage validation was called + await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); + + // Verify no further methods were called + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + } + + [Fact] + public async Task SaveFileSendAsync_UserCanAccessPremium_StorageFull_ThrowsBadRequest() + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = userId + }; + var fileData = new SendFileData(); + var fileLength = 1024L; // 1KB + + // Configure validation service to return less storage remaining than needed + _sendValidationService.StorageRemainingForSendAsync(send).Returns(512L); // Only 512 bytes available + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + Assert.Contains("Not enough storage available", exception.Message); + + // Verify storage validation was called + await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); + + // Verify no further methods were called + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + } + + [Fact] + public async Task SaveFileSendAsync_UserCanAccessPremium_IsNotPremium_IsSelfHosted_GiantFile_ThrowsBadRequest() + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = userId + }; + var fileData = new SendFileData(); + var fileLength = 15L * 1024L * 1024L * 1024L; // 15GB + + // Configure validation service to return large but insufficient storage (10GB for self-hosted non-premium) + _sendValidationService.StorageRemainingForSendAsync(send) + .Returns(10L * 1024L * 1024L * 1024L); // 10GB remaining (self-hosted default) + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + Assert.Contains("Not enough storage available", exception.Message); + + // Verify storage validation was called + await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); + + // Verify no further methods were called + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + } + + [Fact] + public async Task SaveFileSendAsync_UserCanAccessPremium_IsNotPremium_IsNotSelfHosted_TwoGigabyteFile_ThrowsBadRequest() + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = userId + }; + var fileData = new SendFileData(); + var fileLength = 2L * 1024L * 1024L * 1024L; // 2GB + + // Configure validation service to return 1GB storage (cloud non-premium default) + _sendValidationService.StorageRemainingForSendAsync(send) + .Returns(1L * 1024L * 1024L * 1024L); // 1GB remaining (cloud default) + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + Assert.Contains("Not enough storage available", exception.Message); + + // Verify storage validation was called + await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); + + // Verify no further methods were called + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + } + + [Fact] + public async Task SaveFileSendAsync_ThroughOrg_MaxStorageIsNull_ThrowsBadRequest() + { + // Arrange + var organizationId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + OrganizationId = organizationId + }; + + var fileData = new SendFileData + { + FileName = "test.txt" + }; + + const long fileLength = 1000; + + // Set up validation service to return 0 storage remaining + // This simulates the case when an organization's max storage is null + _sendValidationService.StorageRemainingForSendAsync(send).Returns(0L); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + Assert.Equal("Not enough storage available.", exception.Message); + + // Verify the method was called exactly once + await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); + } + + [Fact] + public async Task SaveFileSendAsync_ThroughOrg_MaxStorageIsNull_TwoGBFile_ThrowsBadRequest() + { + // Arrange + var orgId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + OrganizationId = orgId, + UserId = null + }; + var fileData = new SendFileData(); + var fileLength = 2L * 1024L * 1024L * 1024L; // 2GB + + // Configure validation service to throw BadRequest when checking storage for org without storage + _sendValidationService.StorageRemainingForSendAsync(send) + .Throws(new BadRequestException("This organization cannot use file sends.")); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + Assert.Contains("This organization cannot use file sends", exception.Message); + + // Verify storage validation was called + await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); + + // Verify no further methods were called + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + } + + [Fact] + public async Task SaveFileSendAsync_ThroughOrg_MaxStorageIsOneGB_TwoGBFile_ThrowsBadRequest() + { + // Arrange + var orgId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + OrganizationId = orgId, + UserId = null + }; + var fileData = new SendFileData(); + var fileLength = 2L * 1024L * 1024L * 1024L; // 2GB + + // Configure validation service to return 1GB storage (org's max storage limit) + _sendValidationService.StorageRemainingForSendAsync(send) + .Returns(1L * 1024L * 1024L * 1024L); // 1GB remaining + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + Assert.Contains("Not enough storage available", exception.Message); + + // Verify storage validation was called + await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); + + // Verify no further methods were called + await _sendRepository.DidNotReceive().CreateAsync(Arg.Any()); + await _sendRepository.DidNotReceive().UpsertAsync(Arg.Any()); + await _sendFileStorageService.DidNotReceive().GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendCreateAsync(Arg.Any()); + await _pushNotificationService.DidNotReceive().PushSyncSendUpdateAsync(Arg.Any()); + } + + [Fact] + public async Task SaveFileSendAsync_HasEnoughStorage_Success() + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = userId + }; + var fileData = new SendFileData(); + var fileLength = 500L * 1024L; // 500KB + var expectedFileId = "generatedfileid"; + var expectedUploadUrl = "https://upload.example.com/url"; + + // Configure storage validation to return more storage than needed + _sendValidationService.StorageRemainingForSendAsync(send) + .Returns(1024L * 1024L); // 1MB remaining + + // Configure file storage service to return upload URL + _sendFileStorageService.GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()) + .Returns(expectedUploadUrl); + + // Set up string generator to return predictable file ID + _sendCoreHelperService.SecureRandomString(32, false, false) + .Returns(expectedFileId); + + // Act + var result = await _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength); + + // Assert + Assert.Equal(expectedUploadUrl, result); + + // Verify storage validation was called + await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); + + // Verify upload URL was requested + await _sendFileStorageService.Received(1).GetSendFileUploadUrlAsync(send, expectedFileId); + } + + [Fact] + public async Task SaveFileSendAsync_HasEnoughStorage_SendFileThrows_CleansUp() + { + // Arrange + var userId = Guid.NewGuid(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = userId + }; + var fileData = new SendFileData(); + var fileLength = 500L * 1024L; // 500KB + var expectedFileId = "generatedfileid"; + + // Configure storage validation to return more storage than needed + _sendValidationService.StorageRemainingForSendAsync(send) + .Returns(1024L * 1024L); // 1MB remaining + + // Set up string generator to return predictable file ID + _sendCoreHelperService.SecureRandomString(32, false, false) + .Returns(expectedFileId); + + // Configure file storage service to throw exception when getting upload URL + _sendFileStorageService.GetSendFileUploadUrlAsync(Arg.Any(), Arg.Any()) + .Throws(new Exception("Storage service unavailable")); + + // Act & Assert + await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.SaveFileSendAsync(send, fileData, fileLength)); + + // Verify storage validation was called + await _sendValidationService.Received(1).StorageRemainingForSendAsync(send); + + // Verify file was cleaned up after failure + await _sendFileStorageService.Received(1).DeleteFileAsync(send, expectedFileId); + } + + [Fact] + public async Task UpdateFileToExistingSendAsync_SendNull_ThrowsBadRequest() + { + // Arrange + Stream stream = new MemoryStream(); + Send send = null; + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send)); + + Assert.Equal("Send does not have file data", exception.Message); + + // Verify no interactions with storage service + await _sendFileStorageService.DidNotReceiveWithAnyArgs().UploadNewFileAsync( + Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task UpdateFileToExistingSendAsync_SendDataNull_ThrowsBadRequest() + { + // Arrange + Stream stream = new MemoryStream(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.File, + UserId = Guid.NewGuid(), + Data = null // Send exists but has null Data property + }; + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send)); + + Assert.Equal("Send does not have file data", exception.Message); + + // Verify no interactions with storage service + await _sendFileStorageService.DidNotReceiveWithAnyArgs().UploadNewFileAsync( + Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task UpdateFileToExistingSendAsync_NotFileType_ThrowsBadRequest() + { + // Arrange + Stream stream = new MemoryStream(); + var send = new Send + { + Id = Guid.NewGuid(), + Type = SendType.Text, // Not a file type + UserId = Guid.NewGuid(), + Data = "{\"someData\":\"value\"}" // Has data, but not file data + }; + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send)); + + Assert.Equal("Not a File Type Send.", exception.Message); + + // Verify no interactions with storage service + await _sendFileStorageService.DidNotReceiveWithAnyArgs().UploadNewFileAsync( + Arg.Any(), Arg.Any(), Arg.Any()); + } + + [Fact] + public async Task UpdateFileToExistingSendAsync_StreamPositionRestToZero_Success() + { + // Arrange + var stream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + stream.Position = 2; + var sendId = Guid.NewGuid(); + var userId = Guid.NewGuid(); + var fileId = "existingfileid123"; + + var sendFileData = new SendFileData { Id = fileId, Size = 1000, Validated = false }; + var send = new Send + { + Id = sendId, + UserId = userId, + Type = SendType.File, + Data = JsonSerializer.Serialize(sendFileData) + }; + + // Setup validation to succeed + _sendFileStorageService.ValidateFileAsync(send, sendFileData.Id, sendFileData.Size, SendFileSettingHelper.FILE_SIZE_LEEWAY).Returns((true, sendFileData.Size)); + + // Act + await _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send); + + // Assert + // Verify file was uploaded with correct parameters + await _sendFileStorageService.Received(1).UploadNewFileAsync( + Arg.Is(s => s == stream && s.Position == 0), // Ensure stream position is reset + Arg.Is(s => s.Id == sendId && s.UserId == userId), + Arg.Is(id => id == fileId) + ); + } + + + [Fact] + public async Task UploadFileToExistingSendAsync_Success() + { + // Arrange + var stream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + stream.Position = 2; // Simulate a non-zero position + var sendId = Guid.NewGuid(); + var userId = Guid.NewGuid(); + var fileId = "existingfileid123"; + + var sendFileData = new SendFileData { Id = fileId, Size = 1000, Validated = false }; + var send = new Send + { + Id = sendId, + UserId = userId, + Type = SendType.File, + Data = JsonSerializer.Serialize(sendFileData) + }; + + _sendFileStorageService.ValidateFileAsync(send, sendFileData.Id, sendFileData.Size, SendFileSettingHelper.FILE_SIZE_LEEWAY).Returns((true, sendFileData.Size)); + + // Act + await _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send); + + // Assert + // Verify file was uploaded with correct parameters + await _sendFileStorageService.Received(1).UploadNewFileAsync( + Arg.Is(s => s == stream && s.Position == 0), // Ensure stream position is reset + Arg.Is(s => s.Id == sendId && s.UserId == userId), + Arg.Is(id => id == fileId) + ); + } + + [Fact] + public async Task UpdateFileToExistingSendAsync_InvalidSize_ThrowsBadRequest() + { + // Arrange + var stream = new MemoryStream(new byte[] { 1, 2, 3, 4, 5 }); + var sendId = Guid.NewGuid(); + var userId = Guid.NewGuid(); + var fileId = "existingfileid123"; + + var sendFileData = new SendFileData { Id = fileId, Size = 1000, Validated = false }; + var send = new Send + { + Id = sendId, + UserId = userId, + Type = SendType.File, + Data = JsonSerializer.Serialize(sendFileData) + }; + + // Configure storage service to upload successfully + _sendFileStorageService.UploadNewFileAsync( + Arg.Any(), Arg.Any(), Arg.Any()) + .Returns(Task.CompletedTask); + + // Configure validation to fail due to file size mismatch + _nonAnonymousSendCommand.ConfirmFileSize(send) + .Returns(false); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _nonAnonymousSendCommand.UploadFileToExistingSendAsync(stream, send)); + + Assert.Equal("File received does not match expected file length.", exception.Message); + } +} diff --git a/test/Core.Test/Tools/Services/SendAuthorizationServiceTests.cs b/test/Core.Test/Tools/Services/SendAuthorizationServiceTests.cs new file mode 100644 index 0000000000..9b2637d030 --- /dev/null +++ b/test/Core.Test/Tools/Services/SendAuthorizationServiceTests.cs @@ -0,0 +1,175 @@ +using Bit.Core.Context; +using Bit.Core.Platform.Push; +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.Repositories; +using Bit.Core.Tools.Services; +using Microsoft.AspNetCore.Identity; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Tools.Services; + +public class SendAuthorizationServiceTests +{ + private readonly ISendRepository _sendRepository; + private readonly IPasswordHasher _passwordHasher; + private readonly IPushNotificationService _pushNotificationService; + private readonly IReferenceEventService _referenceEventService; + private readonly ICurrentContext _currentContext; + private readonly SendAuthorizationService _sendAuthorizationService; + + public SendAuthorizationServiceTests() + { + _sendRepository = Substitute.For(); + _passwordHasher = Substitute.For>(); + _pushNotificationService = Substitute.For(); + _referenceEventService = Substitute.For(); + _currentContext = Substitute.For(); + + _sendAuthorizationService = new SendAuthorizationService( + _sendRepository, + _passwordHasher, + _pushNotificationService, + _referenceEventService, + _currentContext); + } + + + [Fact] + public void SendCanBeAccessed_Success_ReturnsTrue() + { + // Arrange + var send = new Send + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + MaxAccessCount = 10, + AccessCount = 5, + ExpirationDate = DateTime.UtcNow.AddYears(1), + DeletionDate = DateTime.UtcNow.AddYears(1), + Disabled = false, + Password = "hashedPassword123" + }; + + const string password = "TEST"; + + _passwordHasher + .VerifyHashedPassword(Arg.Any(), send.Password, password) + .Returns(PasswordVerificationResult.Success); + + // Act + var result = + _sendAuthorizationService.SendCanBeAccessed(send, password); + + // Assert + Assert.Equal(SendAccessResult.Granted, result); + } + + [Fact] + public void SendCanBeAccessed_NullMaxAccess_Success() + { + // Arrange + var send = new Send + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + MaxAccessCount = null, + AccessCount = 5, + ExpirationDate = DateTime.UtcNow.AddYears(1), + DeletionDate = DateTime.UtcNow.AddYears(1), + Disabled = false, + Password = "hashedPassword123" + }; + + const string password = "TEST"; + + _passwordHasher + .VerifyHashedPassword(Arg.Any(), send.Password, password) + .Returns(PasswordVerificationResult.Success); + + // Act + var result = _sendAuthorizationService.SendCanBeAccessed(send, password); + + // Assert + Assert.Equal(SendAccessResult.Granted, result); + } + + [Fact] + public void SendCanBeAccessed_NullSend_DoesNotGrantAccess() + { + // Arrange + _passwordHasher + .VerifyHashedPassword(Arg.Any(), "TEST", "TEST") + .Returns(PasswordVerificationResult.Success); + + // Act + var result = + _sendAuthorizationService.SendCanBeAccessed(null, "TEST"); + + // Assert + Assert.Equal(SendAccessResult.Denied, result); + } + + [Fact] + public void SendCanBeAccessed_RehashNeeded_RehashesPassword() + { + // Arrange + var now = DateTime.UtcNow; + var send = new Send + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + MaxAccessCount = null, + AccessCount = 5, + ExpirationDate = now.AddYears(1), + DeletionDate = now.AddYears(1), + Disabled = false, + Password = "TEST" + }; + + _passwordHasher + .VerifyHashedPassword(Arg.Any(), "TEST", "TEST") + .Returns(PasswordVerificationResult.SuccessRehashNeeded); + + // Act + var result = + _sendAuthorizationService.SendCanBeAccessed(send, "TEST"); + + // Assert + _passwordHasher + .Received(1) + .HashPassword(Arg.Any(), "TEST"); + + Assert.Equal(SendAccessResult.Granted, result); + } + + [Fact] + public void SendCanBeAccessed_VerifyFailed_PasswordInvalidReturnsTrue() + { + // Arrange + var now = DateTime.UtcNow; + var send = new Send + { + Id = Guid.NewGuid(), + UserId = Guid.NewGuid(), + MaxAccessCount = null, + AccessCount = 5, + ExpirationDate = now.AddYears(1), + DeletionDate = now.AddYears(1), + Disabled = false, + Password = "TEST" + }; + + _passwordHasher + .VerifyHashedPassword(Arg.Any(), "TEST", "TEST") + .Returns(PasswordVerificationResult.Failed); + + // Act + var result = + _sendAuthorizationService.SendCanBeAccessed(send, "TEST"); + + // Assert + Assert.Equal(SendAccessResult.PasswordInvalid, result); + } +} diff --git a/test/Core.Test/Tools/Services/SendServiceTests.cs b/test/Core.Test/Tools/Services/SendServiceTests.cs deleted file mode 100644 index 86d476340d..0000000000 --- a/test/Core.Test/Tools/Services/SendServiceTests.cs +++ /dev/null @@ -1,867 +0,0 @@ -using System.Text; -using System.Text.Json; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.AdminConsole.Enums; -using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies; -using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; -using Bit.Core.AdminConsole.Services; -using Bit.Core.Entities; -using Bit.Core.Exceptions; -using Bit.Core.Models.Data.Organizations.OrganizationUsers; -using Bit.Core.Platform.Push; -using Bit.Core.Repositories; -using Bit.Core.Services; -using Bit.Core.Test.AutoFixture.CurrentContextFixtures; -using Bit.Core.Test.Entities; -using Bit.Core.Test.Tools.AutoFixture.SendFixtures; -using Bit.Core.Tools.Entities; -using Bit.Core.Tools.Enums; -using Bit.Core.Tools.Models.Data; -using Bit.Core.Tools.Repositories; -using Bit.Core.Tools.Services; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using Microsoft.AspNetCore.Identity; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -using GlobalSettings = Bit.Core.Settings.GlobalSettings; - -namespace Bit.Core.Test.Tools.Services; - -[SutProviderCustomize] -[CurrentContextCustomize] -[UserSendCustomize] -public class SendServiceTests -{ - private void SaveSendAsync_Setup(SendType sendType, bool disableSendPolicyAppliesToUser, - SutProvider sutProvider, Send send) - { - send.Id = default; - send.Type = sendType; - - sutProvider.GetDependency().AnyPoliciesApplicableToUserAsync( - Arg.Any(), PolicyType.DisableSend).Returns(disableSendPolicyAppliesToUser); - } - - // Disable Send policy check - - [Theory] - [BitAutoData(SendType.File)] - [BitAutoData(SendType.Text)] - public async Task SaveSendAsync_DisableSend_Applies_throws(SendType sendType, - SutProvider sutProvider, Send send) - { - SaveSendAsync_Setup(sendType, disableSendPolicyAppliesToUser: true, sutProvider, send); - - await Assert.ThrowsAsync(() => sutProvider.Sut.SaveSendAsync(send)); - } - - [Theory] - [BitAutoData(SendType.File)] - [BitAutoData(SendType.Text)] - public async Task SaveSendAsync_DisableSend_DoesntApply_success(SendType sendType, - SutProvider sutProvider, Send send) - { - SaveSendAsync_Setup(sendType, disableSendPolicyAppliesToUser: false, sutProvider, send); - - await sutProvider.Sut.SaveSendAsync(send); - - await sutProvider.GetDependency().Received(1).CreateAsync(send); - } - - // Send Options Policy - Disable Hide Email check - - private void SaveSendAsync_HideEmail_Setup(bool disableHideEmailAppliesToUser, - SutProvider sutProvider, Send send, Policy policy) - { - send.HideEmail = true; - - var sendOptions = new SendOptionsPolicyData - { - DisableHideEmail = disableHideEmailAppliesToUser - }; - policy.Data = JsonSerializer.Serialize(sendOptions, new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - }); - - sutProvider.GetDependency().GetPoliciesApplicableToUserAsync( - Arg.Any(), PolicyType.SendOptions).Returns(new List() - { - new() { PolicyType = policy.Type, PolicyData = policy.Data, OrganizationId = policy.OrganizationId, PolicyEnabled = policy.Enabled } - }); - } - - [Theory] - [BitAutoData(SendType.File)] - [BitAutoData(SendType.Text)] - public async Task SaveSendAsync_DisableHideEmail_Applies_throws(SendType sendType, - SutProvider sutProvider, Send send, Policy policy) - { - SaveSendAsync_Setup(sendType, false, sutProvider, send); - SaveSendAsync_HideEmail_Setup(true, sutProvider, send, policy); - - await Assert.ThrowsAsync(() => sutProvider.Sut.SaveSendAsync(send)); - } - - [Theory] - [BitAutoData(SendType.File)] - [BitAutoData(SendType.Text)] - public async Task SaveSendAsync_DisableHideEmail_DoesntApply_success(SendType sendType, - SutProvider sutProvider, Send send, Policy policy) - { - SaveSendAsync_Setup(sendType, false, sutProvider, send); - SaveSendAsync_HideEmail_Setup(false, sutProvider, send, policy); - - await sutProvider.Sut.SaveSendAsync(send); - - await sutProvider.GetDependency().Received(1).CreateAsync(send); - } - - // Disable Send policy check - vNext - private void SaveSendAsync_Setup_vNext(SutProvider sutProvider, Send send, - DisableSendPolicyRequirement disableSendPolicyRequirement, SendOptionsPolicyRequirement sendOptionsPolicyRequirement) - { - sutProvider.GetDependency().GetAsync(send.UserId!.Value) - .Returns(disableSendPolicyRequirement); - sutProvider.GetDependency().GetAsync(send.UserId!.Value) - .Returns(sendOptionsPolicyRequirement); - sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); - - // Should not be called in these tests - sutProvider.GetDependency().AnyPoliciesApplicableToUserAsync( - Arg.Any(), Arg.Any()).ThrowsAsync(); - } - - [Theory] - [BitAutoData(SendType.File)] - [BitAutoData(SendType.Text)] - public async Task SaveSendAsync_DisableSend_Applies_Throws_vNext(SendType sendType, - SutProvider sutProvider, [NewUserSendCustomize] Send send) - { - send.Type = sendType; - SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement { DisableSend = true }, new SendOptionsPolicyRequirement()); - - var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.SaveSendAsync(send)); - Assert.Contains("Due to an Enterprise Policy, you are only able to delete an existing Send.", - exception.Message); - } - - [Theory] - [BitAutoData(SendType.File)] - [BitAutoData(SendType.Text)] - public async Task SaveSendAsync_DisableSend_DoesntApply_Success_vNext(SendType sendType, - SutProvider sutProvider, [NewUserSendCustomize] Send send) - { - send.Type = sendType; - SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement()); - - await sutProvider.Sut.SaveSendAsync(send); - - await sutProvider.GetDependency().Received(1).CreateAsync(send); - } - - // Send Options Policy - Disable Hide Email check - - [Theory] - [BitAutoData(SendType.File)] - [BitAutoData(SendType.Text)] - public async Task SaveSendAsync_DisableHideEmail_Applies_Throws_vNext(SendType sendType, - SutProvider sutProvider, [NewUserSendCustomize] Send send) - { - send.Type = sendType; - SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement { DisableHideEmail = true }); - send.HideEmail = true; - - var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.SaveSendAsync(send)); - Assert.Contains("Due to an Enterprise Policy, you are not allowed to hide your email address from recipients when creating or editing a Send.", exception.Message); - } - - [Theory] - [BitAutoData(SendType.File)] - [BitAutoData(SendType.Text)] - public async Task SaveSendAsync_DisableHideEmail_Applies_ButEmailNotHidden_Success_vNext(SendType sendType, - SutProvider sutProvider, [NewUserSendCustomize] Send send) - { - send.Type = sendType; - SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement { DisableHideEmail = true }); - send.HideEmail = false; - - await sutProvider.Sut.SaveSendAsync(send); - - await sutProvider.GetDependency().Received(1).CreateAsync(send); - } - - [Theory] - [BitAutoData(SendType.File)] - [BitAutoData(SendType.Text)] - public async Task SaveSendAsync_DisableHideEmail_DoesntApply_Success_vNext(SendType sendType, - SutProvider sutProvider, [NewUserSendCustomize] Send send) - { - send.Type = sendType; - SaveSendAsync_Setup_vNext(sutProvider, send, new DisableSendPolicyRequirement(), new SendOptionsPolicyRequirement()); - send.HideEmail = true; - - await sutProvider.Sut.SaveSendAsync(send); - - await sutProvider.GetDependency().Received(1).CreateAsync(send); - } - - [Theory] - [BitAutoData] - public async Task SaveSendAsync_ExistingSend_Updates(SutProvider sutProvider, - Send send) - { - send.Id = Guid.NewGuid(); - - var now = DateTime.UtcNow; - await sutProvider.Sut.SaveSendAsync(send); - - Assert.True(send.RevisionDate - now < TimeSpan.FromSeconds(1)); - - await sutProvider.GetDependency() - .Received(1) - .UpsertAsync(send); - - await sutProvider.GetDependency() - .Received(1) - .PushSyncSendUpdateAsync(send); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_TextType_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - send.Type = SendType.Text; - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, null, 0) - ); - - Assert.Contains("not of type \"file\"", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_EmptyFile_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - send.Type = SendType.File; - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, null, 0) - ); - - Assert.Contains("no file data", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_UserCannotAccessPremium_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - var user = new User - { - Id = Guid.NewGuid(), - }; - - send.UserId = user.Id; - send.Type = SendType.File; - - sutProvider.GetDependency() - .GetByIdAsync(user.Id) - .Returns(user); - - sutProvider.GetDependency() - .CanAccessPremium(user) - .Returns(false); - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, null, 1) - ); - - Assert.Contains("must have premium", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_UserHasUnconfirmedEmail_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - var user = new User - { - Id = Guid.NewGuid(), - EmailVerified = false, - }; - - send.UserId = user.Id; - send.Type = SendType.File; - - sutProvider.GetDependency() - .GetByIdAsync(user.Id) - .Returns(user); - - sutProvider.GetDependency() - .CanAccessPremium(user) - .Returns(true); - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, null, 1) - ); - - Assert.Contains("must confirm your email", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_UserCanAccessPremium_HasNoStorage_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - var user = new User - { - Id = Guid.NewGuid(), - EmailVerified = true, - Premium = true, - MaxStorageGb = null, - Storage = 0, - }; - - send.UserId = user.Id; - send.Type = SendType.File; - - sutProvider.GetDependency() - .GetByIdAsync(user.Id) - .Returns(user); - - sutProvider.GetDependency() - .CanAccessPremium(user) - .Returns(true); - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, null, 1) - ); - - Assert.Contains("not enough storage", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_UserCanAccessPremium_StorageFull_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - var user = new User - { - Id = Guid.NewGuid(), - EmailVerified = true, - Premium = true, - MaxStorageGb = 2, - Storage = 2 * UserTests.Multiplier, - }; - - send.UserId = user.Id; - send.Type = SendType.File; - - sutProvider.GetDependency() - .GetByIdAsync(user.Id) - .Returns(user); - - sutProvider.GetDependency() - .CanAccessPremium(user) - .Returns(true); - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, null, 1) - ); - - Assert.Contains("not enough storage", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_UserCanAccessPremium_IsNotPremium_IsSelfHosted_GiantFile_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - var user = new User - { - Id = Guid.NewGuid(), - EmailVerified = true, - Premium = false, - }; - - send.UserId = user.Id; - send.Type = SendType.File; - - sutProvider.GetDependency() - .GetByIdAsync(user.Id) - .Returns(user); - - sutProvider.GetDependency() - .CanAccessPremium(user) - .Returns(true); - - sutProvider.GetDependency() - .SelfHosted = true; - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, null, 11000 * UserTests.Multiplier) - ); - - Assert.Contains("not enough storage", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_UserCanAccessPremium_IsNotPremium_IsNotSelfHosted_TwoGigabyteFile_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - var user = new User - { - Id = Guid.NewGuid(), - EmailVerified = true, - Premium = false, - }; - - send.UserId = user.Id; - send.Type = SendType.File; - - sutProvider.GetDependency() - .GetByIdAsync(user.Id) - .Returns(user); - - sutProvider.GetDependency() - .CanAccessPremium(user) - .Returns(true); - - sutProvider.GetDependency() - .SelfHosted = false; - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, null, 2 * UserTests.Multiplier) - ); - - Assert.Contains("not enough storage", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_ThroughOrg_MaxStorageIsNull_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - var org = new Organization - { - Id = Guid.NewGuid(), - MaxStorageGb = null, - }; - - send.UserId = null; - send.OrganizationId = org.Id; - send.Type = SendType.File; - - sutProvider.GetDependency() - .GetByIdAsync(org.Id) - .Returns(org); - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, null, 1) - ); - - Assert.Contains("organization cannot use file sends", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_ThroughOrg_MaxStorageIsNull_TwoGBFile_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - var org = new Organization - { - Id = Guid.NewGuid(), - MaxStorageGb = null, - }; - - send.UserId = null; - send.OrganizationId = org.Id; - send.Type = SendType.File; - - sutProvider.GetDependency() - .GetByIdAsync(org.Id) - .Returns(org); - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, null, 1) - ); - - Assert.Contains("organization cannot use file sends", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_ThroughOrg_MaxStorageIsOneGB_TwoGBFile_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - var org = new Organization - { - Id = Guid.NewGuid(), - MaxStorageGb = 1, - }; - - send.UserId = null; - send.OrganizationId = org.Id; - send.Type = SendType.File; - - sutProvider.GetDependency() - .GetByIdAsync(org.Id) - .Returns(org); - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, null, 2 * UserTests.Multiplier) - ); - - Assert.Contains("not enough storage", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_HasEnoughStorage_Success(SutProvider sutProvider, - Send send) - { - var user = new User - { - Id = Guid.NewGuid(), - EmailVerified = true, - MaxStorageGb = 10, - }; - - var data = new SendFileData - { - - }; - - send.UserId = user.Id; - send.Type = SendType.File; - - var testUrl = "https://test.com/"; - - sutProvider.GetDependency() - .GetByIdAsync(user.Id) - .Returns(user); - - sutProvider.GetDependency() - .CanAccessPremium(user) - .Returns(true); - - sutProvider.GetDependency() - .GetSendFileUploadUrlAsync(send, Arg.Any()) - .Returns(testUrl); - - var utcNow = DateTime.UtcNow; - - var url = await sutProvider.Sut.SaveFileSendAsync(send, data, 1 * UserTests.Multiplier); - - Assert.Equal(testUrl, url); - Assert.True(send.RevisionDate - utcNow < TimeSpan.FromSeconds(1)); - - await sutProvider.GetDependency() - .Received(1) - .GetSendFileUploadUrlAsync(send, Arg.Any()); - - await sutProvider.GetDependency() - .Received(1) - .UpsertAsync(send); - - await sutProvider.GetDependency() - .Received(1) - .PushSyncSendUpdateAsync(send); - } - - [Theory] - [BitAutoData] - public async Task SaveFileSendAsync_HasEnoughStorage_SendFileThrows_CleansUp(SutProvider sutProvider, - Send send) - { - var user = new User - { - Id = Guid.NewGuid(), - EmailVerified = true, - MaxStorageGb = 10, - }; - - var data = new SendFileData - { - - }; - - send.UserId = user.Id; - send.Type = SendType.File; - - sutProvider.GetDependency() - .GetByIdAsync(user.Id) - .Returns(user); - - sutProvider.GetDependency() - .CanAccessPremium(user) - .Returns(true); - - sutProvider.GetDependency() - .GetSendFileUploadUrlAsync(send, Arg.Any()) - .Returns(callInfo => throw new Exception("Problem")); - - var utcNow = DateTime.UtcNow; - - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.SaveFileSendAsync(send, data, 1 * UserTests.Multiplier) - ); - - Assert.True(send.RevisionDate - utcNow < TimeSpan.FromSeconds(1)); - Assert.Equal("Problem", exception.Message); - - await sutProvider.GetDependency() - .Received(1) - .GetSendFileUploadUrlAsync(send, Arg.Any()); - - await sutProvider.GetDependency() - .Received(1) - .UpsertAsync(send); - - await sutProvider.GetDependency() - .Received(1) - .PushSyncSendUpdateAsync(send); - - await sutProvider.GetDependency() - .Received(1) - .DeleteFileAsync(send, Arg.Any()); - } - - [Theory] - [BitAutoData] - public async Task UpdateFileToExistingSendAsync_SendNull_ThrowsBadRequest(SutProvider sutProvider) - { - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.UploadFileToExistingSendAsync(new MemoryStream(), null) - ); - - Assert.Contains("does not have file data", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task UpdateFileToExistingSendAsync_SendDataNull_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - send.Data = null; - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.UploadFileToExistingSendAsync(new MemoryStream(), send) - ); - - Assert.Contains("does not have file data", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task UpdateFileToExistingSendAsync_NotFileType_ThrowsBadRequest(SutProvider sutProvider, - Send send) - { - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.UploadFileToExistingSendAsync(new MemoryStream(), send) - ); - - Assert.Contains("not a file type send", badRequest.Message, StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [BitAutoData] - public async Task UpdateFileToExistingSendAsync_Success(SutProvider sutProvider, - Send send) - { - var fileContents = "Test file content"; - - var sendFileData = new SendFileData - { - Id = "TEST", - Size = fileContents.Length, - Validated = false, - }; - - send.Type = SendType.File; - send.Data = JsonSerializer.Serialize(sendFileData); - - sutProvider.GetDependency() - .ValidateFileAsync(send, sendFileData.Id, sendFileData.Size, Arg.Any()) - .Returns((true, sendFileData.Size)); - - await sutProvider.Sut.UploadFileToExistingSendAsync(new MemoryStream(Encoding.UTF8.GetBytes(fileContents)), send); - } - - [Theory] - [BitAutoData] - public async Task UpdateFileToExistingSendAsync_InvalidSize(SutProvider sutProvider, - Send send) - { - var fileContents = "Test file content"; - - var sendFileData = new SendFileData - { - Id = "TEST", - Size = fileContents.Length, - }; - - send.Type = SendType.File; - send.Data = JsonSerializer.Serialize(sendFileData); - - sutProvider.GetDependency() - .ValidateFileAsync(send, sendFileData.Id, sendFileData.Size, Arg.Any()) - .Returns((false, sendFileData.Size)); - - var badRequest = await Assert.ThrowsAsync(() => - sutProvider.Sut.UploadFileToExistingSendAsync(new MemoryStream(Encoding.UTF8.GetBytes(fileContents)), send) - ); - } - - [Theory] - [BitAutoData] - public void SendCanBeAccessed_Success(SutProvider sutProvider, Send send) - { - var now = DateTime.UtcNow; - send.MaxAccessCount = 10; - send.AccessCount = 5; - send.ExpirationDate = now.AddYears(1); - send.DeletionDate = now.AddYears(1); - send.Disabled = false; - - sutProvider.GetDependency>() - .VerifyHashedPassword(Arg.Any(), send.Password, "TEST") - .Returns(PasswordVerificationResult.Success); - - var (grant, passwordRequiredError, passwordInvalidError) - = sutProvider.Sut.SendCanBeAccessed(send, "TEST"); - - Assert.True(grant); - Assert.False(passwordRequiredError); - Assert.False(passwordInvalidError); - } - - [Theory] - [BitAutoData] - public void SendCanBeAccessed_NullMaxAccess_Success(SutProvider sutProvider, - Send send) - { - var now = DateTime.UtcNow; - send.MaxAccessCount = null; - send.AccessCount = 5; - send.ExpirationDate = now.AddYears(1); - send.DeletionDate = now.AddYears(1); - send.Disabled = false; - - sutProvider.GetDependency>() - .VerifyHashedPassword(Arg.Any(), send.Password, "TEST") - .Returns(PasswordVerificationResult.Success); - - var (grant, passwordRequiredError, passwordInvalidError) - = sutProvider.Sut.SendCanBeAccessed(send, "TEST"); - - Assert.True(grant); - Assert.False(passwordRequiredError); - Assert.False(passwordInvalidError); - } - - [Theory] - [BitAutoData] - public void SendCanBeAccessed_NullSend_DoesNotGrantAccess(SutProvider sutProvider) - { - sutProvider.GetDependency>() - .VerifyHashedPassword(Arg.Any(), "TEST", "TEST") - .Returns(PasswordVerificationResult.Success); - - var (grant, passwordRequiredError, passwordInvalidError) - = sutProvider.Sut.SendCanBeAccessed(null, "TEST"); - - Assert.False(grant); - Assert.False(passwordRequiredError); - Assert.False(passwordInvalidError); - } - - [Theory] - [BitAutoData] - public void SendCanBeAccessed_NullPassword_PasswordRequiredErrorReturnsTrue(SutProvider sutProvider, - Send send) - { - var now = DateTime.UtcNow; - send.MaxAccessCount = null; - send.AccessCount = 5; - send.ExpirationDate = now.AddYears(1); - send.DeletionDate = now.AddYears(1); - send.Disabled = false; - send.Password = "HASH"; - - sutProvider.GetDependency>() - .VerifyHashedPassword(Arg.Any(), "TEST", "TEST") - .Returns(PasswordVerificationResult.Success); - - var (grant, passwordRequiredError, passwordInvalidError) - = sutProvider.Sut.SendCanBeAccessed(send, null); - - Assert.False(grant); - Assert.True(passwordRequiredError); - Assert.False(passwordInvalidError); - } - - [Theory] - [BitAutoData] - public void SendCanBeAccessed_RehashNeeded_RehashesPassword(SutProvider sutProvider, - Send send) - { - var now = DateTime.UtcNow; - send.MaxAccessCount = null; - send.AccessCount = 5; - send.ExpirationDate = now.AddYears(1); - send.DeletionDate = now.AddYears(1); - send.Disabled = false; - send.Password = "TEST"; - - sutProvider.GetDependency>() - .VerifyHashedPassword(Arg.Any(), "TEST", "TEST") - .Returns(PasswordVerificationResult.SuccessRehashNeeded); - - var (grant, passwordRequiredError, passwordInvalidError) - = sutProvider.Sut.SendCanBeAccessed(send, "TEST"); - - sutProvider.GetDependency>() - .Received(1) - .HashPassword(Arg.Any(), "TEST"); - - Assert.True(grant); - Assert.False(passwordRequiredError); - Assert.False(passwordInvalidError); - } - - [Theory] - [BitAutoData] - public void SendCanBeAccessed_VerifyFailed_PasswordInvalidReturnsTrue(SutProvider sutProvider, - Send send) - { - var now = DateTime.UtcNow; - send.MaxAccessCount = null; - send.AccessCount = 5; - send.ExpirationDate = now.AddYears(1); - send.DeletionDate = now.AddYears(1); - send.Disabled = false; - send.Password = "TEST"; - - sutProvider.GetDependency>() - .VerifyHashedPassword(Arg.Any(), "TEST", "TEST") - .Returns(PasswordVerificationResult.Failed); - - var (grant, passwordRequiredError, passwordInvalidError) - = sutProvider.Sut.SendCanBeAccessed(send, "TEST"); - - Assert.False(grant); - Assert.False(passwordRequiredError); - Assert.True(passwordInvalidError); - } -} From 725a793863b34951080c6a18f0f9105583d169b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Tue, 20 May 2025 14:35:47 +0100 Subject: [PATCH 079/114] [PM-15161] Create ProviderClientOrganizationSignUpCommand command (#5764) * Extract OrganizationService.SignupClientAsync into new ResellerClientOrganizationSignUpCommand * Refactor ResellerClientOrganizationSignUpCommand to remove unused dependencies and simplify SignupClientAsync method signature * Add unit tests for ResellerClientOrganizationSignUpCommand * Rename SignUpProviderClientOrganizationCommand * Rename ProviderClientOrganizationSignUpCommand * Register ProviderClientOrganizationSignUpCommand for dependency injection * Refactor ProviderService to use IProviderClientOrganizationSignUpCommand for organization signup process * Refactor error handling in ProviderClientOrganizationSignUpCommand to use constants for error messages * Remove SignupClientAsync method from IOrganizationService and OrganizationService, along with associated unit tests --- .../AdminConsole/Services/ProviderService.cs | 16 +- .../Services/ProviderServiceTests.cs | 17 +- ...ProviderClientOrganizationSignUpCommand.cs | 187 ++++++++++++++++++ .../Services/IOrganizationService.cs | 3 - .../Implementations/OrganizationService.cs | 60 ------ ...OrganizationServiceCollectionExtensions.cs | 5 +- ...derClientOrganizationSignUpCommandTests.cs | 169 ++++++++++++++++ .../Services/OrganizationServiceTests.cs | 42 ---- 8 files changed, 379 insertions(+), 120 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ProviderClientOrganizationSignUpCommandTests.cs diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs index 2fc44937a7..2925021d65 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs @@ -5,6 +5,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Business.Provider; using Bit.Core.AdminConsole.Models.Business.Tokenables; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Enums; @@ -53,6 +54,7 @@ public class ProviderService : IProviderService private readonly IApplicationCacheService _applicationCacheService; private readonly IProviderBillingService _providerBillingService; private readonly IPricingClient _pricingClient; + private readonly IProviderClientOrganizationSignUpCommand _providerClientOrganizationSignUpCommand; public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository, IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository, @@ -61,7 +63,8 @@ public class ProviderService : IProviderService IOrganizationRepository organizationRepository, GlobalSettings globalSettings, ICurrentContext currentContext, IStripeAdapter stripeAdapter, IFeatureService featureService, IDataProtectorTokenFactory providerDeleteTokenDataFactory, - IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient) + IApplicationCacheService applicationCacheService, IProviderBillingService providerBillingService, IPricingClient pricingClient, + IProviderClientOrganizationSignUpCommand providerClientOrganizationSignUpCommand) { _providerRepository = providerRepository; _providerUserRepository = providerUserRepository; @@ -81,6 +84,7 @@ public class ProviderService : IProviderService _applicationCacheService = applicationCacheService; _providerBillingService = providerBillingService; _pricingClient = pricingClient; + _providerClientOrganizationSignUpCommand = providerClientOrganizationSignUpCommand; } public async Task CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource = null) @@ -560,12 +564,12 @@ public class ProviderService : IProviderService ThrowOnInvalidPlanType(provider.Type, organizationSignup.Plan); - var (organization, _, defaultCollection) = await _organizationService.SignupClientAsync(organizationSignup); + var signUpResponse = await _providerClientOrganizationSignUpCommand.SignUpClientOrganizationAsync(organizationSignup); var providerOrganization = new ProviderOrganization { ProviderId = providerId, - OrganizationId = organization.Id, + OrganizationId = signUpResponse.Organization.Id, Key = organizationSignup.OwnerKey, }; @@ -574,12 +578,12 @@ public class ProviderService : IProviderService // Give the owner Can Manage access over the default collection // The orgUser is not available when the org is created so we have to do it here as part of the invite - var defaultOwnerAccess = defaultCollection != null + var defaultOwnerAccess = signUpResponse.DefaultCollection != null ? [ new CollectionAccessSelection { - Id = defaultCollection.Id, + Id = signUpResponse.DefaultCollection.Id, HidePasswords = false, ReadOnly = false, Manage = true @@ -587,7 +591,7 @@ public class ProviderService : IProviderService ] : Array.Empty(); - await _organizationService.InviteUsersAsync(organization.Id, user.Id, systemUser: null, + await _organizationService.InviteUsersAsync(signUpResponse.Organization.Id, user.Id, systemUser: null, new (OrganizationUserInvite, string)[] { ( diff --git a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs index c66acfa8ce..a07dc7b8f8 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs @@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Business.Provider; using Bit.Core.AdminConsole.Models.Business.Tokenables; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models; @@ -717,8 +718,8 @@ public class ProviderServiceTests sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); var providerOrganizationRepository = sutProvider.GetDependency(); - sutProvider.GetDependency().SignupClientAsync(organizationSignup) - .Returns((organization, null as OrganizationUser, new Collection())); + sutProvider.GetDependency().SignUpClientOrganizationAsync(organizationSignup) + .Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection())); var providerOrganization = await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user); @@ -755,8 +756,8 @@ public class ProviderServiceTests var providerOrganizationRepository = sutProvider.GetDependency(); - sutProvider.GetDependency().SignupClientAsync(organizationSignup) - .Returns((organization, null as OrganizationUser, new Collection())); + sutProvider.GetDependency().SignUpClientOrganizationAsync(organizationSignup) + .Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection())); await Assert.ThrowsAsync(() => sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user)); @@ -782,8 +783,8 @@ public class ProviderServiceTests var providerOrganizationRepository = sutProvider.GetDependency(); - sutProvider.GetDependency().SignupClientAsync(organizationSignup) - .Returns((organization, null as OrganizationUser, new Collection())); + sutProvider.GetDependency().SignUpClientOrganizationAsync(organizationSignup) + .Returns(new ProviderClientOrganizationSignUpResponse(organization, new Collection())); var providerOrganization = await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user); @@ -821,8 +822,8 @@ public class ProviderServiceTests sutProvider.GetDependency().GetByIdAsync(provider.Id).Returns(provider); var providerOrganizationRepository = sutProvider.GetDependency(); - sutProvider.GetDependency().SignupClientAsync(organizationSignup) - .Returns((organization, null as OrganizationUser, defaultCollection)); + sutProvider.GetDependency().SignUpClientOrganizationAsync(organizationSignup) + .Returns(new ProviderClientOrganizationSignUpResponse(organization, defaultCollection)); var providerOrganization = await sutProvider.Sut.CreateOrganizationAsync(provider.Id, organizationSignup, clientOwnerEmail, user); diff --git a/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs new file mode 100644 index 0000000000..b8802ffd0c --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Organizations/ProviderClientOrganizationSignUpCommand.cs @@ -0,0 +1,187 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Billing.Pricing; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Business; +using Bit.Core.Models.StaticStore; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Business; +using Bit.Core.Tools.Services; +using Bit.Core.Utilities; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Organizations; + +public record ProviderClientOrganizationSignUpResponse( + Organization Organization, + Collection DefaultCollection); + +public interface IProviderClientOrganizationSignUpCommand +{ + /// + /// Sign up a new client organization for a provider. + /// + /// The signup information. + /// A tuple containing the new organization and its default collection. + Task SignUpClientOrganizationAsync(OrganizationSignup signup); +} + +public class ProviderClientOrganizationSignUpCommand : IProviderClientOrganizationSignUpCommand +{ + public const string PlanNullErrorMessage = "Password Manager Plan was null."; + public const string PlanDisabledErrorMessage = "Password Manager Plan is disabled."; + public const string AdditionalSeatsNegativeErrorMessage = "You can't subtract Password Manager seats!"; + + private readonly ICurrentContext _currentContext; + private readonly IPricingClient _pricingClient; + private readonly IReferenceEventService _referenceEventService; + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; + private readonly IApplicationCacheService _applicationCacheService; + private readonly ICollectionRepository _collectionRepository; + + public ProviderClientOrganizationSignUpCommand( + ICurrentContext currentContext, + IPricingClient pricingClient, + IReferenceEventService referenceEventService, + IOrganizationRepository organizationRepository, + IOrganizationApiKeyRepository organizationApiKeyRepository, + IApplicationCacheService applicationCacheService, + ICollectionRepository collectionRepository) + { + _currentContext = currentContext; + _pricingClient = pricingClient; + _referenceEventService = referenceEventService; + _organizationRepository = organizationRepository; + _organizationApiKeyRepository = organizationApiKeyRepository; + _applicationCacheService = applicationCacheService; + _collectionRepository = collectionRepository; + } + + public async Task SignUpClientOrganizationAsync(OrganizationSignup signup) + { + var plan = await _pricingClient.GetPlanOrThrow(signup.Plan); + + ValidatePlan(plan, signup.AdditionalSeats); + + var organization = new Organization + { + // Pre-generate the org id so that we can save it with the Stripe subscription. + Id = CoreHelpers.GenerateComb(), + Name = signup.Name, + BillingEmail = signup.BillingEmail, + PlanType = plan!.Type, + Seats = signup.AdditionalSeats, + MaxCollections = plan.PasswordManager.MaxCollections, + MaxStorageGb = 1, + UsePolicies = plan.HasPolicies, + UseSso = plan.HasSso, + UseOrganizationDomains = plan.HasOrganizationDomains, + UseGroups = plan.HasGroups, + UseEvents = plan.HasEvents, + UseDirectory = plan.HasDirectory, + UseTotp = plan.HasTotp, + Use2fa = plan.Has2fa, + UseApi = plan.HasApi, + UseResetPassword = plan.HasResetPassword, + SelfHost = plan.HasSelfHost, + UsersGetPremium = plan.UsersGetPremium, + UseCustomPermissions = plan.HasCustomPermissions, + UseScim = plan.HasScim, + Plan = plan.Name, + Gateway = GatewayType.Stripe, + ReferenceData = signup.Owner.ReferenceData, + Enabled = true, + LicenseKey = CoreHelpers.SecureRandomString(20), + PublicKey = signup.PublicKey, + PrivateKey = signup.PrivateKey, + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + Status = OrganizationStatusType.Created, + UsePasswordManager = true, + // Secrets Manager not available for purchase with Consolidated Billing. + UseSecretsManager = false, + }; + + var returnValue = await SignUpAsync(organization, signup.CollectionName); + + await _referenceEventService.RaiseEventAsync( + new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext) + { + PlanName = plan.Name, + PlanType = plan.Type, + Seats = returnValue.Organization.Seats, + SignupInitiationPath = signup.InitiationPath, + Storage = returnValue.Organization.MaxStorageGb, + }); + + return returnValue; + } + + private static void ValidatePlan(Plan plan, int additionalSeats) + { + if (plan is null) + { + throw new BadRequestException(PlanNullErrorMessage); + } + + if (plan.Disabled) + { + throw new BadRequestException(PlanDisabledErrorMessage); + } + + if (additionalSeats < 0) + { + throw new BadRequestException(AdditionalSeatsNegativeErrorMessage); + } + } + + /// + /// Private helper method to create a new organization. + /// + private async Task SignUpAsync( + Organization organization, string collectionName) + { + try + { + await _organizationRepository.CreateAsync(organization); + await _organizationApiKeyRepository.CreateAsync(new OrganizationApiKey + { + OrganizationId = organization.Id, + ApiKey = CoreHelpers.SecureRandomString(30), + Type = OrganizationApiKeyType.Default, + RevisionDate = DateTime.UtcNow, + }); + await _applicationCacheService.UpsertOrganizationAbilityAsync(organization); + + Collection defaultCollection = null; + if (!string.IsNullOrWhiteSpace(collectionName)) + { + defaultCollection = new Collection + { + Name = collectionName, + OrganizationId = organization.Id, + CreationDate = organization.CreationDate, + RevisionDate = organization.CreationDate + }; + + await _collectionRepository.CreateAsync(defaultCollection, null, null); + } + + return new ProviderClientOrganizationSignUpResponse(organization, defaultCollection); + } + catch + { + if (organization.Id != default) + { + await _organizationRepository.DeleteAsync(organization); + await _applicationCacheService.DeleteOrganizationAbilityAsync(organization.Id); + } + + throw; + } + } +} diff --git a/src/Core/AdminConsole/Services/IOrganizationService.cs b/src/Core/AdminConsole/Services/IOrganizationService.cs index 8baad23f65..5fe68bd22e 100644 --- a/src/Core/AdminConsole/Services/IOrganizationService.cs +++ b/src/Core/AdminConsole/Services/IOrganizationService.cs @@ -18,9 +18,6 @@ public interface IOrganizationService Task AutoAddSeatsAsync(Organization organization, int seatsToAdd); Task AdjustSeatsAsync(Guid organizationId, int seatAdjustment); Task VerifyBankAsync(Guid organizationId, int amount1, int amount2); -#nullable enable - Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup); -#nullable disable /// /// Create a new organization on a self-hosted instance /// diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 4e9d9bdb8a..26ff421328 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -410,66 +410,6 @@ public class OrganizationService : IOrganizationService } } - public async Task<(Organization organization, OrganizationUser organizationUser, Collection defaultCollection)> SignupClientAsync(OrganizationSignup signup) - { - var plan = await _pricingClient.GetPlanOrThrow(signup.Plan); - - ValidatePlan(plan, signup.AdditionalSeats, "Password Manager"); - - var organization = new Organization - { - // Pre-generate the org id so that we can save it with the Stripe subscription. - Id = CoreHelpers.GenerateComb(), - Name = signup.Name, - BillingEmail = signup.BillingEmail, - PlanType = plan!.Type, - Seats = signup.AdditionalSeats, - MaxCollections = plan.PasswordManager.MaxCollections, - MaxStorageGb = 1, - UsePolicies = plan.HasPolicies, - UseSso = plan.HasSso, - UseOrganizationDomains = plan.HasOrganizationDomains, - UseGroups = plan.HasGroups, - UseEvents = plan.HasEvents, - UseDirectory = plan.HasDirectory, - UseTotp = plan.HasTotp, - Use2fa = plan.Has2fa, - UseApi = plan.HasApi, - UseResetPassword = plan.HasResetPassword, - SelfHost = plan.HasSelfHost, - UsersGetPremium = plan.UsersGetPremium, - UseCustomPermissions = plan.HasCustomPermissions, - UseScim = plan.HasScim, - Plan = plan.Name, - Gateway = GatewayType.Stripe, - ReferenceData = signup.Owner.ReferenceData, - Enabled = true, - LicenseKey = CoreHelpers.SecureRandomString(20), - PublicKey = signup.PublicKey, - PrivateKey = signup.PrivateKey, - CreationDate = DateTime.UtcNow, - RevisionDate = DateTime.UtcNow, - Status = OrganizationStatusType.Created, - UsePasswordManager = true, - // Secrets Manager not available for purchase with Consolidated Billing. - UseSecretsManager = false, - }; - - var returnValue = await SignUpAsync(organization, default, signup.OwnerKey, signup.CollectionName, false); - - await _referenceEventService.RaiseEventAsync( - new ReferenceEvent(ReferenceEventType.Signup, organization, _currentContext) - { - PlanName = plan.Name, - PlanType = plan.Type, - Seats = returnValue.Item1.Seats, - SignupInitiationPath = signup.InitiationPath, - Storage = returnValue.Item1.MaxStorageGb, - }); - - return returnValue; - } - private async Task ValidateSignUpPoliciesAsync(Guid ownerId) { var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(ownerId, PolicyType.SingleOrg); diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs index b016e329bf..2bc05017d5 100644 --- a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -69,8 +69,11 @@ public static class OrganizationServiceCollectionExtensions services.AddBaseOrganizationSubscriptionCommandsQueries(); } - private static IServiceCollection AddOrganizationSignUpCommands(this IServiceCollection services) => + private static void AddOrganizationSignUpCommands(this IServiceCollection services) + { services.AddScoped(); + services.AddScoped(); + } private static void AddOrganizationDeleteCommands(this IServiceCollection services) { diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ProviderClientOrganizationSignUpCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ProviderClientOrganizationSignUpCommandTests.cs new file mode 100644 index 0000000000..b13c7e5b65 --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Organizations/OrganizationSignUp/ProviderClientOrganizationSignUpCommandTests.cs @@ -0,0 +1,169 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.OrganizationFeatures.Organizations; +using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Pricing; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Business; +using Bit.Core.Models.Data; +using Bit.Core.Models.StaticStore; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Tools.Enums; +using Bit.Core.Tools.Models.Business; +using Bit.Core.Tools.Services; +using Bit.Core.Utilities; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Organizations.OrganizationSignUp; + +[SutProviderCustomize] +public class ProviderClientOrganizationSignUpCommandTests +{ + [Theory] + [BitAutoData(PlanType.TeamsAnnually)] + [BitAutoData(PlanType.TeamsMonthly)] + [BitAutoData(PlanType.EnterpriseAnnually)] + [BitAutoData(PlanType.EnterpriseMonthly)] + public async Task SignupClientAsync_ValidParameters_CreatesOrganizationSuccessfully( + PlanType planType, + OrganizationSignup signup, + string collectionName, + SutProvider sutProvider) + { + signup.Plan = planType; + signup.AdditionalSeats = 15; + signup.CollectionName = collectionName; + + var plan = StaticStore.GetPlan(signup.Plan); + sutProvider.GetDependency() + .GetPlanOrThrow(signup.Plan) + .Returns(plan); + + var result = await sutProvider.Sut.SignUpClientOrganizationAsync(signup); + + Assert.NotNull(result.Organization); + Assert.NotNull(result.DefaultCollection); + Assert.Equal(collectionName, result.DefaultCollection.Name); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync( + Arg.Is(o => + o.Name == signup.Name && + o.BillingEmail == signup.BillingEmail && + o.PlanType == plan.Type && + o.Seats == signup.AdditionalSeats && + o.MaxCollections == plan.PasswordManager.MaxCollections && + o.UsePasswordManager == true && + o.UseSecretsManager == false && + o.Status == OrganizationStatusType.Created + ) + ); + + await sutProvider.GetDependency() + .Received(1) + .RaiseEventAsync(Arg.Is(referenceEvent => + referenceEvent.Type == ReferenceEventType.Signup && + referenceEvent.PlanName == plan.Name && + referenceEvent.PlanType == plan.Type && + referenceEvent.Seats == result.Organization.Seats && + referenceEvent.Storage == result.Organization.MaxStorageGb && + referenceEvent.SignupInitiationPath == signup.InitiationPath + )); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync( + Arg.Is(c => + c.Name == collectionName && + c.OrganizationId == result.Organization.Id + ), + Arg.Any>(), + Arg.Any>() + ); + + await sutProvider.GetDependency() + .Received(1) + .CreateAsync( + Arg.Is(k => + k.OrganizationId == result.Organization.Id && + k.Type == OrganizationApiKeyType.Default + ) + ); + + await sutProvider.GetDependency() + .Received(1) + .UpsertOrganizationAbilityAsync(Arg.Is(o => o.Id == result.Organization.Id)); + } + + [Theory] + [BitAutoData] + public async Task SignupClientAsync_NullPlan_ThrowsBadRequestException( + OrganizationSignup signup, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetPlanOrThrow(signup.Plan) + .Returns((Plan)null); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpClientOrganizationAsync(signup)); + + Assert.Contains(ProviderClientOrganizationSignUpCommand.PlanNullErrorMessage, exception.Message); + } + + [Theory] + [BitAutoData] + public async Task SignupClientAsync_NegativeAdditionalSeats_ThrowsBadRequestException( + OrganizationSignup signup, + SutProvider sutProvider) + { + signup.Plan = PlanType.TeamsMonthly; + signup.AdditionalSeats = -5; + + var plan = StaticStore.GetPlan(signup.Plan); + sutProvider.GetDependency() + .GetPlanOrThrow(signup.Plan) + .Returns(plan); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpClientOrganizationAsync(signup)); + + Assert.Contains(ProviderClientOrganizationSignUpCommand.AdditionalSeatsNegativeErrorMessage, exception.Message); + } + + [Theory] + [BitAutoData(PlanType.TeamsAnnually)] + public async Task SignupClientAsync_WhenExceptionIsThrown_CleanupIsPerformed( + PlanType planType, + OrganizationSignup signup, + SutProvider sutProvider) + { + signup.Plan = planType; + + var plan = StaticStore.GetPlan(signup.Plan); + sutProvider.GetDependency() + .GetPlanOrThrow(signup.Plan) + .Returns(plan); + + sutProvider.GetDependency() + .When(x => x.CreateAsync(Arg.Any())) + .Do(_ => throw new Exception()); + + var thrownException = await Assert.ThrowsAsync( + () => sutProvider.Sut.SignUpClientOrganizationAsync(signup)); + + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(Arg.Is(o => o.Name == signup.Name)); + + await sutProvider.GetDependency() + .Received(1) + .DeleteOrganizationAbilityAsync(Arg.Any()); + } +} diff --git a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs index c138cfac2e..18f1f79900 100644 --- a/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/AdminConsole/Services/OrganizationServiceTests.cs @@ -9,7 +9,6 @@ using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; using Bit.Core.Context; -using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; @@ -177,47 +176,6 @@ public class OrganizationServiceTests referenceEvent.Users == expectedNewUsersCount)); } - [Theory, BitAutoData] - public async Task SignupClientAsync_Succeeds( - OrganizationSignup signup, - SutProvider sutProvider) - { - signup.Plan = PlanType.TeamsMonthly; - - var plan = StaticStore.GetPlan(signup.Plan); - - sutProvider.GetDependency().GetPlanOrThrow(signup.Plan).Returns(plan); - - var (organization, _, _) = await sutProvider.Sut.SignupClientAsync(signup); - - await sutProvider.GetDependency().Received(1).CreateAsync(Arg.Is(org => - org.Id == organization.Id && - org.Name == signup.Name && - org.Plan == plan.Name && - org.PlanType == plan.Type && - org.UsePolicies == plan.HasPolicies && - org.PublicKey == signup.PublicKey && - org.PrivateKey == signup.PrivateKey && - org.UseSecretsManager == false)); - - await sutProvider.GetDependency().Received(1) - .CreateAsync(Arg.Is(orgApiKey => - orgApiKey.OrganizationId == organization.Id)); - - await sutProvider.GetDependency().Received(1) - .UpsertOrganizationAbilityAsync(organization); - - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().CreateAsync(default); - - await sutProvider.GetDependency().Received(1) - .CreateAsync(Arg.Is(c => c.Name == signup.CollectionName && c.OrganizationId == organization.Id), null, null); - - await sutProvider.GetDependency().Received(1).RaiseEventAsync(Arg.Is( - re => - re.Type == ReferenceEventType.Signup && - re.PlanType == plan.Type)); - } - [Theory] [OrganizationInviteCustomize(InviteeUserType = OrganizationUserType.User, InvitorUserType = OrganizationUserType.Owner), OrganizationCustomize, BitAutoData] From 790173d1c710825289a8a436a8a04bb0360bb4cc Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Tue, 20 May 2025 10:33:40 -0400 Subject: [PATCH 080/114] remove feature flag (#5837) --- .../AdminConsole/Controllers/OrganizationDomainController.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs b/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs index b9afde2724..a8882dfaf3 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs @@ -2,13 +2,11 @@ using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.Models.Response; -using Bit.Core; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Repositories; -using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; @@ -137,7 +135,6 @@ public class OrganizationDomainController : Controller [AllowAnonymous] [HttpPost("domain/sso/verified")] - [RequireFeature(FeatureFlagKeys.VerifiedSsoDomainEndpoint)] public async Task GetVerifiedOrgDomainSsoDetailsAsync( [FromBody] OrganizationDomainSsoDetailsRequestModel model) { From 3aa9812353d50669315123f510aeff3ba6609557 Mon Sep 17 00:00:00 2001 From: Vince Grassia <593223+vgrassia@users.noreply.github.com> Date: Tue, 20 May 2025 16:44:01 +0000 Subject: [PATCH 081/114] Fix actionlint issues (#5841) --- .github/workflows/build.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5077f1ba32..19eea71b6a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -636,7 +636,9 @@ jobs: setup-ephemeral-environment: name: Setup Ephemeral Environment - needs: build-docker + needs: + - build-artifacts + - build-docker if: | needs.build-artifacts.outputs.has_secrets == 'true' && github.event_name == 'pull_request' From e994bf21177e2874d24e1c2893fbab52bfdd3fa0 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 21 May 2025 08:10:34 -0400 Subject: [PATCH 082/114] [PM-21383] Use Stripe to get provider pricing for display when feature flag is on (#5842) * Use ProviderPriceAdapter when getting provider subscription * Update test --- .../Controllers/ProviderBillingController.cs | 21 +++++++++++++++ .../Responses/ProviderSubscriptionResponse.cs | 2 +- .../Billing/Models/ConfiguredProviderPlan.cs | 1 + src/Core/Constants.cs | 1 + src/Core/Services/IStripeAdapter.cs | 1 + .../Services/Implementations/StripeAdapter.cs | 3 +++ .../ProviderBillingControllerTests.cs | 27 ++++++++++++++++++- 7 files changed, 54 insertions(+), 2 deletions(-) diff --git a/src/Api/Billing/Controllers/ProviderBillingController.cs b/src/Api/Billing/Controllers/ProviderBillingController.cs index 78e361e8b3..c1908c253a 100644 --- a/src/Api/Billing/Controllers/ProviderBillingController.cs +++ b/src/Api/Billing/Controllers/ProviderBillingController.cs @@ -1,5 +1,6 @@ using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; +using Bit.Commercial.Core.Billing; using Bit.Core; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Models; @@ -148,13 +149,33 @@ public class ProviderBillingController( var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id); + var getProviderPriceFromStripe = featureService.IsEnabled(FeatureFlagKeys.PM21383_GetProviderPriceFromStripe); + var configuredProviderPlans = await Task.WhenAll(providerPlans.Select(async providerPlan => { var plan = await pricingClient.GetPlanOrThrow(providerPlan.PlanType); + + decimal unitAmount; + + if (getProviderPriceFromStripe) + { + var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, plan.Type); + var price = await stripeAdapter.PriceGetAsync(priceId); + + unitAmount = price.UnitAmountDecimal.HasValue + ? price.UnitAmountDecimal.Value / 100M + : plan.PasswordManager.ProviderPortalSeatPrice; + } + else + { + unitAmount = plan.PasswordManager.ProviderPortalSeatPrice; + } + return new ConfiguredProviderPlan( providerPlan.Id, providerPlan.ProviderId, plan, + unitAmount, providerPlan.SeatMinimum ?? 0, providerPlan.PurchasedSeats ?? 0, providerPlan.AllocatedSeats ?? 0); diff --git a/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs b/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs index a2c6827314..88ccf31452 100644 --- a/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs +++ b/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs @@ -35,7 +35,7 @@ public record ProviderSubscriptionResponse( .Select(providerPlan => { var plan = providerPlan.Plan; - var cost = (providerPlan.SeatMinimum + providerPlan.PurchasedSeats) * plan.PasswordManager.ProviderPortalSeatPrice; + var cost = (providerPlan.SeatMinimum + providerPlan.PurchasedSeats) * providerPlan.Price; var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence; return new ProviderPlanResponse( plan.Name, diff --git a/src/Core/Billing/Models/ConfiguredProviderPlan.cs b/src/Core/Billing/Models/ConfiguredProviderPlan.cs index 72c1ec5b07..77c93773e4 100644 --- a/src/Core/Billing/Models/ConfiguredProviderPlan.cs +++ b/src/Core/Billing/Models/ConfiguredProviderPlan.cs @@ -6,6 +6,7 @@ public record ConfiguredProviderPlan( Guid Id, Guid ProviderId, Plan Plan, + decimal Price, int SeatMinimum, int PurchasedSeats, int AssignedSeats); diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 694521c14e..1c31ffaab4 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -150,6 +150,7 @@ public static class FeatureFlagKeys public const string UseOrganizationWarningsService = "use-organization-warnings-service"; public const string PM20322_AllowTrialLength0 = "pm-20322-allow-trial-length-0"; public const string PM21092_SetNonUSBusinessUseToReverseCharge = "pm-21092-set-non-us-business-use-to-reverse-charge"; + public const string PM21383_GetProviderPriceFromStripe = "pm-21383-get-provider-price-from-stripe"; /* Data Insights and Reporting Team */ public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application"; diff --git a/src/Core/Services/IStripeAdapter.cs b/src/Core/Services/IStripeAdapter.cs index cb95732a6e..1ba93da4fa 100644 --- a/src/Core/Services/IStripeAdapter.cs +++ b/src/Core/Services/IStripeAdapter.cs @@ -57,4 +57,5 @@ public interface IStripeAdapter Task SetupIntentGet(string id, SetupIntentGetOptions options = null); Task SetupIntentVerifyMicroDeposit(string id, SetupIntentVerifyMicrodepositsOptions options); Task> TestClockListAsync(); + Task PriceGetAsync(string id, PriceGetOptions options = null); } diff --git a/src/Core/Services/Implementations/StripeAdapter.cs b/src/Core/Services/Implementations/StripeAdapter.cs index f7f4fea066..fd9f212ee7 100644 --- a/src/Core/Services/Implementations/StripeAdapter.cs +++ b/src/Core/Services/Implementations/StripeAdapter.cs @@ -283,4 +283,7 @@ public class StripeAdapter : IStripeAdapter } return items; } + + public Task PriceGetAsync(string id, PriceGetOptions options = null) + => _priceService.GetAsync(id, options); } diff --git a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs index 36990c7f9a..7933c79b6c 100644 --- a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs @@ -1,6 +1,8 @@ using Bit.Api.Billing.Controllers; using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; +using Bit.Commercial.Core.Billing; +using Bit.Core; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; @@ -285,6 +287,19 @@ public class ProviderBillingControllerTests Discount = new Discount { Coupon = new Coupon { PercentOff = 10 } }, TaxIds = new StripeList { Data = [new TaxId { Value = "123456789" }] } }, + Items = new StripeList + { + Data = [ + new SubscriptionItem + { + Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Enterprise } + }, + new SubscriptionItem + { + Price = new Price { Id = ProviderPriceAdapter.MSP.Active.Teams } + } + ] + }, Status = "unpaid", }; @@ -330,11 +345,21 @@ public class ProviderBillingControllerTests } }; + sutProvider.GetDependency().IsEnabled(FeatureFlagKeys.PM21383_GetProviderPriceFromStripe) + .Returns(true); + sutProvider.GetDependency().GetByProviderId(provider.Id).Returns(providerPlans); foreach (var providerPlan in providerPlans) { - sutProvider.GetDependency().GetPlanOrThrow(providerPlan.PlanType).Returns(StaticStore.GetPlan(providerPlan.PlanType)); + var plan = StaticStore.GetPlan(providerPlan.PlanType); + sutProvider.GetDependency().GetPlanOrThrow(providerPlan.PlanType).Returns(plan); + var priceId = ProviderPriceAdapter.GetPriceId(provider, subscription, providerPlan.PlanType); + sutProvider.GetDependency().PriceGetAsync(priceId) + .Returns(new Price + { + UnitAmountDecimal = plan.PasswordManager.ProviderPortalSeatPrice * 100 + }); } var result = await sutProvider.Sut.GetSubscriptionAsync(provider.Id); From 18d146406c31a3452f8b94ecf51525e7dfb71437 Mon Sep 17 00:00:00 2001 From: Alex Morask <144709477+amorask-bitwarden@users.noreply.github.com> Date: Wed, 21 May 2025 09:04:30 -0400 Subject: [PATCH 083/114] [PM-21099] (NO LOGIC) Organize Billing provider code (#5819) * [NO LOGIC] Organize Billing provider code * Run dotnet format * Run dotnet format' * Fixed using after merge * Fixed test usings after merge --- .../AdminConsole/Providers/CreateProviderCommand.cs | 4 ++-- .../RemoveOrganizationFromProviderCommand.cs | 1 + .../AdminConsole/Services/ProviderService.cs | 2 +- .../Models/ProviderClientInvoiceReportRow.cs | 4 ++-- .../Services}/BusinessUnitConverter.cs | 7 ++++--- .../Services}/ProviderBillingService.cs | 12 ++++++------ .../{ => Providers/Services}/ProviderPriceAdapter.cs | 2 +- .../Utilities/ServiceCollectionExtensions.cs | 4 ++-- .../RemoveOrganizationFromProviderCommandTests.cs | 1 + .../AdminConsole/Services/ProviderServiceTests.cs | 2 +- .../{ => Providers}/BusinessUnitConverterTests.cs | 8 ++++---- .../{ => Providers}/ProviderBillingServiceTests.cs | 12 ++++++------ .../{ => Providers}/ProviderPriceAdapterTests.cs | 4 ++-- .../Billing/{ => Tax}/TaxServiceTests.cs | 2 +- .../Controllers/OrganizationsController.cs | 2 +- .../AdminConsole/Controllers/ProvidersController.cs | 8 ++++---- src/Admin/AdminConsole/Models/ProviderEditModel.cs | 2 +- src/Admin/AdminConsole/Models/ProviderViewModel.cs | 2 +- .../Controllers/BusinessUnitConversionController.cs | 2 +- .../Controllers/MigrateProvidersController.cs | 4 ++-- src/Admin/Billing/Models/ProviderPlanViewModel.cs | 2 +- .../Billing/Views/MigrateProviders/Details.cshtml | 2 +- .../Billing/Views/MigrateProviders/Results.cshtml | 2 +- src/Admin/Startup.cs | 2 +- .../Controllers/OrganizationsController.cs | 2 +- .../Controllers/ProviderClientsController.cs | 2 +- .../Controllers/OrganizationBillingController.cs | 1 + .../Billing/Controllers/ProviderBillingController.cs | 7 ++++--- .../Models/Responses/ProviderSubscriptionResponse.cs | 1 + .../Services/Implementations/ProviderEventService.cs | 4 ++-- .../Entities/ClientOrganizationMigrationRecord.cs | 8 ++++---- .../{ => Providers}/Entities/ProviderInvoiceItem.cs | 8 ++++---- .../Billing/{ => Providers}/Entities/ProviderPlan.cs | 8 ++++---- .../Migration/Models/ClientMigrationTracker.cs | 2 +- .../Migration/Models/ProviderMigrationResult.cs | 4 ++-- .../Migration/Models/ProviderMigrationTracker.cs | 2 +- .../Migration/ServiceCollectionExtensions.cs | 6 +++--- .../Migration/Services/IMigrationTrackerCache.cs | 4 ++-- .../Migration/Services/IOrganizationMigrator.cs | 2 +- .../Migration/Services/IProviderMigrator.cs | 4 ++-- .../MigrationTrackerDistributedCache.cs | 4 ++-- .../Services/Implementations/OrganizationMigrator.cs | 8 ++++---- .../Services/Implementations/ProviderMigrator.cs | 12 ++++++------ .../{ => Providers}/Models/AddableOrganization.cs | 2 +- .../Models}/ChangeProviderPlansCommand.cs | 2 +- .../{ => Providers}/Models/ConfiguredProviderPlan.cs | 2 +- .../Models}/UpdateProviderSeatMinimumsCommand.cs | 2 +- .../IClientOrganizationMigrationRecordRepository.cs | 4 ++-- .../Repositories/IProviderInvoiceItemRepository.cs | 4 ++-- .../Repositories/IProviderPlanRepository.cs | 4 ++-- .../Services/IBusinessUnitConverter.cs | 2 +- .../Services/IProviderBillingService.cs | 6 +++--- .../Models}/AutomaticTaxFactoryParameters.cs | 2 +- .../Billing/Tax/Services/IAutomaticTaxFactory.cs | 2 +- .../Services/Implementations/AutomaticTaxFactory.cs | 2 +- .../Services/Implementations/StripePaymentService.cs | 2 +- .../ClientOrganizationMigrationRecordRepository.cs | 4 ++-- .../Repositories/ProviderInvoiceItemRepository.cs | 4 ++-- .../Billing/Repositories/ProviderPlanRepository.cs | 4 ++-- .../DapperServiceCollectionExtensions.cs | 1 + .../Models/ClientOrganizationMigrationRecord.cs | 4 ++-- .../Billing/Models/ProviderInvoiceItem.cs | 4 ++-- .../Billing/Models/ProviderPlan.cs | 4 ++-- .../ClientOrganizationMigrationRecordRepository.cs | 4 ++-- .../Repositories/ProviderInvoiceItemRepository.cs | 4 ++-- .../Billing/Repositories/ProviderPlanRepository.cs | 4 ++-- .../EntityFrameworkServiceCollectionExtensions.cs | 1 + .../Controllers/OrganizationsControllerTests.cs | 2 +- .../Controllers/OrganizationsControllerTests.cs | 2 +- .../Controllers/ProviderClientsControllerTests.cs | 2 +- .../Controllers/ProviderBillingControllerTests.cs | 7 ++++--- .../Services/ProviderEventServiceTests.cs | 4 ++-- .../Billing/Tax/Services/AutomaticTaxFactoryTests.cs | 2 +- test/Core.Test/Services/StripePaymentServiceTests.cs | 2 +- 74 files changed, 143 insertions(+), 134 deletions(-) rename bitwarden_license/src/Commercial.Core/Billing/{ => Providers}/Models/ProviderClientInvoiceReportRow.cs (91%) rename bitwarden_license/src/Commercial.Core/Billing/{ => Providers/Services}/BusinessUnitConverter.cs (98%) rename bitwarden_license/src/Commercial.Core/Billing/{ => Providers/Services}/ProviderBillingService.cs (99%) rename bitwarden_license/src/Commercial.Core/Billing/{ => Providers/Services}/ProviderPriceAdapter.cs (99%) rename bitwarden_license/test/Commercial.Core.Test/Billing/{ => Providers}/BusinessUnitConverterTests.cs (98%) rename bitwarden_license/test/Commercial.Core.Test/Billing/{ => Providers}/ProviderBillingServiceTests.cs (99%) rename bitwarden_license/test/Commercial.Core.Test/Billing/{ => Providers}/ProviderPriceAdapterTests.cs (97%) rename bitwarden_license/test/Commercial.Core.Test/Billing/{ => Tax}/TaxServiceTests.cs (99%) rename src/Core/Billing/{ => Providers}/Entities/ClientOrganizationMigrationRecord.cs (88%) rename src/Core/Billing/{ => Providers}/Entities/ProviderInvoiceItem.cs (87%) rename src/Core/Billing/{ => Providers}/Entities/ProviderPlan.cs (86%) rename src/Core/Billing/{ => Providers}/Migration/Models/ClientMigrationTracker.cs (90%) rename src/Core/Billing/{ => Providers}/Migration/Models/ProviderMigrationResult.cs (93%) rename src/Core/Billing/{ => Providers}/Migration/Models/ProviderMigrationTracker.cs (90%) rename src/Core/Billing/{ => Providers}/Migration/ServiceCollectionExtensions.cs (71%) rename src/Core/Billing/{ => Providers}/Migration/Services/IMigrationTrackerCache.cs (85%) rename src/Core/Billing/{ => Providers}/Migration/Services/IOrganizationMigrator.cs (72%) rename src/Core/Billing/{ => Providers}/Migration/Services/IProviderMigrator.cs (55%) rename src/Core/Billing/{ => Providers}/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs (96%) rename src/Core/Billing/{ => Providers}/Migration/Services/Implementations/OrganizationMigrator.cs (98%) rename src/Core/Billing/{ => Providers}/Migration/Services/Implementations/ProviderMigrator.cs (98%) rename src/Core/Billing/{ => Providers}/Models/AddableOrganization.cs (72%) rename src/Core/Billing/{Services/Contracts => Providers/Models}/ChangeProviderPlansCommand.cs (80%) rename src/Core/Billing/{ => Providers}/Models/ConfiguredProviderPlan.cs (82%) rename src/Core/Billing/{Services/Contracts => Providers/Models}/UpdateProviderSeatMinimumsCommand.cs (89%) rename src/Core/Billing/{ => Providers}/Repositories/IClientOrganizationMigrationRecordRepository.cs (77%) rename src/Core/Billing/{ => Providers}/Repositories/IProviderInvoiceItemRepository.cs (74%) rename src/Core/Billing/{ => Providers}/Repositories/IProviderPlanRepository.cs (64%) rename src/Core/Billing/{ => Providers}/Services/IBusinessUnitConverter.cs (98%) rename src/Core/Billing/{ => Providers}/Services/IProviderBillingService.cs (98%) rename src/Core/Billing/{Services/Contracts => Tax/Models}/AutomaticTaxFactoryParameters.cs (93%) diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/CreateProviderCommand.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/CreateProviderCommand.cs index 36a5f2c0a9..8dee75c7c2 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/CreateProviderCommand.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/CreateProviderCommand.cs @@ -3,9 +3,9 @@ using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; -using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs index 35a00f4253..4af0e12e64 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Providers/RemoveOrganizationFromProviderCommand.cs @@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Pricing; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Billing.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; diff --git a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs index 2925021d65..ad2d2d2aa1 100644 --- a/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs +++ b/bitwarden_license/src/Commercial.Core/AdminConsole/Services/ProviderService.cs @@ -11,7 +11,7 @@ using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; diff --git a/bitwarden_license/src/Commercial.Core/Billing/Models/ProviderClientInvoiceReportRow.cs b/bitwarden_license/src/Commercial.Core/Billing/Providers/Models/ProviderClientInvoiceReportRow.cs similarity index 91% rename from bitwarden_license/src/Commercial.Core/Billing/Models/ProviderClientInvoiceReportRow.cs rename to bitwarden_license/src/Commercial.Core/Billing/Providers/Models/ProviderClientInvoiceReportRow.cs index c78e213c34..eea40577ad 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/Models/ProviderClientInvoiceReportRow.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/Providers/Models/ProviderClientInvoiceReportRow.cs @@ -1,8 +1,8 @@ using System.Globalization; -using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Providers.Entities; using CsvHelper.Configuration.Attributes; -namespace Bit.Commercial.Core.Billing.Models; +namespace Bit.Commercial.Core.Billing.Providers.Models; public class ProviderClientInvoiceReportRow { diff --git a/bitwarden_license/src/Commercial.Core/Billing/BusinessUnitConverter.cs b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/BusinessUnitConverter.cs similarity index 98% rename from bitwarden_license/src/Commercial.Core/Billing/BusinessUnitConverter.cs rename to bitwarden_license/src/Commercial.Core/Billing/Providers/Services/BusinessUnitConverter.cs index d27b45af4a..8f6eb07fe1 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/BusinessUnitConverter.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/BusinessUnitConverter.cs @@ -7,11 +7,12 @@ using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing; using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Billing.Services; using Bit.Core.Entities; using Bit.Core.Enums; @@ -24,7 +25,7 @@ using Microsoft.Extensions.Logging; using OneOf; using Stripe; -namespace Bit.Commercial.Core.Billing; +namespace Bit.Commercial.Core.Billing.Providers.Services; [RequireFeature(FeatureFlagKeys.PM18770_EnableOrganizationBusinessUnitConversion)] public class BusinessUnitConverter( diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs similarity index 99% rename from bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs rename to bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs index c8d6505183..8c90d778bc 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderBillingService.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderBillingService.cs @@ -1,5 +1,5 @@ using System.Globalization; -using Bit.Commercial.Core.Billing.Models; +using Bit.Commercial.Core.Billing.Providers.Models; using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; @@ -8,14 +8,15 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing; using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Models; +using Bit.Core.Billing.Providers.Repositories; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Billing.Services; -using Bit.Core.Billing.Services.Contracts; using Bit.Core.Billing.Tax.Models; using Bit.Core.Billing.Tax.Services; using Bit.Core.Enums; @@ -28,12 +29,11 @@ using Braintree; using CsvHelper; using Microsoft.Extensions.Logging; using Stripe; - using static Bit.Core.Billing.Utilities; using Customer = Stripe.Customer; using Subscription = Stripe.Subscription; -namespace Bit.Commercial.Core.Billing; +namespace Bit.Commercial.Core.Billing.Providers.Services; public class ProviderBillingService( IBraintreeGateway braintreeGateway, diff --git a/bitwarden_license/src/Commercial.Core/Billing/ProviderPriceAdapter.cs b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderPriceAdapter.cs similarity index 99% rename from bitwarden_license/src/Commercial.Core/Billing/ProviderPriceAdapter.cs rename to bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderPriceAdapter.cs index a9dbb6febf..8c55d31f2c 100644 --- a/bitwarden_license/src/Commercial.Core/Billing/ProviderPriceAdapter.cs +++ b/bitwarden_license/src/Commercial.Core/Billing/Providers/Services/ProviderPriceAdapter.cs @@ -6,7 +6,7 @@ using Bit.Core.Billing; using Bit.Core.Billing.Enums; using Stripe; -namespace Bit.Commercial.Core.Billing; +namespace Bit.Commercial.Core.Billing.Providers.Services; public static class ProviderPriceAdapter { diff --git a/bitwarden_license/src/Commercial.Core/Utilities/ServiceCollectionExtensions.cs b/bitwarden_license/src/Commercial.Core/Utilities/ServiceCollectionExtensions.cs index 7f8c82e2c9..34f49e0ccc 100644 --- a/bitwarden_license/src/Commercial.Core/Utilities/ServiceCollectionExtensions.cs +++ b/bitwarden_license/src/Commercial.Core/Utilities/ServiceCollectionExtensions.cs @@ -1,9 +1,9 @@ using Bit.Commercial.Core.AdminConsole.Providers; using Bit.Commercial.Core.AdminConsole.Services; -using Bit.Commercial.Core.Billing; +using Bit.Commercial.Core.Billing.Providers.Services; using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Services; -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Providers.Services; using Microsoft.Extensions.DependencyInjection; namespace Bit.Commercial.Core.Utilities; diff --git a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs index dd40d7d943..5be18116c0 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/ProviderFeatures/RemoveOrganizationFromProviderCommandTests.cs @@ -8,6 +8,7 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Billing.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; diff --git a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs index a07dc7b8f8..cb8a9e8c69 100644 --- a/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/AdminConsole/Services/ProviderServiceTests.cs @@ -11,7 +11,7 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/BusinessUnitConverterTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/BusinessUnitConverterTests.cs similarity index 98% rename from bitwarden_license/test/Commercial.Core.Test/Billing/BusinessUnitConverterTests.cs rename to bitwarden_license/test/Commercial.Core.Test/Billing/Providers/BusinessUnitConverterTests.cs index 5d2d0a2c7c..c27d990213 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/BusinessUnitConverterTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/BusinessUnitConverterTests.cs @@ -1,16 +1,16 @@ #nullable enable using System.Text; -using Bit.Commercial.Core.Billing; +using Bit.Commercial.Core.Billing.Providers.Services; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing; using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Billing.Services; using Bit.Core.Entities; using Bit.Core.Enums; @@ -25,7 +25,7 @@ using NSubstitute; using Stripe; using Xunit; -namespace Bit.Commercial.Core.Test.Billing; +namespace Bit.Commercial.Core.Test.Billing.Providers; public class BusinessUnitConverterTests { diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/ProviderBillingServiceTests.cs similarity index 99% rename from bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs rename to bitwarden_license/test/Commercial.Core.Test/Billing/Providers/ProviderBillingServiceTests.cs index 92094d026e..9af9a71cce 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderBillingServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/ProviderBillingServiceTests.cs @@ -1,7 +1,7 @@ using System.Globalization; using System.Net; -using Bit.Commercial.Core.Billing; -using Bit.Commercial.Core.Billing.Models; +using Bit.Commercial.Core.Billing.Providers.Models; +using Bit.Commercial.Core.Billing.Providers.Services; using Bit.Core; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; @@ -10,13 +10,13 @@ using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Caches; using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Models; +using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Billing.Services; -using Bit.Core.Billing.Services.Contracts; using Bit.Core.Billing.Tax.Services; using Bit.Core.Entities; using Bit.Core.Enums; @@ -40,7 +40,7 @@ using Customer = Stripe.Customer; using PaymentMethod = Stripe.PaymentMethod; using Subscription = Stripe.Subscription; -namespace Bit.Commercial.Core.Test.Billing; +namespace Bit.Commercial.Core.Test.Billing.Providers; [SutProviderCustomize] public class ProviderBillingServiceTests diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderPriceAdapterTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/ProviderPriceAdapterTests.cs similarity index 97% rename from bitwarden_license/test/Commercial.Core.Test/Billing/ProviderPriceAdapterTests.cs rename to bitwarden_license/test/Commercial.Core.Test/Billing/Providers/ProviderPriceAdapterTests.cs index 9ecb4b0511..3087d5761c 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/ProviderPriceAdapterTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/Providers/ProviderPriceAdapterTests.cs @@ -1,11 +1,11 @@ -using Bit.Commercial.Core.Billing; +using Bit.Commercial.Core.Billing.Providers.Services; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Billing.Enums; using Stripe; using Xunit; -namespace Bit.Commercial.Core.Test.Billing; +namespace Bit.Commercial.Core.Test.Billing.Providers; public class ProviderPriceAdapterTests { diff --git a/bitwarden_license/test/Commercial.Core.Test/Billing/TaxServiceTests.cs b/bitwarden_license/test/Commercial.Core.Test/Billing/Tax/TaxServiceTests.cs similarity index 99% rename from bitwarden_license/test/Commercial.Core.Test/Billing/TaxServiceTests.cs rename to bitwarden_license/test/Commercial.Core.Test/Billing/Tax/TaxServiceTests.cs index 0a20b34818..f3164a14e0 100644 --- a/bitwarden_license/test/Commercial.Core.Test/Billing/TaxServiceTests.cs +++ b/bitwarden_license/test/Commercial.Core.Test/Billing/Tax/TaxServiceTests.cs @@ -3,7 +3,7 @@ using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; -namespace Bit.Commercial.Core.Test.Billing; +namespace Bit.Commercial.Core.Test.Billing.Tax; [SutProviderCustomize] public class TaxServiceTests diff --git a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs index 6eb81b5956..8cd2222dbf 100644 --- a/src/Admin/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Admin/AdminConsole/Controllers/OrganizationsController.cs @@ -11,7 +11,7 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models.OrganizationConnectionConfigs; diff --git a/src/Admin/AdminConsole/Controllers/ProvidersController.cs b/src/Admin/AdminConsole/Controllers/ProvidersController.cs index dd4332358c..b4abf81ee2 100644 --- a/src/Admin/AdminConsole/Controllers/ProvidersController.cs +++ b/src/Admin/AdminConsole/Controllers/ProvidersController.cs @@ -10,13 +10,13 @@ using Bit.Core.AdminConsole.Providers.Interfaces; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Repositories; -using Bit.Core.Billing.Services; -using Bit.Core.Billing.Services.Contracts; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Models; +using Bit.Core.Billing.Providers.Repositories; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; diff --git a/src/Admin/AdminConsole/Models/ProviderEditModel.cs b/src/Admin/AdminConsole/Models/ProviderEditModel.cs index 44eebb8d7d..de9e25fa6f 100644 --- a/src/Admin/AdminConsole/Models/ProviderEditModel.cs +++ b/src/Admin/AdminConsole/Models/ProviderEditModel.cs @@ -2,8 +2,8 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; -using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Providers.Entities; using Bit.Core.Enums; using Bit.SharedWeb.Utilities; diff --git a/src/Admin/AdminConsole/Models/ProviderViewModel.cs b/src/Admin/AdminConsole/Models/ProviderViewModel.cs index bcb96df006..2d4ba5012c 100644 --- a/src/Admin/AdminConsole/Models/ProviderViewModel.cs +++ b/src/Admin/AdminConsole/Models/ProviderViewModel.cs @@ -2,8 +2,8 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Models.Data.Provider; -using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; +using Bit.Core.Billing.Providers.Entities; namespace Bit.Admin.AdminConsole.Models; diff --git a/src/Admin/Billing/Controllers/BusinessUnitConversionController.cs b/src/Admin/Billing/Controllers/BusinessUnitConversionController.cs index 2421710d41..be3a94949f 100644 --- a/src/Admin/Billing/Controllers/BusinessUnitConversionController.cs +++ b/src/Admin/Billing/Controllers/BusinessUnitConversionController.cs @@ -7,7 +7,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Exceptions; using Bit.Core.Repositories; using Bit.Core.Utilities; diff --git a/src/Admin/Billing/Controllers/MigrateProvidersController.cs b/src/Admin/Billing/Controllers/MigrateProvidersController.cs index d4ef105e34..ef5ea2312e 100644 --- a/src/Admin/Billing/Controllers/MigrateProvidersController.cs +++ b/src/Admin/Billing/Controllers/MigrateProvidersController.cs @@ -1,8 +1,8 @@ using Bit.Admin.Billing.Models; using Bit.Admin.Enums; using Bit.Admin.Utilities; -using Bit.Core.Billing.Migration.Models; -using Bit.Core.Billing.Migration.Services; +using Bit.Core.Billing.Providers.Migration.Models; +using Bit.Core.Billing.Providers.Migration.Services; using Bit.Core.Utilities; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; diff --git a/src/Admin/Billing/Models/ProviderPlanViewModel.cs b/src/Admin/Billing/Models/ProviderPlanViewModel.cs index 7a50aba286..391c24d6df 100644 --- a/src/Admin/Billing/Models/ProviderPlanViewModel.cs +++ b/src/Admin/Billing/Models/ProviderPlanViewModel.cs @@ -1,4 +1,4 @@ -using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Providers.Entities; namespace Bit.Admin.Billing.Models; diff --git a/src/Admin/Billing/Views/MigrateProviders/Details.cshtml b/src/Admin/Billing/Views/MigrateProviders/Details.cshtml index 303e6d2e45..6ee0344057 100644 --- a/src/Admin/Billing/Views/MigrateProviders/Details.cshtml +++ b/src/Admin/Billing/Views/MigrateProviders/Details.cshtml @@ -1,5 +1,5 @@ @using System.Text.Json -@model Bit.Core.Billing.Migration.Models.ProviderMigrationResult +@model Bit.Core.Billing.Providers.Migration.Models.ProviderMigrationResult @{ ViewData["Title"] = "Results"; } diff --git a/src/Admin/Billing/Views/MigrateProviders/Results.cshtml b/src/Admin/Billing/Views/MigrateProviders/Results.cshtml index 45611de80e..94db08db3d 100644 --- a/src/Admin/Billing/Views/MigrateProviders/Results.cshtml +++ b/src/Admin/Billing/Views/MigrateProviders/Results.cshtml @@ -1,4 +1,4 @@ -@model Bit.Core.Billing.Migration.Models.ProviderMigrationResult[] +@model Bit.Core.Billing.Providers.Migration.Models.ProviderMigrationResult[] @{ ViewData["Title"] = "Results"; } diff --git a/src/Admin/Startup.cs b/src/Admin/Startup.cs index 11f9e7ce68..5b34e13f6c 100644 --- a/src/Admin/Startup.cs +++ b/src/Admin/Startup.cs @@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.Extensions.DependencyInjection.Extensions; using Bit.Admin.Services; using Bit.Core.Billing.Extensions; -using Bit.Core.Billing.Migration; +using Bit.Core.Billing.Providers.Migration; #if !OSS using Bit.Commercial.Core.Utilities; diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs index f402c927e0..0d498beab1 100644 --- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs +++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs @@ -25,7 +25,7 @@ using Bit.Core.Auth.Services; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; diff --git a/src/Api/AdminConsole/Controllers/ProviderClientsController.cs b/src/Api/AdminConsole/Controllers/ProviderClientsController.cs index 74d2feff3c..f226ba316e 100644 --- a/src/Api/AdminConsole/Controllers/ProviderClientsController.cs +++ b/src/Api/AdminConsole/Controllers/ProviderClientsController.cs @@ -2,7 +2,7 @@ using Bit.Api.Billing.Models.Requests; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Models.Business; diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index 1ae1f2e655..9e57545098 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -8,6 +8,7 @@ using Bit.Core; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Sales; using Bit.Core.Billing.Pricing; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Billing.Services; using Bit.Core.Billing.Tax.Models; using Bit.Core.Context; diff --git a/src/Api/Billing/Controllers/ProviderBillingController.cs b/src/Api/Billing/Controllers/ProviderBillingController.cs index c1908c253a..37130d54ce 100644 --- a/src/Api/Billing/Controllers/ProviderBillingController.cs +++ b/src/Api/Billing/Controllers/ProviderBillingController.cs @@ -1,11 +1,12 @@ using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; -using Bit.Commercial.Core.Billing; +using Bit.Commercial.Core.Billing.Providers.Services; using Bit.Core; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Billing.Models; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Models; +using Bit.Core.Billing.Providers.Repositories; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Billing.Services; using Bit.Core.Billing.Tax.Models; using Bit.Core.Context; diff --git a/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs b/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs index 88ccf31452..e5b868af9a 100644 --- a/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs +++ b/src/Api/Billing/Models/Responses/ProviderSubscriptionResponse.cs @@ -2,6 +2,7 @@ using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models; +using Bit.Core.Billing.Providers.Models; using Bit.Core.Billing.Tax.Models; using Stripe; diff --git a/src/Billing/Services/Implementations/ProviderEventService.cs b/src/Billing/Services/Implementations/ProviderEventService.cs index 4e35a6c894..1f6ef741df 100644 --- a/src/Billing/Services/Implementations/ProviderEventService.cs +++ b/src/Billing/Services/Implementations/ProviderEventService.cs @@ -1,8 +1,8 @@ using Bit.Billing.Constants; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Billing.Entities; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Enums; using Bit.Core.Repositories; using Stripe; diff --git a/src/Core/Billing/Entities/ClientOrganizationMigrationRecord.cs b/src/Core/Billing/Providers/Entities/ClientOrganizationMigrationRecord.cs similarity index 88% rename from src/Core/Billing/Entities/ClientOrganizationMigrationRecord.cs rename to src/Core/Billing/Providers/Entities/ClientOrganizationMigrationRecord.cs index 1e719b3ceb..bbb0a90b04 100644 --- a/src/Core/Billing/Entities/ClientOrganizationMigrationRecord.cs +++ b/src/Core/Billing/Providers/Entities/ClientOrganizationMigrationRecord.cs @@ -1,12 +1,12 @@ -using System.ComponentModel.DataAnnotations; +#nullable enable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Utilities; -#nullable enable - -namespace Bit.Core.Billing.Entities; +namespace Bit.Core.Billing.Providers.Entities; public class ClientOrganizationMigrationRecord : ITableObject { diff --git a/src/Core/Billing/Entities/ProviderInvoiceItem.cs b/src/Core/Billing/Providers/Entities/ProviderInvoiceItem.cs similarity index 87% rename from src/Core/Billing/Entities/ProviderInvoiceItem.cs rename to src/Core/Billing/Providers/Entities/ProviderInvoiceItem.cs index 566d7514e7..9d9eeda754 100644 --- a/src/Core/Billing/Entities/ProviderInvoiceItem.cs +++ b/src/Core/Billing/Providers/Entities/ProviderInvoiceItem.cs @@ -1,10 +1,10 @@ -using System.ComponentModel.DataAnnotations; +#nullable enable + +using System.ComponentModel.DataAnnotations; using Bit.Core.Entities; using Bit.Core.Utilities; -#nullable enable - -namespace Bit.Core.Billing.Entities; +namespace Bit.Core.Billing.Providers.Entities; public class ProviderInvoiceItem : ITableObject { diff --git a/src/Core/Billing/Entities/ProviderPlan.cs b/src/Core/Billing/Providers/Entities/ProviderPlan.cs similarity index 86% rename from src/Core/Billing/Entities/ProviderPlan.cs rename to src/Core/Billing/Providers/Entities/ProviderPlan.cs index fd131f64e6..d06c81e9ce 100644 --- a/src/Core/Billing/Entities/ProviderPlan.cs +++ b/src/Core/Billing/Providers/Entities/ProviderPlan.cs @@ -1,10 +1,10 @@ -using Bit.Core.Billing.Enums; +#nullable enable + +using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Utilities; -#nullable enable - -namespace Bit.Core.Billing.Entities; +namespace Bit.Core.Billing.Providers.Entities; public class ProviderPlan : ITableObject { diff --git a/src/Core/Billing/Migration/Models/ClientMigrationTracker.cs b/src/Core/Billing/Providers/Migration/Models/ClientMigrationTracker.cs similarity index 90% rename from src/Core/Billing/Migration/Models/ClientMigrationTracker.cs rename to src/Core/Billing/Providers/Migration/Models/ClientMigrationTracker.cs index 69398004fd..ae0c28de86 100644 --- a/src/Core/Billing/Migration/Models/ClientMigrationTracker.cs +++ b/src/Core/Billing/Providers/Migration/Models/ClientMigrationTracker.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Billing.Migration.Models; +namespace Bit.Core.Billing.Providers.Migration.Models; public enum ClientMigrationProgress { diff --git a/src/Core/Billing/Migration/Models/ProviderMigrationResult.cs b/src/Core/Billing/Providers/Migration/Models/ProviderMigrationResult.cs similarity index 93% rename from src/Core/Billing/Migration/Models/ProviderMigrationResult.cs rename to src/Core/Billing/Providers/Migration/Models/ProviderMigrationResult.cs index 137ba8bd0d..6f3c3be11d 100644 --- a/src/Core/Billing/Migration/Models/ProviderMigrationResult.cs +++ b/src/Core/Billing/Providers/Migration/Models/ProviderMigrationResult.cs @@ -1,6 +1,6 @@ -using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Providers.Entities; -namespace Bit.Core.Billing.Migration.Models; +namespace Bit.Core.Billing.Providers.Migration.Models; public class ProviderMigrationResult { diff --git a/src/Core/Billing/Migration/Models/ProviderMigrationTracker.cs b/src/Core/Billing/Providers/Migration/Models/ProviderMigrationTracker.cs similarity index 90% rename from src/Core/Billing/Migration/Models/ProviderMigrationTracker.cs rename to src/Core/Billing/Providers/Migration/Models/ProviderMigrationTracker.cs index 7bfef8a931..f4708d4cbd 100644 --- a/src/Core/Billing/Migration/Models/ProviderMigrationTracker.cs +++ b/src/Core/Billing/Providers/Migration/Models/ProviderMigrationTracker.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Billing.Migration.Models; +namespace Bit.Core.Billing.Providers.Migration.Models; public enum ProviderMigrationProgress { diff --git a/src/Core/Billing/Migration/ServiceCollectionExtensions.cs b/src/Core/Billing/Providers/Migration/ServiceCollectionExtensions.cs similarity index 71% rename from src/Core/Billing/Migration/ServiceCollectionExtensions.cs rename to src/Core/Billing/Providers/Migration/ServiceCollectionExtensions.cs index 109259d59a..1061c82888 100644 --- a/src/Core/Billing/Migration/ServiceCollectionExtensions.cs +++ b/src/Core/Billing/Providers/Migration/ServiceCollectionExtensions.cs @@ -1,8 +1,8 @@ -using Bit.Core.Billing.Migration.Services; -using Bit.Core.Billing.Migration.Services.Implementations; +using Bit.Core.Billing.Providers.Migration.Services; +using Bit.Core.Billing.Providers.Migration.Services.Implementations; using Microsoft.Extensions.DependencyInjection; -namespace Bit.Core.Billing.Migration; +namespace Bit.Core.Billing.Providers.Migration; public static class ServiceCollectionExtensions { diff --git a/src/Core/Billing/Migration/Services/IMigrationTrackerCache.cs b/src/Core/Billing/Providers/Migration/Services/IMigrationTrackerCache.cs similarity index 85% rename from src/Core/Billing/Migration/Services/IMigrationTrackerCache.cs rename to src/Core/Billing/Providers/Migration/Services/IMigrationTrackerCache.cs index 6734c69566..70649590df 100644 --- a/src/Core/Billing/Migration/Services/IMigrationTrackerCache.cs +++ b/src/Core/Billing/Providers/Migration/Services/IMigrationTrackerCache.cs @@ -1,8 +1,8 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; -using Bit.Core.Billing.Migration.Models; +using Bit.Core.Billing.Providers.Migration.Models; -namespace Bit.Core.Billing.Migration.Services; +namespace Bit.Core.Billing.Providers.Migration.Services; public interface IMigrationTrackerCache { diff --git a/src/Core/Billing/Migration/Services/IOrganizationMigrator.cs b/src/Core/Billing/Providers/Migration/Services/IOrganizationMigrator.cs similarity index 72% rename from src/Core/Billing/Migration/Services/IOrganizationMigrator.cs rename to src/Core/Billing/Providers/Migration/Services/IOrganizationMigrator.cs index 7bc9443717..a0548277b4 100644 --- a/src/Core/Billing/Migration/Services/IOrganizationMigrator.cs +++ b/src/Core/Billing/Providers/Migration/Services/IOrganizationMigrator.cs @@ -1,6 +1,6 @@ using Bit.Core.AdminConsole.Entities; -namespace Bit.Core.Billing.Migration.Services; +namespace Bit.Core.Billing.Providers.Migration.Services; public interface IOrganizationMigrator { diff --git a/src/Core/Billing/Migration/Services/IProviderMigrator.cs b/src/Core/Billing/Providers/Migration/Services/IProviderMigrator.cs similarity index 55% rename from src/Core/Billing/Migration/Services/IProviderMigrator.cs rename to src/Core/Billing/Providers/Migration/Services/IProviderMigrator.cs index 9ca14e7fd9..328c2419f4 100644 --- a/src/Core/Billing/Migration/Services/IProviderMigrator.cs +++ b/src/Core/Billing/Providers/Migration/Services/IProviderMigrator.cs @@ -1,6 +1,6 @@ -using Bit.Core.Billing.Migration.Models; +using Bit.Core.Billing.Providers.Migration.Models; -namespace Bit.Core.Billing.Migration.Services; +namespace Bit.Core.Billing.Providers.Migration.Services; public interface IProviderMigrator { diff --git a/src/Core/Billing/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs b/src/Core/Billing/Providers/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs similarity index 96% rename from src/Core/Billing/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs rename to src/Core/Billing/Providers/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs index 920bc55392..ea7d118cfa 100644 --- a/src/Core/Billing/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs +++ b/src/Core/Billing/Providers/Migration/Services/Implementations/MigrationTrackerDistributedCache.cs @@ -1,11 +1,11 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; -using Bit.Core.Billing.Migration.Models; +using Bit.Core.Billing.Providers.Migration.Models; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.DependencyInjection; -namespace Bit.Core.Billing.Migration.Services.Implementations; +namespace Bit.Core.Billing.Providers.Migration.Services.Implementations; public class MigrationTrackerDistributedCache( [FromKeyedServices("persistent")] diff --git a/src/Core/Billing/Migration/Services/Implementations/OrganizationMigrator.cs b/src/Core/Billing/Providers/Migration/Services/Implementations/OrganizationMigrator.cs similarity index 98% rename from src/Core/Billing/Migration/Services/Implementations/OrganizationMigrator.cs rename to src/Core/Billing/Providers/Migration/Services/Implementations/OrganizationMigrator.cs index 204022380d..3b874579e5 100644 --- a/src/Core/Billing/Migration/Services/Implementations/OrganizationMigrator.cs +++ b/src/Core/Billing/Providers/Migration/Services/Implementations/OrganizationMigrator.cs @@ -1,10 +1,10 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Migration.Models; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Migration.Models; +using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Services; @@ -12,7 +12,7 @@ using Microsoft.Extensions.Logging; using Stripe; using Plan = Bit.Core.Models.StaticStore.Plan; -namespace Bit.Core.Billing.Migration.Services.Implementations; +namespace Bit.Core.Billing.Providers.Migration.Services.Implementations; public class OrganizationMigrator( IClientOrganizationMigrationRecordRepository clientOrganizationMigrationRecordRepository, diff --git a/src/Core/Billing/Migration/Services/Implementations/ProviderMigrator.cs b/src/Core/Billing/Providers/Migration/Services/Implementations/ProviderMigrator.cs similarity index 98% rename from src/Core/Billing/Migration/Services/Implementations/ProviderMigrator.cs rename to src/Core/Billing/Providers/Migration/Services/Implementations/ProviderMigrator.cs index 384cfca1d1..3a0b579dcf 100644 --- a/src/Core/Billing/Migration/Services/Implementations/ProviderMigrator.cs +++ b/src/Core/Billing/Providers/Migration/Services/Implementations/ProviderMigrator.cs @@ -3,18 +3,18 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Migration.Models; -using Bit.Core.Billing.Repositories; -using Bit.Core.Billing.Services; -using Bit.Core.Billing.Services.Contracts; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Migration.Models; +using Bit.Core.Billing.Providers.Models; +using Bit.Core.Billing.Providers.Repositories; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Repositories; using Bit.Core.Services; using Microsoft.Extensions.Logging; using Stripe; -namespace Bit.Core.Billing.Migration.Services.Implementations; +namespace Bit.Core.Billing.Providers.Migration.Services.Implementations; public class ProviderMigrator( IClientOrganizationMigrationRecordRepository clientOrganizationMigrationRecordRepository, diff --git a/src/Core/Billing/Models/AddableOrganization.cs b/src/Core/Billing/Providers/Models/AddableOrganization.cs similarity index 72% rename from src/Core/Billing/Models/AddableOrganization.cs rename to src/Core/Billing/Providers/Models/AddableOrganization.cs index fe6d5458bd..aca7a158b0 100644 --- a/src/Core/Billing/Models/AddableOrganization.cs +++ b/src/Core/Billing/Providers/Models/AddableOrganization.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Billing.Models; +namespace Bit.Core.Billing.Providers.Models; public record AddableOrganization( Guid Id, diff --git a/src/Core/Billing/Services/Contracts/ChangeProviderPlansCommand.cs b/src/Core/Billing/Providers/Models/ChangeProviderPlansCommand.cs similarity index 80% rename from src/Core/Billing/Services/Contracts/ChangeProviderPlansCommand.cs rename to src/Core/Billing/Providers/Models/ChangeProviderPlansCommand.cs index 385782c8ad..053d912291 100644 --- a/src/Core/Billing/Services/Contracts/ChangeProviderPlansCommand.cs +++ b/src/Core/Billing/Providers/Models/ChangeProviderPlansCommand.cs @@ -1,7 +1,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Billing.Enums; -namespace Bit.Core.Billing.Services.Contracts; +namespace Bit.Core.Billing.Providers.Models; public record ChangeProviderPlanCommand( Provider Provider, diff --git a/src/Core/Billing/Models/ConfiguredProviderPlan.cs b/src/Core/Billing/Providers/Models/ConfiguredProviderPlan.cs similarity index 82% rename from src/Core/Billing/Models/ConfiguredProviderPlan.cs rename to src/Core/Billing/Providers/Models/ConfiguredProviderPlan.cs index 77c93773e4..d875106a9e 100644 --- a/src/Core/Billing/Models/ConfiguredProviderPlan.cs +++ b/src/Core/Billing/Providers/Models/ConfiguredProviderPlan.cs @@ -1,6 +1,6 @@ using Bit.Core.Models.StaticStore; -namespace Bit.Core.Billing.Models; +namespace Bit.Core.Billing.Providers.Models; public record ConfiguredProviderPlan( Guid Id, diff --git a/src/Core/Billing/Services/Contracts/UpdateProviderSeatMinimumsCommand.cs b/src/Core/Billing/Providers/Models/UpdateProviderSeatMinimumsCommand.cs similarity index 89% rename from src/Core/Billing/Services/Contracts/UpdateProviderSeatMinimumsCommand.cs rename to src/Core/Billing/Providers/Models/UpdateProviderSeatMinimumsCommand.cs index 2d2535b60a..dfd04e6605 100644 --- a/src/Core/Billing/Services/Contracts/UpdateProviderSeatMinimumsCommand.cs +++ b/src/Core/Billing/Providers/Models/UpdateProviderSeatMinimumsCommand.cs @@ -1,7 +1,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.Billing.Enums; -namespace Bit.Core.Billing.Services.Contracts; +namespace Bit.Core.Billing.Providers.Models; /// The provider to update the seat minimums for. /// The new seat minimums for the provider. diff --git a/src/Core/Billing/Repositories/IClientOrganizationMigrationRecordRepository.cs b/src/Core/Billing/Providers/Repositories/IClientOrganizationMigrationRecordRepository.cs similarity index 77% rename from src/Core/Billing/Repositories/IClientOrganizationMigrationRecordRepository.cs rename to src/Core/Billing/Providers/Repositories/IClientOrganizationMigrationRecordRepository.cs index 2165984383..53eb51403f 100644 --- a/src/Core/Billing/Repositories/IClientOrganizationMigrationRecordRepository.cs +++ b/src/Core/Billing/Providers/Repositories/IClientOrganizationMigrationRecordRepository.cs @@ -1,7 +1,7 @@ -using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Providers.Entities; using Bit.Core.Repositories; -namespace Bit.Core.Billing.Repositories; +namespace Bit.Core.Billing.Providers.Repositories; public interface IClientOrganizationMigrationRecordRepository : IRepository { diff --git a/src/Core/Billing/Repositories/IProviderInvoiceItemRepository.cs b/src/Core/Billing/Providers/Repositories/IProviderInvoiceItemRepository.cs similarity index 74% rename from src/Core/Billing/Repositories/IProviderInvoiceItemRepository.cs rename to src/Core/Billing/Providers/Repositories/IProviderInvoiceItemRepository.cs index a722d4cf9d..931d8a9186 100644 --- a/src/Core/Billing/Repositories/IProviderInvoiceItemRepository.cs +++ b/src/Core/Billing/Providers/Repositories/IProviderInvoiceItemRepository.cs @@ -1,7 +1,7 @@ -using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Providers.Entities; using Bit.Core.Repositories; -namespace Bit.Core.Billing.Repositories; +namespace Bit.Core.Billing.Providers.Repositories; public interface IProviderInvoiceItemRepository : IRepository { diff --git a/src/Core/Billing/Repositories/IProviderPlanRepository.cs b/src/Core/Billing/Providers/Repositories/IProviderPlanRepository.cs similarity index 64% rename from src/Core/Billing/Repositories/IProviderPlanRepository.cs rename to src/Core/Billing/Providers/Repositories/IProviderPlanRepository.cs index eccbad82bb..d1cf91ea56 100644 --- a/src/Core/Billing/Repositories/IProviderPlanRepository.cs +++ b/src/Core/Billing/Providers/Repositories/IProviderPlanRepository.cs @@ -1,7 +1,7 @@ -using Bit.Core.Billing.Entities; +using Bit.Core.Billing.Providers.Entities; using Bit.Core.Repositories; -namespace Bit.Core.Billing.Repositories; +namespace Bit.Core.Billing.Providers.Repositories; public interface IProviderPlanRepository : IRepository { diff --git a/src/Core/Billing/Services/IBusinessUnitConverter.cs b/src/Core/Billing/Providers/Services/IBusinessUnitConverter.cs similarity index 98% rename from src/Core/Billing/Services/IBusinessUnitConverter.cs rename to src/Core/Billing/Providers/Services/IBusinessUnitConverter.cs index 06ff883eae..99df6b1bef 100644 --- a/src/Core/Billing/Services/IBusinessUnitConverter.cs +++ b/src/Core/Billing/Providers/Services/IBusinessUnitConverter.cs @@ -3,7 +3,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using OneOf; -namespace Bit.Core.Billing.Services; +namespace Bit.Core.Billing.Providers.Services; public interface IBusinessUnitConverter { diff --git a/src/Core/Billing/Services/IProviderBillingService.cs b/src/Core/Billing/Providers/Services/IProviderBillingService.cs similarity index 98% rename from src/Core/Billing/Services/IProviderBillingService.cs rename to src/Core/Billing/Providers/Services/IProviderBillingService.cs index b6ddbdd642..b634f1a81c 100644 --- a/src/Core/Billing/Services/IProviderBillingService.cs +++ b/src/Core/Billing/Providers/Services/IProviderBillingService.cs @@ -1,14 +1,14 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities.Provider; -using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models; -using Bit.Core.Billing.Services.Contracts; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Models; using Bit.Core.Billing.Tax.Models; using Bit.Core.Models.Business; using Stripe; -namespace Bit.Core.Billing.Services; +namespace Bit.Core.Billing.Providers.Services; public interface IProviderBillingService { diff --git a/src/Core/Billing/Services/Contracts/AutomaticTaxFactoryParameters.cs b/src/Core/Billing/Tax/Models/AutomaticTaxFactoryParameters.cs similarity index 93% rename from src/Core/Billing/Services/Contracts/AutomaticTaxFactoryParameters.cs rename to src/Core/Billing/Tax/Models/AutomaticTaxFactoryParameters.cs index 19a4f0bdfa..a58daa9c48 100644 --- a/src/Core/Billing/Services/Contracts/AutomaticTaxFactoryParameters.cs +++ b/src/Core/Billing/Tax/Models/AutomaticTaxFactoryParameters.cs @@ -2,7 +2,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Entities; -namespace Bit.Core.Billing.Services.Contracts; +namespace Bit.Core.Billing.Tax.Models; public class AutomaticTaxFactoryParameters { diff --git a/src/Core/Billing/Tax/Services/IAutomaticTaxFactory.cs b/src/Core/Billing/Tax/Services/IAutomaticTaxFactory.cs index 90a3bc08ad..c0a31efb3c 100644 --- a/src/Core/Billing/Tax/Services/IAutomaticTaxFactory.cs +++ b/src/Core/Billing/Tax/Services/IAutomaticTaxFactory.cs @@ -1,4 +1,4 @@ -using Bit.Core.Billing.Services.Contracts; +using Bit.Core.Billing.Tax.Models; namespace Bit.Core.Billing.Tax.Services; diff --git a/src/Core/Billing/Tax/Services/Implementations/AutomaticTaxFactory.cs b/src/Core/Billing/Tax/Services/Implementations/AutomaticTaxFactory.cs index fa110f79d5..6086a16b79 100644 --- a/src/Core/Billing/Tax/Services/Implementations/AutomaticTaxFactory.cs +++ b/src/Core/Billing/Tax/Services/Implementations/AutomaticTaxFactory.cs @@ -1,7 +1,7 @@ #nullable enable using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services.Contracts; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Entities; using Bit.Core.Services; diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 34be6d59c5..23d06bed2b 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -7,7 +7,7 @@ using Bit.Core.Billing.Extensions; using Bit.Core.Billing.Models; using Bit.Core.Billing.Models.Business; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services.Contracts; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Billing.Tax.Requests; using Bit.Core.Billing.Tax.Responses; using Bit.Core.Billing.Tax.Services; diff --git a/src/Infrastructure.Dapper/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs b/src/Infrastructure.Dapper/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs index 155abdb4b4..e43eb9a71f 100644 --- a/src/Infrastructure.Dapper/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs +++ b/src/Infrastructure.Dapper/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs @@ -1,6 +1,6 @@ using System.Data; -using Bit.Core.Billing.Entities; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Settings; using Bit.Infrastructure.Dapper.Repositories; using Dapper; diff --git a/src/Infrastructure.Dapper/Billing/Repositories/ProviderInvoiceItemRepository.cs b/src/Infrastructure.Dapper/Billing/Repositories/ProviderInvoiceItemRepository.cs index 69a4be1ef8..cf5ac07ead 100644 --- a/src/Infrastructure.Dapper/Billing/Repositories/ProviderInvoiceItemRepository.cs +++ b/src/Infrastructure.Dapper/Billing/Repositories/ProviderInvoiceItemRepository.cs @@ -1,6 +1,6 @@ using System.Data; -using Bit.Core.Billing.Entities; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Settings; using Bit.Infrastructure.Dapper.Repositories; using Dapper; diff --git a/src/Infrastructure.Dapper/Billing/Repositories/ProviderPlanRepository.cs b/src/Infrastructure.Dapper/Billing/Repositories/ProviderPlanRepository.cs index f8448f4198..52977c9d3c 100644 --- a/src/Infrastructure.Dapper/Billing/Repositories/ProviderPlanRepository.cs +++ b/src/Infrastructure.Dapper/Billing/Repositories/ProviderPlanRepository.cs @@ -1,6 +1,6 @@ using System.Data; -using Bit.Core.Billing.Entities; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Settings; using Bit.Infrastructure.Dapper.Repositories; using Dapper; diff --git a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs index d48fe95096..ba374ae988 100644 --- a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs +++ b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Repositories; +using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Billing.Repositories; using Bit.Core.KeyManagement.Repositories; using Bit.Core.NotificationCenter.Repositories; diff --git a/src/Infrastructure.EntityFramework/Billing/Models/ClientOrganizationMigrationRecord.cs b/src/Infrastructure.EntityFramework/Billing/Models/ClientOrganizationMigrationRecord.cs index 4271df292a..6d77fd9ed9 100644 --- a/src/Infrastructure.EntityFramework/Billing/Models/ClientOrganizationMigrationRecord.cs +++ b/src/Infrastructure.EntityFramework/Billing/Models/ClientOrganizationMigrationRecord.cs @@ -2,7 +2,7 @@ namespace Bit.Infrastructure.EntityFramework.Billing.Models; -public class ClientOrganizationMigrationRecord : Core.Billing.Entities.ClientOrganizationMigrationRecord +public class ClientOrganizationMigrationRecord : Core.Billing.Providers.Entities.ClientOrganizationMigrationRecord { } @@ -11,6 +11,6 @@ public class ClientOrganizationMigrationRecordProfile : Profile { public ClientOrganizationMigrationRecordProfile() { - CreateMap().ReverseMap(); + CreateMap().ReverseMap(); } } diff --git a/src/Infrastructure.EntityFramework/Billing/Models/ProviderInvoiceItem.cs b/src/Infrastructure.EntityFramework/Billing/Models/ProviderInvoiceItem.cs index 1eea0bf9d2..1bea786f21 100644 --- a/src/Infrastructure.EntityFramework/Billing/Models/ProviderInvoiceItem.cs +++ b/src/Infrastructure.EntityFramework/Billing/Models/ProviderInvoiceItem.cs @@ -4,7 +4,7 @@ using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider; namespace Bit.Infrastructure.EntityFramework.Billing.Models; // ReSharper disable once ClassWithVirtualMembersNeverInherited.Global -public class ProviderInvoiceItem : Core.Billing.Entities.ProviderInvoiceItem +public class ProviderInvoiceItem : Core.Billing.Providers.Entities.ProviderInvoiceItem { public virtual Provider Provider { get; set; } } @@ -13,6 +13,6 @@ public class ProviderInvoiceItemMapperProfile : Profile { public ProviderInvoiceItemMapperProfile() { - CreateMap().ReverseMap(); + CreateMap().ReverseMap(); } } diff --git a/src/Infrastructure.EntityFramework/Billing/Models/ProviderPlan.cs b/src/Infrastructure.EntityFramework/Billing/Models/ProviderPlan.cs index 4dbbfe71d7..c9ba4c813e 100644 --- a/src/Infrastructure.EntityFramework/Billing/Models/ProviderPlan.cs +++ b/src/Infrastructure.EntityFramework/Billing/Models/ProviderPlan.cs @@ -4,7 +4,7 @@ using Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider; namespace Bit.Infrastructure.EntityFramework.Billing.Models; // ReSharper disable once ClassWithVirtualMembersNeverInherited.Global -public class ProviderPlan : Core.Billing.Entities.ProviderPlan +public class ProviderPlan : Core.Billing.Providers.Entities.ProviderPlan { public virtual Provider Provider { get; set; } } @@ -13,6 +13,6 @@ public class ProviderPlanMapperProfile : Profile { public ProviderPlanMapperProfile() { - CreateMap().ReverseMap(); + CreateMap().ReverseMap(); } } diff --git a/src/Infrastructure.EntityFramework/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs b/src/Infrastructure.EntityFramework/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs index c7c9a6118b..4a9a82c9dc 100644 --- a/src/Infrastructure.EntityFramework/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs +++ b/src/Infrastructure.EntityFramework/Billing/Repositories/ClientOrganizationMigrationRecordRepository.cs @@ -1,6 +1,6 @@ using AutoMapper; -using Bit.Core.Billing.Entities; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Infrastructure.EntityFramework/Billing/Repositories/ProviderInvoiceItemRepository.cs b/src/Infrastructure.EntityFramework/Billing/Repositories/ProviderInvoiceItemRepository.cs index 87e960e123..ed729070ae 100644 --- a/src/Infrastructure.EntityFramework/Billing/Repositories/ProviderInvoiceItemRepository.cs +++ b/src/Infrastructure.EntityFramework/Billing/Repositories/ProviderInvoiceItemRepository.cs @@ -1,6 +1,6 @@ using AutoMapper; -using Bit.Core.Billing.Entities; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using LinqToDB; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Infrastructure.EntityFramework/Billing/Repositories/ProviderPlanRepository.cs b/src/Infrastructure.EntityFramework/Billing/Repositories/ProviderPlanRepository.cs index 386f7115d7..e022527d64 100644 --- a/src/Infrastructure.EntityFramework/Billing/Repositories/ProviderPlanRepository.cs +++ b/src/Infrastructure.EntityFramework/Billing/Repositories/ProviderPlanRepository.cs @@ -1,6 +1,6 @@ using AutoMapper; -using Bit.Core.Billing.Entities; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs index c9f0406a58..22818517d3 100644 --- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.Auth.Repositories; +using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Billing.Repositories; using Bit.Core.Enums; using Bit.Core.KeyManagement.Repositories; diff --git a/test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index 0b5f5c1f01..44ad5088cd 100644 --- a/test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Admin.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -5,7 +5,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Test.Common.AutoFixture; diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs index 7d0a57ea45..3484c9a995 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationsControllerTests.cs @@ -21,7 +21,7 @@ using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Services; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; diff --git a/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs index 8ddd92a5fa..c7c749effd 100644 --- a/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/ProviderClientsControllerTests.cs @@ -6,7 +6,7 @@ using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Billing.Enums; -using Bit.Core.Billing.Services; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; diff --git a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs index 7933c79b6c..a082caa469 100644 --- a/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/ProviderBillingControllerTests.cs @@ -1,16 +1,17 @@ using Bit.Api.Billing.Controllers; using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Responses; -using Bit.Commercial.Core.Billing; +using Bit.Commercial.Core.Billing.Providers.Services; using Bit.Core; using Bit.Core.AdminConsole.Entities.Provider; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Repositories; using Bit.Core.Billing.Constants; -using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; +using Bit.Core.Billing.Providers.Services; using Bit.Core.Billing.Services; using Bit.Core.Billing.Tax.Models; using Bit.Core.Context; diff --git a/test/Billing.Test/Services/ProviderEventServiceTests.cs b/test/Billing.Test/Services/ProviderEventServiceTests.cs index e080dd8288..7d95157bd2 100644 --- a/test/Billing.Test/Services/ProviderEventServiceTests.cs +++ b/test/Billing.Test/Services/ProviderEventServiceTests.cs @@ -4,10 +4,10 @@ using Bit.Billing.Test.Utilities; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Models.Data.Provider; using Bit.Core.AdminConsole.Repositories; -using Bit.Core.Billing.Entities; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Repositories; +using Bit.Core.Billing.Providers.Entities; +using Bit.Core.Billing.Providers.Repositories; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Utilities; diff --git a/test/Core.Test/Billing/Tax/Services/AutomaticTaxFactoryTests.cs b/test/Core.Test/Billing/Tax/Services/AutomaticTaxFactoryTests.cs index 8de51b1745..d9d2679bca 100644 --- a/test/Core.Test/Billing/Tax/Services/AutomaticTaxFactoryTests.cs +++ b/test/Core.Test/Billing/Tax/Services/AutomaticTaxFactoryTests.cs @@ -2,7 +2,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services.Contracts; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Billing.Tax.Services.Implementations; using Bit.Core.Entities; using Bit.Test.Common.AutoFixture; diff --git a/test/Core.Test/Services/StripePaymentServiceTests.cs b/test/Core.Test/Services/StripePaymentServiceTests.cs index fa1dd60617..7d8a059d76 100644 --- a/test/Core.Test/Services/StripePaymentServiceTests.cs +++ b/test/Core.Test/Services/StripePaymentServiceTests.cs @@ -1,7 +1,7 @@ using Bit.Core.Billing.Enums; using Bit.Core.Billing.Models.StaticStore.Plans; using Bit.Core.Billing.Pricing; -using Bit.Core.Billing.Services.Contracts; +using Bit.Core.Billing.Tax.Models; using Bit.Core.Billing.Tax.Requests; using Bit.Core.Billing.Tax.Services; using Bit.Core.Enums; From 1ec06faf7dea576b28f79742da5f0277e0ca98a2 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Wed, 21 May 2025 21:28:37 +0100 Subject: [PATCH 084/114] [PM 21889] 500 Response on POST billing/restart-subscription (#5851) * Resolve the 500 errors on restart subscription * Rename the variable name --- .../Billing/Controllers/OrganizationBillingController.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs index 9e57545098..094ca0a435 100644 --- a/src/Api/Billing/Controllers/OrganizationBillingController.cs +++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs @@ -302,8 +302,12 @@ public class OrganizationBillingController( Debug.Assert(org is not null, "This organization has already been found via this same ID, this should be fine."); var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken); var taxInformation = TaxInformation.From(organizationSignup.TaxInfo); - await organizationBillingService.UpdatePaymentMethod(org, paymentSource, taxInformation); await organizationBillingService.Finalize(sale); + var updatedOrg = await organizationRepository.GetByIdAsync(organizationId); + if (updatedOrg != null) + { + await organizationBillingService.UpdatePaymentMethod(updatedOrg, paymentSource, taxInformation); + } return TypedResults.Ok(); } From ad8b1d914315f38d48d8c946d02fa325c592c30d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 09:51:38 -0400 Subject: [PATCH 085/114] [deps] Auth: Update sass-loader to v16.0.5 (#5689) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Ike <137194738+ike-kottlowski@users.noreply.github.com> --- bitwarden_license/src/Sso/package-lock.json | 8 ++++---- bitwarden_license/src/Sso/package.json | 2 +- src/Admin/package-lock.json | 8 ++++---- src/Admin/package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bitwarden_license/src/Sso/package-lock.json b/bitwarden_license/src/Sso/package-lock.json index 98ea72c69e..70ee3ed3cd 100644 --- a/bitwarden_license/src/Sso/package-lock.json +++ b/bitwarden_license/src/Sso/package-lock.json @@ -18,7 +18,7 @@ "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", "sass": "1.85.0", - "sass-loader": "16.0.4", + "sass-loader": "16.0.5", "webpack": "5.97.1", "webpack-cli": "5.1.4" } @@ -1898,9 +1898,9 @@ } }, "node_modules/sass-loader": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz", - "integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==", + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz", + "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/bitwarden_license/src/Sso/package.json b/bitwarden_license/src/Sso/package.json index 289612e79a..1e46a3dc80 100644 --- a/bitwarden_license/src/Sso/package.json +++ b/bitwarden_license/src/Sso/package.json @@ -17,7 +17,7 @@ "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", "sass": "1.85.0", - "sass-loader": "16.0.4", + "sass-loader": "16.0.5", "webpack": "5.97.1", "webpack-cli": "5.1.4" } diff --git a/src/Admin/package-lock.json b/src/Admin/package-lock.json index 3d339bd80c..56944920b8 100644 --- a/src/Admin/package-lock.json +++ b/src/Admin/package-lock.json @@ -19,7 +19,7 @@ "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", "sass": "1.85.0", - "sass-loader": "16.0.4", + "sass-loader": "16.0.5", "webpack": "5.97.1", "webpack-cli": "5.1.4" } @@ -1899,9 +1899,9 @@ } }, "node_modules/sass-loader": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz", - "integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==", + "version": "16.0.5", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz", + "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/Admin/package.json b/src/Admin/package.json index eed8eaf7aa..ee6a7f5dfb 100644 --- a/src/Admin/package.json +++ b/src/Admin/package.json @@ -18,7 +18,7 @@ "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", "sass": "1.85.0", - "sass-loader": "16.0.4", + "sass-loader": "16.0.5", "webpack": "5.97.1", "webpack-cli": "5.1.4" } From ad22f77aba99b5a5350b3c79fb392c92ca1a7fb8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 11:17:17 -0400 Subject: [PATCH 086/114] [deps] Auth: Update webpack to v5.99.8 (#5728) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bitwarden_license/src/Sso/package-lock.json | 99 +++------------------ bitwarden_license/src/Sso/package.json | 2 +- src/Admin/package-lock.json | 99 +++------------------ src/Admin/package.json | 2 +- 4 files changed, 22 insertions(+), 180 deletions(-) diff --git a/bitwarden_license/src/Sso/package-lock.json b/bitwarden_license/src/Sso/package-lock.json index 70ee3ed3cd..3e207c2d8a 100644 --- a/bitwarden_license/src/Sso/package-lock.json +++ b/bitwarden_license/src/Sso/package-lock.json @@ -19,7 +19,7 @@ "mini-css-extract-plugin": "2.9.2", "sass": "1.85.0", "sass-loader": "16.0.5", - "webpack": "5.97.1", + "webpack": "5.99.8", "webpack-cli": "5.1.4" } }, @@ -1106,13 +1106,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-uri": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", @@ -1754,16 +1747,6 @@ "dev": true, "license": "MIT" }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -1939,9 +1922,9 @@ } }, "node_modules/schema-utils": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", - "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2193,16 +2176,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2225,14 +2198,15 @@ } }, "node_modules/webpack": { - "version": "5.97.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", - "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "version": "5.99.8", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz", + "integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==", "dev": true, "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", @@ -2249,9 +2223,9 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", + "schema-utils": "^4.3.2", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", + "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, @@ -2352,59 +2326,6 @@ "node": ">=10.13.0" } }, - "node_modules/webpack/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/bitwarden_license/src/Sso/package.json b/bitwarden_license/src/Sso/package.json index 1e46a3dc80..0cdd8871c0 100644 --- a/bitwarden_license/src/Sso/package.json +++ b/bitwarden_license/src/Sso/package.json @@ -18,7 +18,7 @@ "mini-css-extract-plugin": "2.9.2", "sass": "1.85.0", "sass-loader": "16.0.5", - "webpack": "5.97.1", + "webpack": "5.99.8", "webpack-cli": "5.1.4" } } diff --git a/src/Admin/package-lock.json b/src/Admin/package-lock.json index 56944920b8..ee4a951363 100644 --- a/src/Admin/package-lock.json +++ b/src/Admin/package-lock.json @@ -20,7 +20,7 @@ "mini-css-extract-plugin": "2.9.2", "sass": "1.85.0", "sass-loader": "16.0.5", - "webpack": "5.97.1", + "webpack": "5.99.8", "webpack-cli": "5.1.4" } }, @@ -1107,13 +1107,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, "node_modules/fast-uri": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", @@ -1755,16 +1748,6 @@ "dev": true, "license": "MIT" }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -1940,9 +1923,9 @@ } }, "node_modules/schema-utils": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", - "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2202,16 +2185,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -2234,14 +2207,15 @@ } }, "node_modules/webpack": { - "version": "5.97.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", - "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "version": "5.99.8", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz", + "integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==", "dev": true, "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", @@ -2258,9 +2232,9 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", + "schema-utils": "^4.3.2", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", + "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, @@ -2361,59 +2335,6 @@ "node": ">=10.13.0" } }, - "node_modules/webpack/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/webpack/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/webpack/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/src/Admin/package.json b/src/Admin/package.json index ee6a7f5dfb..b69bdcace4 100644 --- a/src/Admin/package.json +++ b/src/Admin/package.json @@ -19,7 +19,7 @@ "mini-css-extract-plugin": "2.9.2", "sass": "1.85.0", "sass-loader": "16.0.5", - "webpack": "5.97.1", + "webpack": "5.99.8", "webpack-cli": "5.1.4" } } From dd60769499d14543e3b607526e8d4053c0d4f384 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 12:24:55 -0400 Subject: [PATCH 087/114] [deps] Auth: Update bootstrap to v5.3.6 (#5687) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bitwarden_license/src/Sso/package-lock.json | 8 ++++---- bitwarden_license/src/Sso/package.json | 2 +- src/Admin/package-lock.json | 8 ++++---- src/Admin/package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bitwarden_license/src/Sso/package-lock.json b/bitwarden_license/src/Sso/package-lock.json index 3e207c2d8a..c7d113f56d 100644 --- a/bitwarden_license/src/Sso/package-lock.json +++ b/bitwarden_license/src/Sso/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "license": "-", "dependencies": { - "bootstrap": "5.3.3", + "bootstrap": "5.3.6", "font-awesome": "4.7.0", "jquery": "3.7.1" }, @@ -748,9 +748,9 @@ } }, "node_modules/bootstrap": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", - "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz", + "integrity": "sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==", "funding": [ { "type": "github", diff --git a/bitwarden_license/src/Sso/package.json b/bitwarden_license/src/Sso/package.json index 0cdd8871c0..51436d2a11 100644 --- a/bitwarden_license/src/Sso/package.json +++ b/bitwarden_license/src/Sso/package.json @@ -8,7 +8,7 @@ "build": "webpack" }, "dependencies": { - "bootstrap": "5.3.3", + "bootstrap": "5.3.6", "font-awesome": "4.7.0", "jquery": "3.7.1" }, diff --git a/src/Admin/package-lock.json b/src/Admin/package-lock.json index ee4a951363..95e514ce04 100644 --- a/src/Admin/package-lock.json +++ b/src/Admin/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "license": "GPL-3.0", "dependencies": { - "bootstrap": "5.3.3", + "bootstrap": "5.3.6", "font-awesome": "4.7.0", "jquery": "3.7.1", "toastr": "2.1.4" @@ -749,9 +749,9 @@ } }, "node_modules/bootstrap": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", - "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", + "version": "5.3.6", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz", + "integrity": "sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==", "funding": [ { "type": "github", diff --git a/src/Admin/package.json b/src/Admin/package.json index b69bdcace4..74c2ceca7a 100644 --- a/src/Admin/package.json +++ b/src/Admin/package.json @@ -8,7 +8,7 @@ "build": "webpack" }, "dependencies": { - "bootstrap": "5.3.3", + "bootstrap": "5.3.6", "font-awesome": "4.7.0", "jquery": "3.7.1", "toastr": "2.1.4" From 34414a061099215f77d6bd9dd47740e4a8b6d8e0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 17:02:23 +0000 Subject: [PATCH 088/114] [deps] Auth: Update sass to v1.88.0 (#5684) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Ike <137194738+ike-kottlowski@users.noreply.github.com> --- bitwarden_license/src/Sso/package-lock.json | 8 ++++---- bitwarden_license/src/Sso/package.json | 2 +- src/Admin/package-lock.json | 8 ++++---- src/Admin/package.json | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bitwarden_license/src/Sso/package-lock.json b/bitwarden_license/src/Sso/package-lock.json index c7d113f56d..4312ff549c 100644 --- a/bitwarden_license/src/Sso/package-lock.json +++ b/bitwarden_license/src/Sso/package-lock.json @@ -17,7 +17,7 @@ "css-loader": "7.1.2", "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", - "sass": "1.85.0", + "sass": "1.88.0", "sass-loader": "16.0.5", "webpack": "5.99.8", "webpack-cli": "5.1.4" @@ -1860,9 +1860,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.85.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz", - "integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==", + "version": "1.88.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz", + "integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/bitwarden_license/src/Sso/package.json b/bitwarden_license/src/Sso/package.json index 51436d2a11..137f86680c 100644 --- a/bitwarden_license/src/Sso/package.json +++ b/bitwarden_license/src/Sso/package.json @@ -16,7 +16,7 @@ "css-loader": "7.1.2", "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", - "sass": "1.85.0", + "sass": "1.88.0", "sass-loader": "16.0.5", "webpack": "5.99.8", "webpack-cli": "5.1.4" diff --git a/src/Admin/package-lock.json b/src/Admin/package-lock.json index 95e514ce04..b50596af21 100644 --- a/src/Admin/package-lock.json +++ b/src/Admin/package-lock.json @@ -18,7 +18,7 @@ "css-loader": "7.1.2", "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", - "sass": "1.85.0", + "sass": "1.88.0", "sass-loader": "16.0.5", "webpack": "5.99.8", "webpack-cli": "5.1.4" @@ -1861,9 +1861,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.85.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz", - "integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==", + "version": "1.88.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz", + "integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/Admin/package.json b/src/Admin/package.json index 74c2ceca7a..e88cd42eca 100644 --- a/src/Admin/package.json +++ b/src/Admin/package.json @@ -17,7 +17,7 @@ "css-loader": "7.1.2", "expose-loader": "5.0.1", "mini-css-extract-plugin": "2.9.2", - "sass": "1.85.0", + "sass": "1.88.0", "sass-loader": "16.0.5", "webpack": "5.99.8", "webpack-cli": "5.1.4" From 77865f071a32c3e31b631f6b712c3e767ce455b4 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 May 2025 13:33:46 -0400 Subject: [PATCH 089/114] [deps] Auth: Lock file maintenance (#5747) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- bitwarden_license/src/Sso/package-lock.json | 76 ++++++++++----------- src/Admin/package-lock.json | 76 ++++++++++----------- 2 files changed, 76 insertions(+), 76 deletions(-) diff --git a/bitwarden_license/src/Sso/package-lock.json b/bitwarden_license/src/Sso/package-lock.json index 4312ff549c..636d6317a1 100644 --- a/bitwarden_license/src/Sso/package-lock.json +++ b/bitwarden_license/src/Sso/package-lock.json @@ -455,13 +455,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.13.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", - "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", + "version": "22.15.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", + "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@webassemblyjs/ast": { @@ -781,9 +781,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", "dev": true, "funding": [ { @@ -801,10 +801,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -821,9 +821,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001707", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", - "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", + "version": "1.0.30001718", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", + "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", "dev": true, "funding": [ { @@ -975,9 +975,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.128", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz", - "integrity": "sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==", + "version": "1.5.155", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", + "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", "dev": true, "license": "ISC" }, @@ -1009,9 +1009,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", - "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, "license": "MIT" }, @@ -1241,9 +1241,9 @@ } }, "node_modules/immutable": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz", - "integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz", + "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==", "dev": true, "license": "MIT" }, @@ -1942,9 +1942,9 @@ } }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -2061,9 +2061,9 @@ } }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", "dev": true, "license": "MIT", "engines": { @@ -2071,14 +2071,14 @@ } }, "node_modules/terser": { - "version": "5.39.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", - "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", + "version": "5.39.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz", + "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.14.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -2139,9 +2139,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, @@ -2184,9 +2184,9 @@ "license": "MIT" }, "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", "dev": true, "license": "MIT", "dependencies": { diff --git a/src/Admin/package-lock.json b/src/Admin/package-lock.json index b50596af21..e73ccfcef5 100644 --- a/src/Admin/package-lock.json +++ b/src/Admin/package-lock.json @@ -456,13 +456,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.13.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", - "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", + "version": "22.15.21", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz", + "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.20.0" + "undici-types": "~6.21.0" } }, "node_modules/@webassemblyjs/ast": { @@ -782,9 +782,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.24.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", + "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", "dev": true, "funding": [ { @@ -802,10 +802,10 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", + "caniuse-lite": "^1.0.30001716", + "electron-to-chromium": "^1.5.149", "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -822,9 +822,9 @@ "license": "MIT" }, "node_modules/caniuse-lite": { - "version": "1.0.30001707", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", - "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", + "version": "1.0.30001718", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", + "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", "dev": true, "funding": [ { @@ -976,9 +976,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.128", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz", - "integrity": "sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==", + "version": "1.5.155", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", + "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", "dev": true, "license": "ISC" }, @@ -1010,9 +1010,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", - "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", "dev": true, "license": "MIT" }, @@ -1242,9 +1242,9 @@ } }, "node_modules/immutable": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz", - "integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz", + "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==", "dev": true, "license": "MIT" }, @@ -1943,9 +1943,9 @@ } }, "node_modules/semver": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", - "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { @@ -2062,9 +2062,9 @@ } }, "node_modules/tapable": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", "dev": true, "license": "MIT", "engines": { @@ -2072,14 +2072,14 @@ } }, "node_modules/terser": { - "version": "5.39.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", - "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", + "version": "5.39.2", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz", + "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.8.2", + "acorn": "^8.14.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -2148,9 +2148,9 @@ } }, "node_modules/undici-types": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", - "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, @@ -2193,9 +2193,9 @@ "license": "MIT" }, "node_modules/watchpack": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", - "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", + "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", "dev": true, "license": "MIT", "dependencies": { From 328b84eea0f93d5ede787509ccf5f859594fb0c4 Mon Sep 17 00:00:00 2001 From: Matt Gibson Date: Thu, 22 May 2025 12:49:14 -0700 Subject: [PATCH 090/114] Add-userid-to-encryption-methods (#5838) * Add userId to auth success response * Validate user that encrypted a cipher matches the user posting the request * Remove userId from auth success we don't want to expand this response model --- .../Vault/Controllers/CiphersController.cs | 69 +++++++++++++++++++ .../Models/Request/CipherRequestModel.cs | 4 ++ 2 files changed, 73 insertions(+) diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs index 4f105128ea..251362589e 100644 --- a/src/Api/Vault/Controllers/CiphersController.cs +++ b/src/Api/Vault/Controllers/CiphersController.cs @@ -151,6 +151,16 @@ public class CiphersController : Controller public async Task Post([FromBody] CipherRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); + + // Validate the model was encrypted for the posting user + if (model.EncryptedFor != null) + { + if (model.EncryptedFor != user.Id) + { + throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); + } + } + var cipher = model.ToCipherDetails(user.Id); if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value)) { @@ -170,6 +180,16 @@ public class CiphersController : Controller public async Task PostCreate([FromBody] CipherCreateRequestModel model) { var user = await _userService.GetUserByPrincipalAsync(User); + + // Validate the model was encrypted for the posting user + if (model.Cipher.EncryptedFor != null) + { + if (model.Cipher.EncryptedFor != user.Id) + { + throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); + } + } + var cipher = model.Cipher.ToCipherDetails(user.Id); if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value)) { @@ -192,6 +212,16 @@ public class CiphersController : Controller } var userId = _userService.GetProperUserId(User).Value; + + // Validate the model was encrypted for the posting user + if (model.Cipher.EncryptedFor != null) + { + if (model.Cipher.EncryptedFor != userId) + { + throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); + } + } + await _cipherService.SaveAsync(cipher, userId, model.Cipher.LastKnownRevisionDate, model.CollectionIds, true, false); var response = new CipherMiniResponseModel(cipher, _globalSettings, false); @@ -209,6 +239,15 @@ public class CiphersController : Controller throw new NotFoundException(); } + // Validate the model was encrypted for the posting user + if (model.EncryptedFor != null) + { + if (model.EncryptedFor != user.Id) + { + throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); + } + } + ValidateClientVersionForFido2CredentialSupport(cipher); var collectionIds = (await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id)).Select(c => c.CollectionId).ToList(); @@ -237,6 +276,15 @@ public class CiphersController : Controller var userId = _userService.GetProperUserId(User).Value; var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(id); + // Validate the model was encrypted for the posting user + if (model.EncryptedFor != null) + { + if (model.EncryptedFor != userId) + { + throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); + } + } + ValidateClientVersionForFido2CredentialSupport(cipher); if (cipher == null || !cipher.OrganizationId.HasValue || @@ -658,6 +706,15 @@ public class CiphersController : Controller throw new NotFoundException(); } + // Validate the model was encrypted for the posting user + if (model.Cipher.EncryptedFor != null) + { + if (model.Cipher.EncryptedFor != user.Id) + { + throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); + } + } + ValidateClientVersionForFido2CredentialSupport(cipher); var original = cipher.Clone(); @@ -1019,6 +1076,18 @@ public class CiphersController : Controller var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: false); var ciphersDict = ciphers.ToDictionary(c => c.Id); + // Validate the model was encrypted for the posting user + foreach (var cipher in model.Ciphers) + { + if (cipher.EncryptedFor != null) + { + if (cipher.EncryptedFor != userId) + { + throw new BadRequestException("Cipher was not encrypted for the current user. Please try again."); + } + } + } + var shareCiphers = new List<(Cipher, DateTime?)>(); foreach (var cipher in model.Ciphers) { diff --git a/src/Api/Vault/Models/Request/CipherRequestModel.cs b/src/Api/Vault/Models/Request/CipherRequestModel.cs index 89eda415b1..5c288ab66d 100644 --- a/src/Api/Vault/Models/Request/CipherRequestModel.cs +++ b/src/Api/Vault/Models/Request/CipherRequestModel.cs @@ -11,6 +11,10 @@ namespace Bit.Api.Vault.Models.Request; public class CipherRequestModel { + /// + /// The Id of the user that encrypted the cipher. It should always represent a UserId. + /// + public Guid? EncryptedFor { get; set; } public CipherType Type { get; set; } [StringLength(36)] From 83478f9c698411da1d4ea539a0a3303775530f4d Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Fri, 23 May 2025 11:27:37 +1000 Subject: [PATCH 091/114] [PM-13274] [Unified] Add integration tests for creating and updating collections (#5814) --- .../AdminConsole/OrganizationTestHelpers.cs | 11 +- .../CollectionRepositoryCreateTests.cs | 105 +++++++++++++ .../CollectionRepositoryReplaceTests.cs | 147 ++++++++++++++++++ .../CollectionRepositoryTests.cs | 145 +---------------- 4 files changed, 263 insertions(+), 145 deletions(-) create mode 100644 test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryCreateTests.cs create mode 100644 test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryReplaceTests.cs rename test/Infrastructure.IntegrationTest/{Vault/Repositories => AdminConsole/Repositories/CollectionRepository}/CollectionRepositoryTests.cs (76%) diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs b/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs index e631280bb3..144bff9dcb 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs @@ -1,5 +1,6 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Billing.Enums; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; @@ -26,15 +27,23 @@ public static class OrganizationTestHelpers }); } + /// + /// Creates an Enterprise organization. + /// public static Task CreateTestOrganizationAsync(this IOrganizationRepository organizationRepository, string identifier = "test") => organizationRepository.CreateAsync(new Organization { Name = $"{identifier}-{Guid.NewGuid()}", BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL - Plan = "Test", // TODO: EF does not enforce this being NOT NULl + Plan = "Enterprise (Annually)", // TODO: EF does not enforce this being NOT NULl + PlanType = PlanType.EnterpriseAnnually }); + /// + /// Creates a confirmed Owner for the specified organization and user. + /// Does not include any cryptographic material. + /// public static Task CreateTestOrganizationUserAsync( this IOrganizationUserRepository organizationUserRepository, Organization organization, diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryCreateTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryCreateTests.cs new file mode 100644 index 0000000000..8a51f201dc --- /dev/null +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryCreateTests.cs @@ -0,0 +1,105 @@ +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Entities; +using Bit.Core.Models.Data; +using Bit.Core.Repositories; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository; + +public class CollectionRepositoryCreateTests +{ + [DatabaseTheory, DatabaseData] + public async Task CreateAsync_WithAccess_Works( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IGroupRepository groupRepository, + ICollectionRepository collectionRepository) + { + // Arrange + var organization = await organizationRepository.CreateTestOrganizationAsync(); + + var user1 = await userRepository.CreateTestUserAsync(); + var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user1); + + var user2 = await userRepository.CreateTestUserAsync(); + var orgUser2 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user2); + + var group1 = await groupRepository.CreateTestGroupAsync(organization); + var group2 = await groupRepository.CreateTestGroupAsync(organization); + + var collection = new Collection + { + Name = "Test Collection Name", + OrganizationId = organization.Id, + }; + + // Act + await collectionRepository.CreateAsync(collection, + [ + new CollectionAccessSelection { Id = group1.Id, Manage = true, HidePasswords = true, ReadOnly = false, }, + new CollectionAccessSelection { Id = group2.Id, Manage = false, HidePasswords = false, ReadOnly = true, }, + ], + [ + new CollectionAccessSelection { Id = orgUser1.Id, Manage = true, HidePasswords = false, ReadOnly = true }, + new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = true, ReadOnly = false }, + ] + ); + + // Assert + var (actualCollection, actualAccess) = await collectionRepository.GetByIdWithAccessAsync(collection.Id); + + Assert.NotNull(actualCollection); + Assert.Equal("Test Collection Name", actualCollection.Name); + + var groups = actualAccess.Groups.ToArray(); + Assert.Equal(2, groups.Length); + Assert.Single(groups, g => g.Id == group1.Id && g.Manage && g.HidePasswords && !g.ReadOnly); + Assert.Single(groups, g => g.Id == group2.Id && !g.Manage && !g.HidePasswords && g.ReadOnly); + + var users = actualAccess.Users.ToArray(); + Assert.Equal(2, users.Length); + Assert.Single(users, u => u.Id == orgUser1.Id && u.Manage && !u.HidePasswords && u.ReadOnly); + Assert.Single(users, u => u.Id == orgUser2.Id && !u.Manage && u.HidePasswords && !u.ReadOnly); + + // Clean up data + await userRepository.DeleteAsync(user1); + await userRepository.DeleteAsync(user2); + await organizationRepository.DeleteAsync(organization); + await groupRepository.DeleteManyAsync([group1.Id, group2.Id]); + await organizationUserRepository.DeleteManyAsync([orgUser1.Id, orgUser2.Id]); + } + + /// + /// Makes sure that the sproc handles empty sets. + /// + [DatabaseTheory, DatabaseData] + public async Task CreateAsync_WithNoAccess_Works( + IOrganizationRepository organizationRepository, + ICollectionRepository collectionRepository) + { + // Arrange + var organization = await organizationRepository.CreateTestOrganizationAsync(); + + var collection = new Collection + { + Name = "Test Collection Name", + OrganizationId = organization.Id, + }; + + // Act + await collectionRepository.CreateAsync(collection, [], []); + + // Assert + var (actualCollection, actualAccess) = await collectionRepository.GetByIdWithAccessAsync(collection.Id); + + Assert.NotNull(actualCollection); + Assert.Equal("Test Collection Name", actualCollection.Name); + + Assert.Empty(actualAccess.Groups); + Assert.Empty(actualAccess.Users); + + // Clean up + await organizationRepository.DeleteAsync(organization); + } +} diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryReplaceTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryReplaceTests.cs new file mode 100644 index 0000000000..df01276493 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryReplaceTests.cs @@ -0,0 +1,147 @@ +using Bit.Core.AdminConsole.Repositories; +using Bit.Core.Entities; +using Bit.Core.Models.Data; +using Bit.Core.Repositories; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository; + +public class CollectionRepositoryReplaceTests +{ + [DatabaseTheory, DatabaseData] + public async Task ReplaceAsync_WithAccess_Works( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IGroupRepository groupRepository, + ICollectionRepository collectionRepository) + { + // Arrange + var organization = await organizationRepository.CreateTestOrganizationAsync(); + + var user1 = await userRepository.CreateTestUserAsync(); + var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user1); + + var user2 = await userRepository.CreateTestUserAsync(); + var orgUser2 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user2); + + var user3 = await userRepository.CreateTestUserAsync(); + var orgUser3 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user3); + + var group1 = await groupRepository.CreateTestGroupAsync(organization); + var group2 = await groupRepository.CreateTestGroupAsync(organization); + var group3 = await groupRepository.CreateTestGroupAsync(organization); + + var collection = new Collection + { + Name = "Test Collection Name", + OrganizationId = organization.Id, + }; + + await collectionRepository.CreateAsync(collection, + [ + new CollectionAccessSelection { Id = group1.Id, Manage = true, HidePasswords = true, ReadOnly = false, }, + new CollectionAccessSelection { Id = group2.Id, Manage = false, HidePasswords = false, ReadOnly = true, }, + ], + [ + new CollectionAccessSelection { Id = orgUser1.Id, Manage = true, HidePasswords = false, ReadOnly = true }, + new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = true, ReadOnly = false }, + ] + ); + + // Act + collection.Name = "Updated Collection Name"; + + await collectionRepository.ReplaceAsync(collection, + [ + // Delete group1 + // Update group2: + new CollectionAccessSelection { Id = group2.Id, Manage = true, HidePasswords = true, ReadOnly = false, }, + // Add group3: + new CollectionAccessSelection { Id = group3.Id, Manage = false, HidePasswords = false, ReadOnly = true, }, + ], + [ + // Delete orgUser1 + // Update orgUser2: + new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = false, ReadOnly = true }, + // Add orgUser3: + new CollectionAccessSelection { Id = orgUser3.Id, Manage = true, HidePasswords = false, ReadOnly = true }, + ] + ); + + // Assert + var (actualCollection, actualAccess) = await collectionRepository.GetByIdWithAccessAsync(collection.Id); + + Assert.NotNull(actualCollection); + Assert.Equal("Updated Collection Name", actualCollection.Name); + + var groups = actualAccess.Groups.ToArray(); + Assert.Equal(2, groups.Length); + Assert.Single(groups, g => g.Id == group2.Id && g.Manage && g.HidePasswords && !g.ReadOnly); + Assert.Single(groups, g => g.Id == group3.Id && !g.Manage && !g.HidePasswords && g.ReadOnly); + + var users = actualAccess.Users.ToArray(); + + Assert.Equal(2, users.Length); + Assert.Single(users, u => u.Id == orgUser2.Id && !u.Manage && !u.HidePasswords && u.ReadOnly); + Assert.Single(users, u => u.Id == orgUser3.Id && u.Manage && !u.HidePasswords && u.ReadOnly); + + // Clean up data + await userRepository.DeleteAsync(user1); + await userRepository.DeleteAsync(user2); + await userRepository.DeleteAsync(user3); + await organizationRepository.DeleteAsync(organization); + } + + /// + /// Makes sure that the sproc handles empty sets. + /// + [DatabaseTheory, DatabaseData] + public async Task ReplaceAsync_WithNoAccess_Works( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + IGroupRepository groupRepository, + ICollectionRepository collectionRepository) + { + // Arrange + var organization = await organizationRepository.CreateTestOrganizationAsync(); + + var user = await userRepository.CreateTestUserAsync(); + var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user); + + var group = await groupRepository.CreateTestGroupAsync(organization); + + var collection = new Collection + { + Name = "Test Collection Name", + OrganizationId = organization.Id, + }; + + await collectionRepository.CreateAsync(collection, + [ + new CollectionAccessSelection { Id = group.Id, Manage = true, HidePasswords = false, ReadOnly = true }, + ], + [ + new CollectionAccessSelection { Id = orgUser.Id, Manage = true, HidePasswords = false, ReadOnly = true }, + ]); + + // Act + collection.Name = "Updated Collection Name"; + + await collectionRepository.ReplaceAsync(collection, [], []); + + // Assert + var (actualCollection, actualAccess) = await collectionRepository.GetByIdWithAccessAsync(collection.Id); + + Assert.NotNull(actualCollection); + Assert.Equal("Updated Collection Name", actualCollection.Name); + + Assert.Empty(actualAccess.Groups); + Assert.Empty(actualAccess.Users); + + // Clean up + await userRepository.DeleteAsync(user); + await organizationRepository.DeleteAsync(organization); + } +} diff --git a/test/Infrastructure.IntegrationTest/Vault/Repositories/CollectionRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryTests.cs similarity index 76% rename from test/Infrastructure.IntegrationTest/Vault/Repositories/CollectionRepositoryTests.cs rename to test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryTests.cs index 268d46ef6b..b96998415d 100644 --- a/test/Infrastructure.IntegrationTest/Vault/Repositories/CollectionRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/CollectionRepository/CollectionRepositoryTests.cs @@ -7,7 +7,7 @@ using Bit.Core.Models.Data; using Bit.Core.Repositories; using Xunit; -namespace Bit.Infrastructure.IntegrationTest.Repositories; +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository; public class CollectionRepositoryTests { @@ -463,147 +463,4 @@ public class CollectionRepositoryTests Assert.False(c3.Unmanaged); }); } - - [DatabaseTheory, DatabaseData] - public async Task ReplaceAsync_Works( - IUserRepository userRepository, - IOrganizationRepository organizationRepository, - IOrganizationUserRepository organizationUserRepository, - IGroupRepository groupRepository, - ICollectionRepository collectionRepository) - { - var user = await userRepository.CreateAsync(new User - { - Name = "Test User", - Email = $"test+{Guid.NewGuid()}@email.com", - ApiKey = "TEST", - SecurityStamp = "stamp", - }); - - var organization = await organizationRepository.CreateAsync(new Organization - { - Name = "Test Org", - PlanType = PlanType.EnterpriseAnnually, - Plan = "Test Plan", - BillingEmail = "billing@email.com" - }); - - var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser - { - OrganizationId = organization.Id, - UserId = user.Id, - Status = OrganizationUserStatusType.Confirmed, - }); - - var orgUser2 = await organizationUserRepository.CreateAsync(new OrganizationUser - { - OrganizationId = organization.Id, - UserId = user.Id, - Status = OrganizationUserStatusType.Confirmed, - }); - - var orgUser3 = await organizationUserRepository.CreateAsync(new OrganizationUser - { - OrganizationId = organization.Id, - UserId = user.Id, - Status = OrganizationUserStatusType.Confirmed, - }); - - var group1 = await groupRepository.CreateAsync(new Group - { - Name = "Test Group #1", - OrganizationId = organization.Id, - }); - - var group2 = await groupRepository.CreateAsync(new Group - { - Name = "Test Group #2", - OrganizationId = organization.Id, - }); - - var group3 = await groupRepository.CreateAsync(new Group - { - Name = "Test Group #3", - OrganizationId = organization.Id, - }); - - var collection = new Collection - { - Name = "Test Collection Name", - OrganizationId = organization.Id, - }; - - await collectionRepository.CreateAsync(collection, - [ - new CollectionAccessSelection { Id = group1.Id, Manage = true, HidePasswords = true, ReadOnly = false, }, - new CollectionAccessSelection { Id = group2.Id, Manage = false, HidePasswords = false, ReadOnly = true, }, - ], - [ - new CollectionAccessSelection { Id = orgUser1.Id, Manage = true, HidePasswords = false, ReadOnly = true }, - new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = true, ReadOnly = false }, - ] - ); - - collection.Name = "Updated Collection Name"; - - await collectionRepository.ReplaceAsync(collection, - [ - // Should delete group1 - new CollectionAccessSelection { Id = group2.Id, Manage = true, HidePasswords = true, ReadOnly = false, }, - // Should add group3 - new CollectionAccessSelection { Id = group3.Id, Manage = false, HidePasswords = false, ReadOnly = true, }, - ], - [ - // Should delete orgUser1 - new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = false, ReadOnly = true }, - // Should add orgUser3 - new CollectionAccessSelection { Id = orgUser3.Id, Manage = true, HidePasswords = false, ReadOnly = true }, - ] - ); - - // Assert it - var info = await collectionRepository.GetByIdWithPermissionsAsync(collection.Id, user.Id, true); - - Assert.NotNull(info); - - Assert.Equal("Updated Collection Name", info.Name); - - var groups = info.Groups.ToArray(); - - Assert.Equal(2, groups.Length); - - var actualGroup2 = Assert.Single(groups.Where(g => g.Id == group2.Id)); - - Assert.True(actualGroup2.Manage); - Assert.True(actualGroup2.HidePasswords); - Assert.False(actualGroup2.ReadOnly); - - var actualGroup3 = Assert.Single(groups.Where(g => g.Id == group3.Id)); - - Assert.False(actualGroup3.Manage); - Assert.False(actualGroup3.HidePasswords); - Assert.True(actualGroup3.ReadOnly); - - var users = info.Users.ToArray(); - - Assert.Equal(2, users.Length); - - var actualOrgUser2 = Assert.Single(users.Where(u => u.Id == orgUser2.Id)); - - Assert.False(actualOrgUser2.Manage); - Assert.False(actualOrgUser2.HidePasswords); - Assert.True(actualOrgUser2.ReadOnly); - - var actualOrgUser3 = Assert.Single(users.Where(u => u.Id == orgUser3.Id)); - - Assert.True(actualOrgUser3.Manage); - Assert.False(actualOrgUser3.HidePasswords); - Assert.True(actualOrgUser3.ReadOnly); - - // Clean up data - await userRepository.DeleteAsync(user); - await organizationRepository.DeleteAsync(organization); - await groupRepository.DeleteManyAsync([group1.Id, group2.Id, group3.Id]); - await organizationUserRepository.DeleteManyAsync([orgUser1.Id, orgUser2.Id, orgUser3.Id]); - } } From 198d96e1555d9c899db36728b37cbbcfc75f99f3 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Fri, 23 May 2025 11:45:41 +1000 Subject: [PATCH 092/114] [PM-21612] [Unified] Fix unhandled error when editing an invited member (#5817) * Check for UserId instead of passing potentially default value to bump account revision date method. * Pass explicit UserId into CipherRepository.CreateAsync method used for imports. --- src/Core/Entities/User.cs | 5 ++ .../ImportFeatures/ImportCiphersCommand.cs | 2 +- .../Vault/Repositories/ICipherRepository.cs | 5 +- .../Vault/Repositories/CipherRepository.cs | 4 +- .../OrganizationUserRepository.cs | 24 +++-- .../Repositories/DatabaseContextExtensions.cs | 14 ++- .../Vault/Repositories/CipherRepository.cs | 7 +- .../ImportCiphersAsyncCommandTests.cs | 4 +- .../OrganizationRepositoryTests.cs | 3 +- .../OrganizationUserRepositoryTests.cs | 8 +- .../AutoFixture/OrganizationUserFixtures.cs | 1 + .../Vault/AutoFixture/CipherFixtures.cs | 1 + .../Repositories/CipherRepositoryTests.cs | 3 +- .../AdminConsole/OrganizationTestHelpers.cs | 21 +++++ .../OrganizationUserReplaceTests.cs | 88 +++++++++++++++++++ .../OrganizationUserRepositoryTests.cs | 2 +- 16 files changed, 169 insertions(+), 23 deletions(-) create mode 100644 test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserReplaceTests.cs rename test/Infrastructure.IntegrationTest/AdminConsole/Repositories/{ => OrganizationUserRepository}/OrganizationUserRepositoryTests.cs (99%) diff --git a/src/Core/Entities/User.cs b/src/Core/Entities/User.cs index b3a6a9592e..08981ca2d3 100644 --- a/src/Core/Entities/User.cs +++ b/src/Core/Entities/User.cs @@ -36,6 +36,11 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac public string? TwoFactorRecoveryCode { get; set; } public string? EquivalentDomains { get; set; } public string? ExcludedGlobalEquivalentDomains { get; set; } + /// + /// The Account Revision Date is used to check if new sync needs to occur. It should be updated + /// whenever a change is made that affects a client's sync data; for example, updating their vault or + /// organization membership. + /// public DateTime AccountRevisionDate { get; set; } = DateTime.UtcNow; public string? Key { get; set; } public string? PublicKey { get; set; } diff --git a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs index 3c58dca183..fd7a82172c 100644 --- a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs +++ b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs @@ -115,7 +115,7 @@ public class ImportCiphersCommand : IImportCiphersCommand } // Create it all - await _cipherRepository.CreateAsync(ciphers, newFolders); + await _cipherRepository.CreateAsync(importingUserId, ciphers, newFolders); // push await _pushService.PushSyncVaultAsync(importingUserId); diff --git a/src/Core/Vault/Repositories/ICipherRepository.cs b/src/Core/Vault/Repositories/ICipherRepository.cs index f6767fada2..46742c6aa3 100644 --- a/src/Core/Vault/Repositories/ICipherRepository.cs +++ b/src/Core/Vault/Repositories/ICipherRepository.cs @@ -32,7 +32,10 @@ public interface ICipherRepository : IRepository Task DeleteByUserIdAsync(Guid userId); Task DeleteByOrganizationIdAsync(Guid organizationId); Task UpdateCiphersAsync(Guid userId, IEnumerable ciphers); - Task CreateAsync(IEnumerable ciphers, IEnumerable folders); + /// + /// Create ciphers and folders for the specified UserId. Must not be used to create organization owned items. + /// + Task CreateAsync(Guid userId, IEnumerable ciphers, IEnumerable folders); Task CreateAsync(IEnumerable ciphers, IEnumerable collections, IEnumerable collectionCiphers, IEnumerable collectionUsers); Task SoftDeleteAsync(IEnumerable ids, Guid userId); diff --git a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs index 3df365330c..e0a89b1685 100644 --- a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs @@ -484,7 +484,7 @@ public class CipherRepository : Repository, ICipherRepository } } - public async Task CreateAsync(IEnumerable ciphers, IEnumerable folders) + public async Task CreateAsync(Guid userId, IEnumerable ciphers, IEnumerable folders) { if (!ciphers.Any()) { @@ -518,7 +518,7 @@ public class CipherRepository : Repository, ICipherRepository await connection.ExecuteAsync( $"[{Schema}].[User_BumpAccountRevisionDate]", - new { Id = ciphers.First().UserId }, + new { Id = userId }, commandType: CommandType.StoredProcedure, transaction: transaction); transaction.Commit(); diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index 10d92357fe..5ef59d51db 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -1,4 +1,5 @@ -using AutoMapper; +using System.Diagnostics; +using AutoMapper; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.Enums; @@ -7,11 +8,12 @@ using Bit.Core.Models.Data; using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Infrastructure.EntityFramework.Models; +using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Repositories.Queries; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -namespace Bit.Infrastructure.EntityFramework.Repositories; +namespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; public class OrganizationUserRepository : Repository, IOrganizationUserRepository { @@ -440,15 +442,23 @@ public class OrganizationUserRepository : Repository requestedCollections) diff --git a/src/Infrastructure.EntityFramework/Repositories/DatabaseContextExtensions.cs b/src/Infrastructure.EntityFramework/Repositories/DatabaseContextExtensions.cs index 6e954e030c..40f2a79887 100644 --- a/src/Infrastructure.EntityFramework/Repositories/DatabaseContextExtensions.cs +++ b/src/Infrastructure.EntityFramework/Repositories/DatabaseContextExtensions.cs @@ -1,4 +1,6 @@ -using System.Diagnostics; +#nullable enable + +using System.Diagnostics; using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.Auth.Enums; using Bit.Core.Enums; @@ -11,8 +13,18 @@ namespace Bit.Infrastructure.EntityFramework.Repositories; public static class DatabaseContextExtensions { + /// + /// Bump the account revision date for the user. + /// The caller is responsible for providing a valid UserId (not a null or default Guid) for a user that exists + /// in the database. + /// public static async Task UserBumpAccountRevisionDateAsync(this DatabaseContext context, Guid userId) { + if (userId == Guid.Empty) + { + throw new ArgumentException("Invalid UserId."); + } + var user = await context.Users.FindAsync(userId); Debug.Assert(user is not null, "The user id is expected to be validated as a true-in database user before making this call."); user.AccountRevisionDate = DateTime.UtcNow; diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs index 090c36ff29..befb835e26 100644 --- a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs +++ b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs @@ -142,8 +142,10 @@ public class CipherRepository : Repository ciphers, IEnumerable folders) + public async Task CreateAsync(Guid userId, IEnumerable ciphers, + IEnumerable folders) { + ciphers = ciphers.ToList(); if (!ciphers.Any()) { return; @@ -156,7 +158,8 @@ public class CipherRepository : Repository>(ciphers); await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, cipherEntities); - await dbContext.UserBumpAccountRevisionDateAsync(ciphers.First().UserId.GetValueOrDefault()); + await dbContext.UserBumpAccountRevisionDateAsync(userId); + await dbContext.SaveChangesAsync(); } } diff --git a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs index 89e6d152cc..f73a628940 100644 --- a/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs +++ b/test/Core.Test/Tools/ImportFeatures/ImportCiphersAsyncCommandTests.cs @@ -49,7 +49,7 @@ public class ImportCiphersAsyncCommandTests await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId); // Assert - await sutProvider.GetDependency().Received(1).CreateAsync(ciphers, Arg.Any>()); + await sutProvider.GetDependency().Received(1).CreateAsync(importingUserId, ciphers, Arg.Any>()); await sutProvider.GetDependency().Received(1).PushSyncVaultAsync(importingUserId); } @@ -77,7 +77,7 @@ public class ImportCiphersAsyncCommandTests await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId); - await sutProvider.GetDependency().Received(1).CreateAsync(ciphers, Arg.Any>()); + await sutProvider.GetDependency().Received(1).CreateAsync(importingUserId, ciphers, Arg.Any>()); await sutProvider.GetDependency().Received(1).PushSyncVaultAsync(importingUserId); } diff --git a/test/Infrastructure.EFIntegration.Test/AdminConsole/Repositories/OrganizationRepositoryTests.cs b/test/Infrastructure.EFIntegration.Test/AdminConsole/Repositories/OrganizationRepositoryTests.cs index e8bafaea5b..e5ad4f505a 100644 --- a/test/Infrastructure.EFIntegration.Test/AdminConsole/Repositories/OrganizationRepositoryTests.cs +++ b/test/Infrastructure.EFIntegration.Test/AdminConsole/Repositories/OrganizationRepositoryTests.cs @@ -7,6 +7,7 @@ using Bit.Core.Test.AutoFixture.Attributes; using Bit.Infrastructure.EFIntegration.Test.AutoFixture; using Bit.Infrastructure.EFIntegration.Test.Repositories.EqualityComparers; using Bit.Infrastructure.EntityFramework.AdminConsole.Models; +using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; using Xunit; using EfRepo = Bit.Infrastructure.EntityFramework.Repositories; using Organization = Bit.Core.AdminConsole.Entities.Organization; @@ -161,7 +162,7 @@ public class OrganizationRepositoryTests [CiSkippedTheory, EfOrganizationUserAutoData] public async Task SearchUnassignedAsync_Works(OrganizationUser orgUser, User user, Organization org, - List efOrgUserRepos, List efOrgRepos, List efUserRepos, + List efOrgUserRepos, List efOrgRepos, List efUserRepos, SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.OrganizationRepository sqlOrgRepo, SqlRepo.UserRepository sqlUserRepo) { orgUser.Type = OrganizationUserType.Owner; diff --git a/test/Infrastructure.EFIntegration.Test/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs b/test/Infrastructure.EFIntegration.Test/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs index 21d4ca3476..b1f9968e14 100644 --- a/test/Infrastructure.EFIntegration.Test/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs +++ b/test/Infrastructure.EFIntegration.Test/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs @@ -24,7 +24,7 @@ public class OrganizationUserRepositoryTests { [CiSkippedTheory, EfOrganizationUserAutoData] public async Task CreateAsync_Works_DataMatches(OrganizationUser orgUser, User user, Organization org, - OrganizationUserCompare equalityComparer, List suts, + OrganizationUserCompare equalityComparer, List suts, List efOrgRepos, List efUserRepos, SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.UserRepository sqlUserRepo, SqlRepo.OrganizationRepository sqlOrgRepo) @@ -67,7 +67,7 @@ public class OrganizationUserRepositoryTests User user, Organization org, OrganizationUserCompare equalityComparer, - List suts, + List suts, List efUserRepos, List efOrgRepos, SqlRepo.OrganizationUserRepository sqlOrgUserRepo, @@ -113,7 +113,7 @@ public class OrganizationUserRepositoryTests } [CiSkippedTheory, EfOrganizationUserAutoData] - public async Task DeleteAsync_Works_DataMatches(OrganizationUser orgUser, User user, Organization org, List suts, + public async Task DeleteAsync_Works_DataMatches(OrganizationUser orgUser, User user, Organization org, List suts, List efUserRepos, List efOrgRepos, SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.UserRepository sqlUserRepo, SqlRepo.OrganizationRepository sqlOrgRepo) @@ -188,7 +188,7 @@ public class OrganizationUserRepositoryTests List efPolicyRepository, List efUserRepository, List efOrganizationRepository, - List suts, + List suts, List efProviderRepository, List efProviderOrganizationRepository, List efProviderUserRepository, diff --git a/test/Infrastructure.EFIntegration.Test/AutoFixture/OrganizationUserFixtures.cs b/test/Infrastructure.EFIntegration.Test/AutoFixture/OrganizationUserFixtures.cs index 191b48852b..8435f2734a 100644 --- a/test/Infrastructure.EFIntegration.Test/AutoFixture/OrganizationUserFixtures.cs +++ b/test/Infrastructure.EFIntegration.Test/AutoFixture/OrganizationUserFixtures.cs @@ -7,6 +7,7 @@ using Bit.Core.Entities; using Bit.Core.Models.Data; using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; using Bit.Core.Test.AutoFixture.UserFixtures; +using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; diff --git a/test/Infrastructure.EFIntegration.Test/Vault/AutoFixture/CipherFixtures.cs b/test/Infrastructure.EFIntegration.Test/Vault/AutoFixture/CipherFixtures.cs index 65b4e4f6d0..7eb4a91ee9 100644 --- a/test/Infrastructure.EFIntegration.Test/Vault/AutoFixture/CipherFixtures.cs +++ b/test/Infrastructure.EFIntegration.Test/Vault/AutoFixture/CipherFixtures.cs @@ -5,6 +5,7 @@ using Bit.Core.Test.AutoFixture.UserFixtures; using Bit.Core.Vault.Entities; using Bit.Core.Vault.Models.Data; using Bit.Infrastructure.EFIntegration.Test.AutoFixture.Relays; +using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Vault.Repositories; using Bit.Test.Common.AutoFixture; diff --git a/test/Infrastructure.EFIntegration.Test/Vault/Repositories/CipherRepositoryTests.cs b/test/Infrastructure.EFIntegration.Test/Vault/Repositories/CipherRepositoryTests.cs index 3618d5dd0e..689bd5e243 100644 --- a/test/Infrastructure.EFIntegration.Test/Vault/Repositories/CipherRepositoryTests.cs +++ b/test/Infrastructure.EFIntegration.Test/Vault/Repositories/CipherRepositoryTests.cs @@ -9,6 +9,7 @@ using Bit.Infrastructure.EntityFramework.Repositories.Queries; using Bit.Test.Common.AutoFixture.Attributes; using LinqToDB; using Xunit; +using EfAdminConsoleRepo = Bit.Infrastructure.EntityFramework.AdminConsole.Repositories; using EfRepo = Bit.Infrastructure.EntityFramework.Repositories; using EfVaultRepo = Bit.Infrastructure.EntityFramework.Vault.Repositories; using SqlRepo = Bit.Infrastructure.Dapper.Repositories; @@ -112,7 +113,7 @@ public class CipherRepositoryTests [CiSkippedTheory, EfOrganizationCipherCustomize, BitAutoData] public async Task CreateAsync_BumpsOrgUserAccountRevisionDates(Cipher cipher, List users, List orgUsers, Collection collection, Organization org, List suts, List efUserRepos, List efOrgRepos, - List efOrgUserRepos, List efCollectionRepos) + List efOrgUserRepos, List efCollectionRepos) { var savedCiphers = new List(); foreach (var sut in suts) diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs b/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs index 144bff9dcb..10361877d8 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/OrganizationTestHelpers.cs @@ -56,6 +56,17 @@ public static class OrganizationTestHelpers Type = OrganizationUserType.Owner }); + public static Task CreateTestOrganizationUserInviteAsync( + this IOrganizationUserRepository organizationUserRepository, + Organization organization) + => organizationUserRepository.CreateAsync(new OrganizationUser + { + OrganizationId = organization.Id, + UserId = null, // Invites are not linked to a UserId + Status = OrganizationUserStatusType.Invited, + Type = OrganizationUserType.Owner + }); + public static Task CreateTestGroupAsync( this IGroupRepository groupRepository, Organization organization, @@ -63,4 +74,14 @@ public static class OrganizationTestHelpers => groupRepository.CreateAsync( new Group { OrganizationId = organization.Id, Name = $"{identifier} {Guid.NewGuid()}" } ); + + public static Task CreateTestCollectionAsync( + this ICollectionRepository collectionRepository, + Organization organization, + string identifier = "test") + => collectionRepository.CreateAsync(new Collection + { + OrganizationId = organization.Id, + Name = $"{identifier} {Guid.NewGuid()}" + }); } diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserReplaceTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserReplaceTests.cs new file mode 100644 index 0000000000..0b38ddc172 --- /dev/null +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserReplaceTests.cs @@ -0,0 +1,88 @@ +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Repositories; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.OrganizationUserRepository; + +public class OrganizationUserReplaceTests +{ + /// + /// Specifically tests OrganizationUsers in the invited state, which is unique because + /// they're not linked to a UserId. + /// + [DatabaseTheory, DatabaseData] + public async Task ReplaceAsync_WithCollectionAccess_WhenUserIsInvited_Success( + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + + var orgUser = await organizationUserRepository.CreateTestOrganizationUserInviteAsync(organization); + + // Act: update the user, including collection access so we test this overloaded method + orgUser.Type = OrganizationUserType.Admin; + orgUser.AccessSecretsManager = true; + var collection = await collectionRepository.CreateTestCollectionAsync(organization); + + await organizationUserRepository.ReplaceAsync(orgUser, [ + new CollectionAccessSelection { Id = collection.Id, Manage = true } + ]); + + // Assert + var (actualOrgUser, actualCollections) = await organizationUserRepository.GetByIdWithCollectionsAsync(orgUser.Id); + Assert.NotNull(actualOrgUser); + Assert.Equal(OrganizationUserType.Admin, actualOrgUser.Type); + Assert.True(actualOrgUser.AccessSecretsManager); + + var collectionAccess = Assert.Single(actualCollections); + Assert.Equal(collection.Id, collectionAccess.Id); + Assert.True(collectionAccess.Manage); + } + + /// + /// Tests OrganizationUsers in the Confirmed status, which is a stand-in for all other + /// non-Invited statuses (which are all linked to a UserId). + /// + /// + /// + /// + [DatabaseTheory, DatabaseData] + public async Task ReplaceAsync_WithCollectionAccess_WhenUserIsConfirmed_Success( + IUserRepository userRepository, + IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, + ICollectionRepository collectionRepository) + { + var organization = await organizationRepository.CreateTestOrganizationAsync(); + + var user = await userRepository.CreateTestUserAsync(); + // OrganizationUser is linked with the User in the Confirmed status + var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user); + + // Act: update the user, including collection access so we test this overloaded method + orgUser.Type = OrganizationUserType.Admin; + orgUser.AccessSecretsManager = true; + var collection = await collectionRepository.CreateTestCollectionAsync(organization); + + await organizationUserRepository.ReplaceAsync(orgUser, [ + new CollectionAccessSelection { Id = collection.Id, Manage = true } + ]); + + // Assert + var (actualOrgUser, actualCollections) = await organizationUserRepository.GetByIdWithCollectionsAsync(orgUser.Id); + Assert.NotNull(actualOrgUser); + Assert.Equal(OrganizationUserType.Admin, actualOrgUser.Type); + Assert.True(actualOrgUser.AccessSecretsManager); + + var collectionAccess = Assert.Single(actualCollections); + Assert.Equal(collection.Id, collectionAccess.Id); + Assert.True(collectionAccess.Manage); + + // Account revision date should be updated to a later date + var actualUser = await userRepository.GetByIdAsync(user.Id); + Assert.NotNull(actualUser); + Assert.True(actualUser.AccountRevisionDate.CompareTo(user.AccountRevisionDate) > 0); + } +} diff --git a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs similarity index 99% rename from test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs rename to test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs index fd759e4777..0df5dcfb50 100644 --- a/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/AdminConsole/Repositories/OrganizationUserRepository/OrganizationUserRepositoryTests.cs @@ -8,7 +8,7 @@ using Bit.Core.Repositories; using Bit.Core.Utilities; using Xunit; -namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories; +namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.OrganizationUserRepository; public class OrganizationUserRepositoryTests { From 542941818a0b86785ef717313dc60beaded03f38 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Fri, 23 May 2025 10:31:10 -0400 Subject: [PATCH 093/114] Disallow non ascii in equivalent domain (#5852) * Test malicious domain change * Add tests to detect non-ascii characters * Revert "Test malicious domain change" This reverts commit 0602bf6d844b611304aba139e9f49cd38594273a. * Remove confusing comment from when I was going to detect problems differently * Update test/Core.Test/Utilities/StaticStoreTests.cs Co-authored-by: Matt Bishop * Update test/Core.Test/Utilities/StaticStoreTests.cs Co-authored-by: Matt Bishop --------- Co-authored-by: Matt Bishop --- test/Core.Test/Utilities/StaticStoreTests.cs | 27 ++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/Core.Test/Utilities/StaticStoreTests.cs b/test/Core.Test/Utilities/StaticStoreTests.cs index e5e2da6a82..05c6d358e5 100644 --- a/test/Core.Test/Utilities/StaticStoreTests.cs +++ b/test/Core.Test/Utilities/StaticStoreTests.cs @@ -28,4 +28,31 @@ public class StaticStoreTests Assert.NotNull(plan); Assert.Equal(planType, plan.Type); } + + [Fact] + public void StaticStore_GlobalEquivalentDomains_OnlyAsciiAllowed() + { + // Ref: https://daniel.haxx.se/blog/2025/05/16/detecting-malicious-unicode/ + // URLs can contain unicode characters that to a computer would point to completely seperate domains but to the + // naked eye look completely identical. For example 'g' and 'ց' look incredibly similar but when included in a + // URL would lead you somewhere different. There is an opening for an attacker to contribute to Bitwarden with a + // url update that could be missed in code review and then if they got a user to that URL Bitwarden could + // consider it equivalent with a cipher in the users vault and offer autofill when we should not. + // GitHub does now show a warning on non-ascii characters but it could still be missed. + // https://github.blog/changelog/2025-05-01-github-now-provides-a-warning-about-hidden-unicode-text/ + + // To defend against this: + // Loop through all equivalent domains and fail if any contain a non-ascii character + // non-ascii character can make a valid URL so it's possible that in the future we have a domain + // we want to allow list, that should be done through `continue`ing in the below foreach loop + // only if the domain strictly equals (do NOT use InvariantCulture comparison) the one added to our allow list. + foreach (var domain in StaticStore.GlobalDomains.SelectMany(p => p.Value)) + { + for (var i = 0; i < domain.Length; i++) + { + var character = domain[i]; + Assert.True(char.IsAscii(character), $"Domain: {domain} contains non-ASCII character: '{character}' at index: {i}"); + } + } + } } From c989abdb82426a5d6aaf63405360af838eca9cfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=9C=A8=20Audrey=20=E2=9C=A8?= Date: Tue, 27 May 2025 08:25:27 -0400 Subject: [PATCH 094/114] [PM-21917] Introduce `SendAuthenticationQuery` (#5857) --- src/Core/Tools/Entities/Send.cs | 12 + .../Models/Data/SendAuthenticationTypes.cs | 50 + .../Interfaces/ISendAuthenticationQuery.cs | 20 + .../Queries/SendAuthenticationQuery.cs | 53 + .../SendServiceCollectionExtension.cs | 3 + .../dbo/Stored Procedures/Send_Create.sql | 11 +- .../dbo/Stored Procedures/Send_Update.sql | 6 +- src/Sql/Tools/dbo/Tables/Send.sql | 1 + .../Services/SendAuthenticationQueryTests.cs | 135 + .../DbScripts/2025-05-20_00_AddSendEmails.sql | 139 + ...18_2025-05-20_00_AddSendEmails.Designer.cs | 3119 ++++++++++++++++ ...50522205018_2025-05-20_00_AddSendEmails.cs | 29 + .../DatabaseContextModelSnapshot.cs | 4 + ...09_2025-05-20_00_AddSendEmails.Designer.cs | 3125 +++++++++++++++++ ...50520201209_2025-05-20_00_AddSendEmails.cs | 28 + .../DatabaseContextModelSnapshot.cs | 4 + ...16_2025-05-20_00_AddSendEmails.Designer.cs | 3108 ++++++++++++++++ ...50520201216_2025-05-20_00_AddSendEmails.cs | 28 + .../DatabaseContextModelSnapshot.cs | 4 + 19 files changed, 9874 insertions(+), 5 deletions(-) create mode 100644 src/Core/Tools/Models/Data/SendAuthenticationTypes.cs create mode 100644 src/Core/Tools/SendFeatures/Queries/Interfaces/ISendAuthenticationQuery.cs create mode 100644 src/Core/Tools/SendFeatures/Queries/SendAuthenticationQuery.cs create mode 100644 test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs create mode 100644 util/Migrator/DbScripts/2025-05-20_00_AddSendEmails.sql create mode 100644 util/MySqlMigrations/Migrations/20250522205018_2025-05-20_00_AddSendEmails.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20250522205018_2025-05-20_00_AddSendEmails.cs create mode 100644 util/PostgresMigrations/Migrations/20250520201209_2025-05-20_00_AddSendEmails.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20250520201209_2025-05-20_00_AddSendEmails.cs create mode 100644 util/SqliteMigrations/Migrations/20250520201216_2025-05-20_00_AddSendEmails.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20250520201216_2025-05-20_00_AddSendEmails.cs diff --git a/src/Core/Tools/Entities/Send.cs b/src/Core/Tools/Entities/Send.cs index 93fa7a37c0..34b68fd6c5 100644 --- a/src/Core/Tools/Entities/Send.cs +++ b/src/Core/Tools/Entities/Send.cs @@ -60,9 +60,21 @@ public class Send : ITableObject /// /// Password provided by the user. Protected with pbkdf2. /// + /// + /// This field is mutually exclusive with + /// [MaxLength(300)] public string? Password { get; set; } + /// + /// Comma-separated list of emails for OTP authentication. + /// + /// + /// This field is mutually exclusive with + /// + [MaxLength(1024)] + public string? Emails { get; set; } + ///
    /// The send becomes unavailable to API callers when /// >= . diff --git a/src/Core/Tools/Models/Data/SendAuthenticationTypes.cs b/src/Core/Tools/Models/Data/SendAuthenticationTypes.cs new file mode 100644 index 0000000000..9ce477ed0c --- /dev/null +++ b/src/Core/Tools/Models/Data/SendAuthenticationTypes.cs @@ -0,0 +1,50 @@ +#nullable enable + +namespace Bit.Core.Tools.Models.Data; + +/// +/// A discriminated union for send authentication. +/// +/// +/// const method : SendAuthenticationMethod; +/// // other variable definitions omitted +/// +/// var token = method switch +/// { +/// NotAuthenticated => issueTokenFor(sendId), +/// ResourcePassword(var expected) => tryIssueTokenFor(sendId, expected, actual), +/// EmailOtp(_) => tryIssueTokenFor(sendId, email, actualOtp), +/// _ => throw new Exception() +/// }; +/// +public abstract record SendAuthenticationMethod; + +/// +/// Never issue a send claim. +/// +/// +/// This claim is issued when a send does not exist or when a send +/// has exceeded its max access attempts. +/// +public record NeverAuthenticate : SendAuthenticationMethod; + +/// +/// Create a send claim automatically. +/// +public record NotAuthenticated : SendAuthenticationMethod; + +/// +/// Create a send claim by requesting a password confirmation hash. +/// +/// +/// A base64 encoded hash that permits access to the send. +/// +public record ResourcePassword(string Hash) : SendAuthenticationMethod; + +/// +/// Create a send claim by requesting a one time password (OTP) confirmation code. +/// +/// +/// The list of email addresses permitted access to the send. +/// +public record EmailOtp(string[] Emails) : SendAuthenticationMethod; diff --git a/src/Core/Tools/SendFeatures/Queries/Interfaces/ISendAuthenticationQuery.cs b/src/Core/Tools/SendFeatures/Queries/Interfaces/ISendAuthenticationQuery.cs new file mode 100644 index 0000000000..f7e5f7022c --- /dev/null +++ b/src/Core/Tools/SendFeatures/Queries/Interfaces/ISendAuthenticationQuery.cs @@ -0,0 +1,20 @@ +using Bit.Core.Tools.Models.Data; + +#nullable enable + +namespace Bit.Core.Tools.SendFeatures.Queries.Interfaces; + +/// +/// Integration with authentication layer for generating send access claims. +/// +public interface ISendAuthenticationQuery +{ + /// + /// Retrieves the authentication method of a Send. + /// + /// Identifies the send to inspect. + /// + /// The authentication method that should be performed for the send. + /// + Task GetAuthenticationMethod(Guid sendId); +} diff --git a/src/Core/Tools/SendFeatures/Queries/SendAuthenticationQuery.cs b/src/Core/Tools/SendFeatures/Queries/SendAuthenticationQuery.cs new file mode 100644 index 0000000000..fed7c9e8d4 --- /dev/null +++ b/src/Core/Tools/SendFeatures/Queries/SendAuthenticationQuery.cs @@ -0,0 +1,53 @@ +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.Repositories; +using Bit.Core.Tools.SendFeatures.Queries.Interfaces; + +#nullable enable + +namespace Bit.Core.Tools.SendFeatures.Queries; + +/// +public class SendAuthenticationQuery : ISendAuthenticationQuery +{ + private static readonly NotAuthenticated NOT_AUTHENTICATED = new NotAuthenticated(); + private static readonly NeverAuthenticate NEVER_AUTHENTICATE = new NeverAuthenticate(); + + private readonly ISendRepository _sendRepository; + + /// + /// Instantiates the command + /// + /// + /// Retrieves send records + /// + /// + /// Thrown when is . + /// + public SendAuthenticationQuery(ISendRepository sendRepository) + { + _sendRepository = sendRepository ?? throw new ArgumentNullException(nameof(sendRepository)); + } + + /// + public async Task GetAuthenticationMethod(Guid sendId) + { + var send = await _sendRepository.GetByIdAsync(sendId); + + SendAuthenticationMethod method = send switch + { + null => NEVER_AUTHENTICATE, + var s when s.AccessCount >= s.MaxAccessCount => NEVER_AUTHENTICATE, + var s when s.Emails is not null => emailOtp(s.Emails), + var s when s.Password is not null => new ResourcePassword(s.Password), + _ => NOT_AUTHENTICATED + }; + + return method; + } + + private EmailOtp emailOtp(string emails) + { + var list = emails.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + return new EmailOtp(list); + } +} diff --git a/src/Core/Tools/SendFeatures/SendServiceCollectionExtension.cs b/src/Core/Tools/SendFeatures/SendServiceCollectionExtension.cs index 02327adaac..3dca1cb482 100644 --- a/src/Core/Tools/SendFeatures/SendServiceCollectionExtension.cs +++ b/src/Core/Tools/SendFeatures/SendServiceCollectionExtension.cs @@ -1,5 +1,7 @@ using Bit.Core.Tools.SendFeatures.Commands; using Bit.Core.Tools.SendFeatures.Commands.Interfaces; +using Bit.Core.Tools.SendFeatures.Queries; +using Bit.Core.Tools.SendFeatures.Queries.Interfaces; using Bit.Core.Tools.Services; using Microsoft.Extensions.DependencyInjection; @@ -14,5 +16,6 @@ public static class SendServiceCollectionExtension services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); } } diff --git a/src/Sql/Tools/dbo/Stored Procedures/Send_Create.sql b/src/Sql/Tools/dbo/Stored Procedures/Send_Create.sql index 396c619ba4..58375a0397 100644 --- a/src/Sql/Tools/dbo/Stored Procedures/Send_Create.sql +++ b/src/Sql/Tools/dbo/Stored Procedures/Send_Create.sql @@ -14,7 +14,10 @@ @DeletionDate DATETIME2(7), @Disabled BIT, @HideEmail BIT, - @CipherId UNIQUEIDENTIFIER = NULL + @CipherId UNIQUEIDENTIFIER = NULL, +-- FIXME: remove null default value once this argument has been +-- in 2 server releases + @Emails NVARCHAR(1024) = NULL AS BEGIN SET NOCOUNT ON @@ -36,7 +39,8 @@ BEGIN [DeletionDate], [Disabled], [HideEmail], - [CipherId] + [CipherId], + [Emails] ) VALUES ( @@ -55,7 +59,8 @@ BEGIN @DeletionDate, @Disabled, @HideEmail, - @CipherId + @CipherId, + @Emails ) IF @UserId IS NOT NULL diff --git a/src/Sql/Tools/dbo/Stored Procedures/Send_Update.sql b/src/Sql/Tools/dbo/Stored Procedures/Send_Update.sql index e975e18ad9..43873b4b88 100644 --- a/src/Sql/Tools/dbo/Stored Procedures/Send_Update.sql +++ b/src/Sql/Tools/dbo/Stored Procedures/Send_Update.sql @@ -14,7 +14,8 @@ @DeletionDate DATETIME2(7), @Disabled BIT, @HideEmail BIT, - @CipherId UNIQUEIDENTIFIER = NULL + @CipherId UNIQUEIDENTIFIER = NULL, + @Emails NVARCHAR(1024) = NULL AS BEGIN SET NOCOUNT ON @@ -36,7 +37,8 @@ BEGIN [DeletionDate] = @DeletionDate, [Disabled] = @Disabled, [HideEmail] = @HideEmail, - [CipherId] = @CipherId + [CipherId] = @CipherId, + [Emails] = @Emails WHERE [Id] = @Id diff --git a/src/Sql/Tools/dbo/Tables/Send.sql b/src/Sql/Tools/dbo/Tables/Send.sql index 71acf4a9f3..2130dbc07e 100644 --- a/src/Sql/Tools/dbo/Tables/Send.sql +++ b/src/Sql/Tools/dbo/Tables/Send.sql @@ -6,6 +6,7 @@ [Data] VARCHAR(MAX) NOT NULL, [Key] VARCHAR (MAX) NOT NULL, [Password] NVARCHAR (300) NULL, + [Emails] NVARCHAR (1024) NULL, [MaxAccessCount] INT NULL, [AccessCount] INT NOT NULL, [CreationDate] DATETIME2 (7) NOT NULL, diff --git a/test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs b/test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs new file mode 100644 index 0000000000..c34afc42bd --- /dev/null +++ b/test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs @@ -0,0 +1,135 @@ +using Bit.Core.Tools.Entities; +using Bit.Core.Tools.Models.Data; +using Bit.Core.Tools.Repositories; +using Bit.Core.Tools.SendFeatures.Queries; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Tools.Services; + +public class SendAuthenticationQueryTests +{ + private readonly ISendRepository _sendRepository; + private readonly SendAuthenticationQuery _sendAuthenticationQuery; + + public SendAuthenticationQueryTests() + { + _sendRepository = Substitute.For(); + _sendAuthenticationQuery = new SendAuthenticationQuery(_sendRepository); + } + + [Fact] + public void Constructor_WithNullRepository_ThrowsArgumentNullException() + { + // Act & Assert + var exception = Assert.Throws(() => new SendAuthenticationQuery(null)); + Assert.Equal("sendRepository", exception.ParamName); + } + + [Theory] + [MemberData(nameof(AuthenticationMethodTestCases))] + public async Task GetAuthenticationMethod_ReturnsExpectedAuthenticationMethod(Send? send, Type expectedType) + { + // Arrange + var sendId = Guid.NewGuid(); + _sendRepository.GetByIdAsync(sendId).Returns(send); + + // Act + var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId); + + // Assert + Assert.IsType(expectedType, result); + } + + [Theory] + [MemberData(nameof(EmailParsingTestCases))] + public async Task GetAuthenticationMethod_WithEmails_ParsesEmailsCorrectly(string emailString, string[] expectedEmails) + { + // Arrange + var sendId = Guid.NewGuid(); + var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: emailString, password: null); + _sendRepository.GetByIdAsync(sendId).Returns(send); + + // Act + var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId); + + // Assert + var emailOtp = Assert.IsType(result); + Assert.Equal(expectedEmails, emailOtp.Emails); + } + + [Fact] + public async Task GetAuthenticationMethod_WithBothEmailsAndPassword_ReturnsEmailOtp() + { + // Arrange + var sendId = Guid.NewGuid(); + var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: "test@example.com", password: "hashedpassword"); + _sendRepository.GetByIdAsync(sendId).Returns(send); + + // Act + var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId); + + // Assert + Assert.IsType(result); + } + + [Fact] + public async Task GetAuthenticationMethod_CallsRepositoryWithCorrectSendId() + { + // Arrange + var sendId = Guid.NewGuid(); + var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: null); + _sendRepository.GetByIdAsync(sendId).Returns(send); + + // Act + await _sendAuthenticationQuery.GetAuthenticationMethod(sendId); + + // Assert + await _sendRepository.Received(1).GetByIdAsync(sendId); + } + + [Fact] + public async Task GetAuthenticationMethod_WhenRepositoryThrows_PropagatesException() + { + // Arrange + var sendId = Guid.NewGuid(); + var expectedException = new InvalidOperationException("Repository error"); + _sendRepository.GetByIdAsync(sendId).Returns(Task.FromException(expectedException)); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => + _sendAuthenticationQuery.GetAuthenticationMethod(sendId)); + Assert.Same(expectedException, exception); + } + + public static IEnumerable AuthenticationMethodTestCases() + { + yield return new object[] { null, typeof(NeverAuthenticate) }; + yield return new object[] { CreateSend(accessCount: 5, maxAccessCount: 5, emails: null, password: null), typeof(NeverAuthenticate) }; + yield return new object[] { CreateSend(accessCount: 6, maxAccessCount: 5, emails: null, password: null), typeof(NeverAuthenticate) }; + yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: "test@example.com", password: null), typeof(EmailOtp) }; + yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: "hashedpassword"), typeof(ResourcePassword) }; + yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: null), typeof(NotAuthenticated) }; + } + + public static IEnumerable EmailParsingTestCases() + { + yield return new object[] { "test@example.com", new[] { "test@example.com" } }; + yield return new object[] { "test1@example.com,test2@example.com", new[] { "test1@example.com", "test2@example.com" } }; + yield return new object[] { " test@example.com , other@example.com ", new[] { "test@example.com", "other@example.com" } }; + yield return new object[] { "test@example.com,,other@example.com", new[] { "test@example.com", "other@example.com" } }; + yield return new object[] { " , test@example.com, ,other@example.com, ", new[] { "test@example.com", "other@example.com" } }; + } + + private static Send CreateSend(int accessCount, int? maxAccessCount, string? emails, string? password) + { + return new Send + { + Id = Guid.NewGuid(), + AccessCount = accessCount, + MaxAccessCount = maxAccessCount, + Emails = emails, + Password = password + }; + } +} diff --git a/util/Migrator/DbScripts/2025-05-20_00_AddSendEmails.sql b/util/Migrator/DbScripts/2025-05-20_00_AddSendEmails.sql new file mode 100644 index 0000000000..25969c1018 --- /dev/null +++ b/util/Migrator/DbScripts/2025-05-20_00_AddSendEmails.sql @@ -0,0 +1,139 @@ +-- Add `Emails` field that stores a comma-separated list of email addresses for +-- email/OTP authentication to table and write methods. The read methods +-- don't need to be updated because they all use `*`. +IF NOT EXISTS( + SELECT * + FROM [sys].[columns] + WHERE [object_id] = OBJECT_ID(N'[dbo].[Send]') + AND [name] = 'Emails') +BEGIN + ALTER TABLE [dbo].[Send] ADD [Emails] NVARCHAR(1024) NULL; +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Send_Update] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data VARCHAR(MAX), + @Key VARCHAR(MAX), + @Password NVARCHAR(300), + @MaxAccessCount INT, + @AccessCount INT, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @ExpirationDate DATETIME2(7), + @DeletionDate DATETIME2(7), + @Disabled BIT, + @HideEmail BIT, + @CipherId UNIQUEIDENTIFIER = NULL, + @Emails NVARCHAR(1024) = NULL +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[Send] + SET + [UserId] = @UserId, + [OrganizationId] = @OrganizationId, + [Type] = @Type, + [Data] = @Data, + [Key] = @Key, + [Password] = @Password, + [MaxAccessCount] = @MaxAccessCount, + [AccessCount] = @AccessCount, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [ExpirationDate] = @ExpirationDate, + [DeletionDate] = @DeletionDate, + [Disabled] = @Disabled, + [HideEmail] = @HideEmail, + [CipherId] = @CipherId, + [Emails] = @Emails + WHERE + [Id] = @Id + + IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END + -- TODO: OrganizationId bump? +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[Send_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Data VARCHAR(MAX), + @Key VARCHAR(MAX), + @Password NVARCHAR(300), + @MaxAccessCount INT, + @AccessCount INT, + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @ExpirationDate DATETIME2(7), + @DeletionDate DATETIME2(7), + @Disabled BIT, + @HideEmail BIT, + @CipherId UNIQUEIDENTIFIER = NULL, + @Emails NVARCHAR(1024) = NULL +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[Send] + ( + [Id], + [UserId], + [OrganizationId], + [Type], + [Data], + [Key], + [Password], + [MaxAccessCount], + [AccessCount], + [CreationDate], + [RevisionDate], + [ExpirationDate], + [DeletionDate], + [Disabled], + [HideEmail], + [CipherId], + [Emails] + ) + VALUES + ( + @Id, + @UserId, + @OrganizationId, + @Type, + @Data, + @Key, + @Password, + @MaxAccessCount, + @AccessCount, + @CreationDate, + @RevisionDate, + @ExpirationDate, + @DeletionDate, + @Disabled, + @HideEmail, + @CipherId, + @Emails + ) + + IF @UserId IS NOT NULL + BEGIN + IF @Type = 1 --File + BEGIN + EXEC [dbo].[User_UpdateStorage] @UserId + END + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END + -- TODO: OrganizationId bump? +END +GO diff --git a/util/MySqlMigrations/Migrations/20250522205018_2025-05-20_00_AddSendEmails.Designer.cs b/util/MySqlMigrations/Migrations/20250522205018_2025-05-20_00_AddSendEmails.Designer.cs new file mode 100644 index 0000000000..b8ea87b21e --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250522205018_2025-05-20_00_AddSendEmails.Designer.cs @@ -0,0 +1,3119 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20250522205018_2025-05-20_00_AddSendEmails")] + partial class _20250520_00_AddSendEmails + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .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("LimitCollectionCreation") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("LimitItemDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("int"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property("Plan") + .IsRequired() + .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("SmSeats") + .HasColumnType("int"); + + b.Property("SmServiceAccounts") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseCustomPermissions") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UseOrganizationDomains") + .HasColumnType("tinyint(1)"); + + b.Property("UsePasswordManager") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property("UseRiskInsights") + .HasColumnType("tinyint(1)"); + + b.Property("UseScim") + .HasColumnType("tinyint(1)"); + + b.Property("UseSecretsManager") + .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.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Template") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.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") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasColumnType("longtext"); + + b.Property("BillingPhone") + .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("DiscountId") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasColumnType("longtext"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("varchar(25)"); + + b.Property("Approved") + .HasColumnType("tinyint(1)"); + + b.Property("AuthenticationDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MasterPasswordHash") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ResponseDate") + .HasColumnType("datetime(6)"); + + b.Property("ResponseDeviceId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ConsumedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AaGuid") + .HasColumnType("char(36)"); + + b.Property("Counter") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SupportsPrf") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AssignedSeats") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Total") + .HasColumnType("decimal(65,30)"); + + b.Property("UsedSeats") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllocatedSeats") + .HasColumnType("int"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("PurchasedSeats") + .HasColumnType("int"); + + b.Property("SeatMinimum") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("varchar(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAtTime") + .HasColumnType("datetime(6)"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longblob"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.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") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Active") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("longtext"); + + b.Property("EncryptedPublicKey") + .HasColumnType("longtext"); + + b.Property("EncryptedUserKey") + .HasColumnType("longtext"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .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("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.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("DomainName") + .HasColumnType("longtext"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InstallationId") + .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("SecretId") + .HasColumnType("char(36)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SystemUser") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + 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("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("JobRunCount") + .HasColumnType("int"); + + b.Property("LastCheckedDate") + .HasColumnType("datetime(6)"); + + b.Property("NextRunDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VerifiedDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("IsAdminInitiated") + .HasColumnType("tinyint(1)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessSecretsManager") + .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("smallint"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + 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("Emails") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + + 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("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .IsRequired() + .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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.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("ProviderId") + .HasColumnType("char(36)"); + + 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("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.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("AvatarColor") + .HasMaxLength(7) + .HasColumnType("varchar(7)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .IsRequired() + .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("FailedLoginCount") + .HasColumnType("int"); + + 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("KdfMemory") + .HasColumnType("int"); + + b.Property("KdfParallelism") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LastEmailChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastFailedLoginDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKdfChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("datetime(6)"); + + 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("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("VerifyDevices") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("varchar(3000)"); + + b.Property("ClientType") + .HasColumnType("tinyint unsigned"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Global") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Priority") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("TaskId") + .HasColumnType("char(36)"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("NotificationId") + .HasColumnType("char(36)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("ReadDate") + .HasColumnType("datetime(6)"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("LastActivityDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar(34)"); + + b.Property("Read") + .HasColumnType("tinyint(1)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Write") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpireAt") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Uri") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.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("Key") + .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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("char(36)"); + + b.Property("SecretsId") + .HasColumnType("char(36)"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/MySqlMigrations/Migrations/20250522205018_2025-05-20_00_AddSendEmails.cs b/util/MySqlMigrations/Migrations/20250522205018_2025-05-20_00_AddSendEmails.cs new file mode 100644 index 0000000000..59669b9566 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250522205018_2025-05-20_00_AddSendEmails.cs @@ -0,0 +1,29 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class _20250520_00_AddSendEmails : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Emails", + table: "Send", + type: "varchar(1024)", + maxLength: 1024, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Emails", + table: "Send"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index 98768e0447..0dd533b8d9 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1437,6 +1437,10 @@ namespace Bit.MySqlMigrations.Migrations b.Property("Disabled") .HasColumnType("tinyint(1)"); + b.Property("Emails") + .HasMaxLength(1024) + .HasColumnType("varchar(1024)"); + b.Property("ExpirationDate") .HasColumnType("datetime(6)"); diff --git a/util/PostgresMigrations/Migrations/20250520201209_2025-05-20_00_AddSendEmails.Designer.cs b/util/PostgresMigrations/Migrations/20250520201209_2025-05-20_00_AddSendEmails.Designer.cs new file mode 100644 index 0000000000..9746663eab --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250520201209_2025-05-20_00_AddSendEmails.Designer.cs @@ -0,0 +1,3125 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20250520201209_2025-05-20_00_AddSendEmails")] + partial class _20250520_00_AddSendEmails + { + /// + 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("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .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 with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with 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("LimitCollectionCreation") + .HasColumnType("boolean"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("boolean"); + + b.Property("LimitItemDeletion") + .HasColumnType("boolean"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("integer"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp with time zone"); + + b.Property("Plan") + .IsRequired() + .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 with time zone"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("SelfHost") + .HasColumnType("boolean"); + + b.Property("SmSeats") + .HasColumnType("integer"); + + b.Property("SmServiceAccounts") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseCustomPermissions") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UseOrganizationDomains") + .HasColumnType("boolean"); + + b.Property("UsePasswordManager") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .HasColumnType("boolean"); + + b.Property("UseRiskInsights") + .HasColumnType("boolean"); + + b.Property("UseScim") + .HasColumnType("boolean"); + + b.Property("UseSecretsManager") + .HasColumnType("boolean"); + + b.Property("UseSso") + .HasColumnType("boolean"); + + b.Property("UseTotp") + .HasColumnType("boolean"); + + b.Property("UsersGetPremium") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Id", "Enabled"), new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Template") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasColumnType("text"); + + b.Property("BillingPhone") + .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 with time zone"); + + b.Property("DiscountId") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasColumnType("text"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with 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 with 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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.Property("Approved") + .HasColumnType("boolean"); + + b.Property("AuthenticationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MasterPasswordHash") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("smallint"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ResponseDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ResponseDeviceId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with 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 with time zone"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with 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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ConsumedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with 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") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("OrganizationId", "ExternalId"), new[] { "UserId" }); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AaGuid") + .HasColumnType("uuid"); + + b.Property("Counter") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SupportsPrf") + .HasColumnType("boolean"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssignedSeats") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UsedSeats") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllocatedSeats") + .HasColumnType("integer"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("PurchasedSeats") + .HasColumnType("integer"); + + b.Property("SeatMinimum") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("character varying(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAtTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("text"); + + b.Property("EncryptedPublicKey") + .HasColumnType("text"); + + b.Property("EncryptedUserKey") + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.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 with time zone"); + + b.Property("DeviceType") + .HasColumnType("smallint"); + + b.Property("DomainName") + .HasColumnType("text"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("InstallationId") + .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("SecretId") + .HasColumnType("uuid"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SystemUser") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("JobRunCount") + .HasColumnType("integer"); + + b.Property("LastCheckedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextRunDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerifiedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAdminInitiated") + .HasColumnType("boolean"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessSecretsManager") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp with 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 with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("Emails") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with 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 with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .IsRequired() + .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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp with 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("ProviderId") + .HasColumnType("uuid"); + + 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("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("character varying(7)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Culture") + .IsRequired() + .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("FailedLoginCount") + .HasColumnType("integer"); + + 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("KdfMemory") + .HasColumnType("integer"); + + b.Property("KdfParallelism") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LastEmailChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastFailedLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKdfChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("timestamp with time zone"); + + 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 with time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with 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("UsesKeyConnector") + .HasColumnType("boolean"); + + b.Property("VerifyDevices") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("character varying(3000)"); + + b.Property("ClientType") + .HasColumnType("smallint"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Global") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Priority") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskId") + .HasColumnType("uuid"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("NotificationId") + .HasColumnType("uuid"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReadDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("Read") + .HasColumnType("boolean"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Write") + .HasColumnType("boolean"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Uri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("uuid"); + + b.Property("SecretsId") + .HasColumnType("uuid"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/PostgresMigrations/Migrations/20250520201209_2025-05-20_00_AddSendEmails.cs b/util/PostgresMigrations/Migrations/20250520201209_2025-05-20_00_AddSendEmails.cs new file mode 100644 index 0000000000..d99a2b63d9 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250520201209_2025-05-20_00_AddSendEmails.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class _20250520_00_AddSendEmails : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Emails", + table: "Send", + type: "character varying(1024)", + maxLength: 1024, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Emails", + table: "Send"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index 736f01c95a..5f9377d99d 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1442,6 +1442,10 @@ namespace Bit.PostgresMigrations.Migrations b.Property("Disabled") .HasColumnType("boolean"); + b.Property("Emails") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)"); + b.Property("ExpirationDate") .HasColumnType("timestamp with time zone"); diff --git a/util/SqliteMigrations/Migrations/20250520201216_2025-05-20_00_AddSendEmails.Designer.cs b/util/SqliteMigrations/Migrations/20250520201216_2025-05-20_00_AddSendEmails.Designer.cs new file mode 100644 index 0000000000..4a3c95d89b --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250520201216_2025-05-20_00_AddSendEmails.Designer.cs @@ -0,0 +1,3108 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20250520201216_2025-05-20_00_AddSendEmails")] + partial class _20250520_00_AddSendEmails + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LimitCollectionCreation") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("INTEGER"); + + b.Property("LimitItemDeletion") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("MaxCollections") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("TEXT"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("SelfHost") + .HasColumnType("INTEGER"); + + b.Property("SmSeats") + .HasColumnType("INTEGER"); + + b.Property("SmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("Use2fa") + .HasColumnType("INTEGER"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("INTEGER"); + + b.Property("UseApi") + .HasColumnType("INTEGER"); + + b.Property("UseCustomPermissions") + .HasColumnType("INTEGER"); + + b.Property("UseDirectory") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.Property("UseGroups") + .HasColumnType("INTEGER"); + + b.Property("UseKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("UseOrganizationDomains") + .HasColumnType("INTEGER"); + + b.Property("UsePasswordManager") + .HasColumnType("INTEGER"); + + b.Property("UsePolicies") + .HasColumnType("INTEGER"); + + b.Property("UseResetPassword") + .HasColumnType("INTEGER"); + + b.Property("UseRiskInsights") + .HasColumnType("INTEGER"); + + b.Property("UseScim") + .HasColumnType("INTEGER"); + + b.Property("UseSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("UseSso") + .HasColumnType("INTEGER"); + + b.Property("UseTotp") + .HasColumnType("INTEGER"); + + b.Property("UsersGetPremium") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Template") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BillingEmail") + .HasColumnType("TEXT"); + + b.Property("BillingPhone") + .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("TEXT"); + + b.Property("DiscountId") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("TEXT"); + + b.Property("Approved") + .HasColumnType("INTEGER"); + + b.Property("AuthenticationDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHash") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceType") + .HasColumnType("INTEGER"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ResponseDate") + .HasColumnType("TEXT"); + + b.Property("ResponseDeviceId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("GranteeId") + .HasColumnType("TEXT"); + + b.Property("GrantorId") + .HasColumnType("TEXT"); + + b.Property("KeyEncrypted") + .HasColumnType("TEXT"); + + b.Property("LastNotificationDate") + .HasColumnType("TEXT"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WaitTimeDays") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ConsumedDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AaGuid") + .HasColumnType("TEXT"); + + b.Property("Counter") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SupportsPrf") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AssignedSeats") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("TEXT"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Total") + .HasColumnType("TEXT"); + + b.Property("UsedSeats") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllocatedSeats") + .HasColumnType("INTEGER"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("PurchasedSeats") + .HasColumnType("INTEGER"); + + b.Property("SeatMinimum") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("TEXT"); + + b.Property("AbsoluteExpiration") + .HasColumnType("TEXT"); + + b.Property("ExpiresAtTime") + .HasColumnType("TEXT"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("BLOB"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasColumnType("TEXT"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActingUserId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasColumnType("INTEGER"); + + b.Property("DomainName") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("PolicyId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderUserId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SystemUser") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Config") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("JobRunCount") + .HasColumnType("INTEGER"); + + b.Property("LastCheckedDate") + .HasColumnType("TEXT"); + + b.Property("NextRunDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VerifiedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("IsAdminInitiated") + .HasColumnType("INTEGER"); + + b.Property("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PlanSponsorshipType") + .HasColumnType("INTEGER"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("ToDelete") + .HasColumnType("INTEGER"); + + b.Property("ValidUntil") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletionDate") + .HasColumnType("TEXT"); + + b.Property("Disabled") + .HasColumnType("INTEGER"); + + b.Property("Emails") + .HasMaxLength(1024) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("HideEmail") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MaxAccessCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Rate") + .HasColumnType("TEXT"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaymentMethodType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Refunded") + .HasColumnType("INTEGER"); + + b.Property("RefundedAmount") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountRevisionDate") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailVerified") + .HasColumnType("INTEGER"); + + b.Property("EquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("FailedLoginCount") + .HasColumnType("INTEGER"); + + b.Property("ForcePasswordReset") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Kdf") + .HasColumnType("INTEGER"); + + b.Property("KdfIterations") + .HasColumnType("INTEGER"); + + b.Property("KdfMemory") + .HasColumnType("INTEGER"); + + b.Property("KdfParallelism") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("LastEmailChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastFailedLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastKdfChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Premium") + .HasColumnType("INTEGER"); + + b.Property("PremiumExpirationDate") + .HasColumnType("TEXT"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("VerifyDevices") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("TEXT"); + + b.Property("ClientType") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Global") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("TaskId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("NotificationId") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("ReadDate") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("TEXT"); + + b.Property("Read") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Write") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpireAt") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Attachments") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Favorites") + .HasColumnType("TEXT"); + + b.Property("Folders") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Reprompt") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("TEXT"); + + b.Property("SecretsId") + .HasColumnType("TEXT"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/SqliteMigrations/Migrations/20250520201216_2025-05-20_00_AddSendEmails.cs b/util/SqliteMigrations/Migrations/20250520201216_2025-05-20_00_AddSendEmails.cs new file mode 100644 index 0000000000..a7e9393a2d --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250520201216_2025-05-20_00_AddSendEmails.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class _20250520_00_AddSendEmails : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "Emails", + table: "Send", + type: "TEXT", + maxLength: 1024, + nullable: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "Emails", + table: "Send"); + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index 1bc1ffbc58..428ad4bfa3 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -1426,6 +1426,10 @@ namespace Bit.SqliteMigrations.Migrations b.Property("Disabled") .HasColumnType("INTEGER"); + b.Property("Emails") + .HasMaxLength(1024) + .HasColumnType("TEXT"); + b.Property("ExpirationDate") .HasColumnType("TEXT"); From f3e637cf2db12200eb4cfe138e9ff940a8a8c645 Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Tue, 27 May 2025 08:28:50 -0400 Subject: [PATCH 095/114] [PM-17562] Add support for retries on event integrations (#5795) * [PM-17562] Add support for retires on event integrations * Add additional test coverage * Fixed missing await call * Remove debug organization id * Respond to PR feedback * Change NotBeforeUtc to DelayUntilDate. Adjust comments. * Respond to PR feedback --- .../Controllers/SlackIntegrationController.cs | 2 +- ...ionIntegrationConfigurationRequestModel.cs | 2 +- src/Api/Startup.cs | 16 +- .../AdminConsole/Enums/IntegrationType.cs | 16 ++ .../Data/Integrations/IIntegrationMessage.cs | 12 + .../Integrations/IntegrationHandlerResult.cs | 16 ++ .../Data/Integrations/IntegrationMessage.cs | 34 +++ .../IntegrationTemplateContext.cs | 9 +- .../Data/Integrations/SlackIntegration.cs | 2 +- .../SlackIntegrationConfiguration.cs | 2 +- .../SlackIntegrationConfigurationDetails.cs | 2 +- .../WebhookIntegrationConfiguration.cs | 2 +- .../WebhookIntegrationConfigurationDetails.cs | 3 + .../WebhookIntegrationConfigurationDetils.cs | 3 - .../Services/IIntegrationHandler.cs | 24 ++ .../Services/IIntegrationPublisher.cs | 8 + .../EventIntegrationHandler.cs | 83 +++++++ .../IntegrationEventHandlerBase.cs | 2 +- .../RabbitMqEventListenerService.cs | 2 +- .../RabbitMqEventWriteService.cs | 2 +- .../RabbitMqIntegrationListenerService.cs | 191 +++++++++++++++ .../RabbitMqIntegrationPublisher.cs | 54 +++++ .../Implementations/SlackEventHandler.cs | 2 +- .../SlackIntegrationHandler.cs | 19 ++ .../Implementations/WebhookEventHandler.cs | 4 +- .../WebhookIntegrationHandler.cs | 61 +++++ .../Utilities/IntegrationTemplateProcessor.cs | 6 +- src/Core/Settings/GlobalSettings.cs | 25 +- src/Events/Startup.cs | 78 +----- src/EventsProcessor/Startup.cs | 49 +--- .../Utilities/ServiceCollectionExtensions.cs | 225 +++++++++++++++--- ...ntegrationsConfigurationControllerTests.cs | 2 +- ...tegrationConfigurationRequestModelTests.cs | 2 +- .../Integrations/IntegrationMessageTests.cs | 53 +++++ .../Services/EventIntegrationHandlerTests.cs | 212 +++++++++++++++++ .../Services/IntegrationHandlerTests.cs | 41 ++++ .../Services/IntegrationTypeTests.cs | 30 +++ .../Services/SlackIntegrationHandlerTests.cs | 42 ++++ .../WebhookIntegrationHandlerTests.cs | 139 +++++++++++ .../IntegrationTemplateProcessorTests.cs | 16 +- 40 files changed, 1277 insertions(+), 216 deletions(-) create mode 100644 src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs create mode 100644 src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs create mode 100644 src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs create mode 100644 src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs delete mode 100644 src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetils.cs create mode 100644 src/Core/AdminConsole/Services/IIntegrationHandler.cs create mode 100644 src/Core/AdminConsole/Services/IIntegrationPublisher.cs create mode 100644 src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs create mode 100644 src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs create mode 100644 src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationPublisher.cs create mode 100644 src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs create mode 100644 src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs create mode 100644 test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs create mode 100644 test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs create mode 100644 test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs create mode 100644 test/Core.Test/AdminConsole/Services/IntegrationTypeTests.cs create mode 100644 test/Core.Test/AdminConsole/Services/SlackIntegrationHandlerTests.cs create mode 100644 test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs diff --git a/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs b/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs index a8bef10dc6..c0ab5c059b 100644 --- a/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs +++ b/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs @@ -2,10 +2,10 @@ using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Core; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Data.Integrations; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Models.Data.Integrations; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs index 6566760e17..ccab2b36ae 100644 --- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs +++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs @@ -1,8 +1,8 @@ using System.ComponentModel.DataAnnotations; using System.Text.Json; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Data.Integrations; using Bit.Core.Enums; -using Bit.Core.Models.Data.Integrations; #nullable enable diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index 68949b052b..e24f96a7a9 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -28,10 +28,8 @@ using Bit.Core.Tools.Entities; using Bit.Core.Vault.Entities; using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Api.Billing; -using Bit.Core.AdminConsole.Services.NoopImplementations; using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Identity.TokenProviders; -using Bit.Core.Services; using Bit.Core.Tools.ImportFeatures; using Bit.Core.Tools.ReportFeatures; using Bit.Core.Auth.Models.Api.Request; @@ -224,18 +222,8 @@ public class Startup services.AddHostedService(); } - // Slack - if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) && - CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) && - CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes)) - { - services.AddHttpClient(SlackService.HttpClientName); - services.AddSingleton(); - } - else - { - services.AddSingleton(); - } + // Add SlackService for OAuth API requests - if configured + services.AddSlackService(globalSettings); } public void Configure( diff --git a/src/Core/AdminConsole/Enums/IntegrationType.cs b/src/Core/AdminConsole/Enums/IntegrationType.cs index 0f5123554e..5edd54df23 100644 --- a/src/Core/AdminConsole/Enums/IntegrationType.cs +++ b/src/Core/AdminConsole/Enums/IntegrationType.cs @@ -7,3 +7,19 @@ public enum IntegrationType : int Slack = 3, Webhook = 4, } + +public static class IntegrationTypeExtensions +{ + public static string ToRoutingKey(this IntegrationType type) + { + switch (type) + { + case IntegrationType.Slack: + return "slack"; + case IntegrationType.Webhook: + return "webhook"; + default: + throw new ArgumentOutOfRangeException(nameof(type), $"Unsupported integration type: {type}"); + } + } +} diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs b/src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs new file mode 100644 index 0000000000..bd1f280cad --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs @@ -0,0 +1,12 @@ +using Bit.Core.Enums; + +namespace Bit.Core.AdminConsole.Models.Data.Integrations; + +public interface IIntegrationMessage +{ + IntegrationType IntegrationType { get; } + int RetryCount { get; set; } + DateTime? DelayUntilDate { get; set; } + void ApplyRetry(DateTime? handlerDelayUntilDate); + string ToJson(); +} diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs new file mode 100644 index 0000000000..d2f0bde693 --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs @@ -0,0 +1,16 @@ +namespace Bit.Core.AdminConsole.Models.Data.Integrations; + +public class IntegrationHandlerResult +{ + public IntegrationHandlerResult(bool success, IIntegrationMessage message) + { + Success = success; + Message = message; + } + + public bool Success { get; set; } = false; + public bool Retryable { get; set; } = false; + public IIntegrationMessage Message { get; set; } + public DateTime? DelayUntilDate { get; set; } + public string FailureReason { get; set; } = string.Empty; +} diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs new file mode 100644 index 0000000000..1f288914d0 --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs @@ -0,0 +1,34 @@ +using System.Text.Json; +using Bit.Core.Enums; + +namespace Bit.Core.AdminConsole.Models.Data.Integrations; + +public class IntegrationMessage : IIntegrationMessage +{ + public IntegrationType IntegrationType { get; set; } + public T Configuration { get; set; } + public string RenderedTemplate { get; set; } + public int RetryCount { get; set; } = 0; + public DateTime? DelayUntilDate { get; set; } + + public void ApplyRetry(DateTime? handlerDelayUntilDate) + { + RetryCount++; + + var baseTime = handlerDelayUntilDate ?? DateTime.UtcNow; + var backoffSeconds = Math.Pow(2, RetryCount); + var jitterSeconds = Random.Shared.Next(0, 3); + + DelayUntilDate = baseTime.AddSeconds(backoffSeconds + jitterSeconds); + } + + public string ToJson() + { + return JsonSerializer.Serialize(this); + } + + public static IntegrationMessage FromJson(string json) + { + return JsonSerializer.Deserialize>(json); + } +} diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationTemplateContext.cs b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationTemplateContext.cs index 18aa3b7681..338c2b963d 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationTemplateContext.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationTemplateContext.cs @@ -1,10 +1,11 @@ -using Bit.Core.AdminConsole.Entities; +#nullable enable + +using Bit.Core.AdminConsole.Entities; using Bit.Core.Entities; using Bit.Core.Enums; +using Bit.Core.Models.Data; -#nullable enable - -namespace Bit.Core.Models.Data.Integrations; +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public class IntegrationTemplateContext(EventMessage eventMessage) { diff --git a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs index e6fc1440ea..4fcce542ce 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs @@ -1,3 +1,3 @@ -namespace Bit.Core.Models.Data.Integrations; +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public record SlackIntegration(string token); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs index ad25d35e7e..2930004cbf 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs @@ -1,3 +1,3 @@ -namespace Bit.Core.Models.Data.Integrations; +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public record SlackIntegrationConfiguration(string channelId); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs index 49ca9df4e0..b81e50d403 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs @@ -1,3 +1,3 @@ -namespace Bit.Core.Models.Data.Integrations; +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public record SlackIntegrationConfigurationDetails(string channelId, string token); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs index 9a7591f24b..e8217d3ad3 100644 --- a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs +++ b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs @@ -1,3 +1,3 @@ -namespace Bit.Core.Models.Data.Integrations; +namespace Bit.Core.AdminConsole.Models.Data.Integrations; public record WebhookIntegrationConfiguration(string url); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs new file mode 100644 index 0000000000..e3e92c900f --- /dev/null +++ b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs @@ -0,0 +1,3 @@ +namespace Bit.Core.AdminConsole.Models.Data.Integrations; + +public record WebhookIntegrationConfigurationDetails(string url); diff --git a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetils.cs b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetils.cs deleted file mode 100644 index f165828de0..0000000000 --- a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetils.cs +++ /dev/null @@ -1,3 +0,0 @@ -namespace Bit.Core.Models.Data.Integrations; - -public record WebhookIntegrationConfigurationDetils(string url); diff --git a/src/Core/AdminConsole/Services/IIntegrationHandler.cs b/src/Core/AdminConsole/Services/IIntegrationHandler.cs new file mode 100644 index 0000000000..bf6e6791cf --- /dev/null +++ b/src/Core/AdminConsole/Services/IIntegrationHandler.cs @@ -0,0 +1,24 @@ +using Bit.Core.AdminConsole.Models.Data.Integrations; + +namespace Bit.Core.Services; + +public interface IIntegrationHandler +{ + Task HandleAsync(string json); +} + +public interface IIntegrationHandler : IIntegrationHandler +{ + Task HandleAsync(IntegrationMessage message); +} + +public abstract class IntegrationHandlerBase : IIntegrationHandler +{ + public async Task HandleAsync(string json) + { + var message = IntegrationMessage.FromJson(json); + return await HandleAsync(message); + } + + public abstract Task HandleAsync(IntegrationMessage message); +} diff --git a/src/Core/AdminConsole/Services/IIntegrationPublisher.cs b/src/Core/AdminConsole/Services/IIntegrationPublisher.cs new file mode 100644 index 0000000000..986ea776e1 --- /dev/null +++ b/src/Core/AdminConsole/Services/IIntegrationPublisher.cs @@ -0,0 +1,8 @@ +using Bit.Core.AdminConsole.Models.Data.Integrations; + +namespace Bit.Core.Services; + +public interface IIntegrationPublisher +{ + Task PublishAsync(IIntegrationMessage message); +} diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs new file mode 100644 index 0000000000..9a80ed67b2 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs @@ -0,0 +1,83 @@ +using System.Text.Json; +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.AdminConsole.Utilities; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Repositories; + +namespace Bit.Core.Services; + +#nullable enable + +public class EventIntegrationHandler( + IntegrationType integrationType, + IIntegrationPublisher integrationPublisher, + IOrganizationIntegrationConfigurationRepository configurationRepository, + IUserRepository userRepository, + IOrganizationRepository organizationRepository) + : IEventMessageHandler +{ + public async Task HandleEventAsync(EventMessage eventMessage) + { + if (eventMessage.OrganizationId is not Guid organizationId) + { + return; + } + + var configurations = await configurationRepository.GetConfigurationDetailsAsync( + organizationId, + integrationType, + eventMessage.Type); + + foreach (var configuration in configurations) + { + var template = configuration.Template ?? string.Empty; + var context = await BuildContextAsync(eventMessage, template); + var renderedTemplate = IntegrationTemplateProcessor.ReplaceTokens(template, context); + + var config = configuration.MergedConfiguration.Deserialize() + ?? throw new InvalidOperationException($"Failed to deserialize to {typeof(T).Name}"); + + var message = new IntegrationMessage + { + IntegrationType = integrationType, + Configuration = config, + RenderedTemplate = renderedTemplate, + RetryCount = 0, + DelayUntilDate = null + }; + + await integrationPublisher.PublishAsync(message); + } + } + + public async Task HandleManyEventsAsync(IEnumerable eventMessages) + { + foreach (var eventMessage in eventMessages) + { + await HandleEventAsync(eventMessage); + } + } + + private async Task BuildContextAsync(EventMessage eventMessage, string template) + { + var context = new IntegrationTemplateContext(eventMessage); + + if (IntegrationTemplateProcessor.TemplateRequiresUser(template) && eventMessage.UserId.HasValue) + { + context.User = await userRepository.GetByIdAsync(eventMessage.UserId.Value); + } + + if (IntegrationTemplateProcessor.TemplateRequiresActingUser(template) && eventMessage.ActingUserId.HasValue) + { + context.ActingUser = await userRepository.GetByIdAsync(eventMessage.ActingUserId.Value); + } + + if (IntegrationTemplateProcessor.TemplateRequiresOrganization(template) && eventMessage.OrganizationId.HasValue) + { + context.Organization = await organizationRepository.GetByIdAsync(eventMessage.OrganizationId.Value); + } + + return context; + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/IntegrationEventHandlerBase.cs b/src/Core/AdminConsole/Services/Implementations/IntegrationEventHandlerBase.cs index d8e521de97..4df2d25b1b 100644 --- a/src/Core/AdminConsole/Services/Implementations/IntegrationEventHandlerBase.cs +++ b/src/Core/AdminConsole/Services/Implementations/IntegrationEventHandlerBase.cs @@ -1,8 +1,8 @@ using System.Text.Json.Nodes; +using Bit.Core.AdminConsole.Models.Data.Integrations; using Bit.Core.AdminConsole.Utilities; using Bit.Core.Enums; using Bit.Core.Models.Data; -using Bit.Core.Models.Data.Integrations; using Bit.Core.Repositories; namespace Bit.Core.Services; diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs index 1ee3fa5ea7..74833f38a0 100644 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs @@ -29,7 +29,7 @@ public class RabbitMqEventListenerService : EventLoggingListenerService UserName = globalSettings.EventLogging.RabbitMq.Username, Password = globalSettings.EventLogging.RabbitMq.Password }; - _exchangeName = globalSettings.EventLogging.RabbitMq.ExchangeName; + _exchangeName = globalSettings.EventLogging.RabbitMq.EventExchangeName; _logger = logger; _queueName = queueName; } diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs index 86abddec58..05fcf71a92 100644 --- a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs @@ -18,7 +18,7 @@ public class RabbitMqEventWriteService : IEventWriteService, IAsyncDisposable UserName = globalSettings.EventLogging.RabbitMq.Username, Password = globalSettings.EventLogging.RabbitMq.Password }; - _exchangeName = globalSettings.EventLogging.RabbitMq.ExchangeName; + _exchangeName = globalSettings.EventLogging.RabbitMq.EventExchangeName; _lazyConnection = new Lazy>(CreateConnectionAsync); } diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs new file mode 100644 index 0000000000..1d6910db95 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs @@ -0,0 +1,191 @@ +using System.Text; +using Bit.Core.Settings; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +namespace Bit.Core.Services; + +public class RabbitMqIntegrationListenerService : BackgroundService +{ + private const string _deadLetterRoutingKey = "dead-letter"; + private IChannel _channel; + private IConnection _connection; + private readonly string _exchangeName; + private readonly string _queueName; + private readonly string _retryQueueName; + private readonly string _deadLetterQueueName; + private readonly string _routingKey; + private readonly string _retryRoutingKey; + private readonly int _maxRetries; + private readonly IIntegrationHandler _handler; + private readonly ConnectionFactory _factory; + private readonly ILogger _logger; + private readonly int _retryTiming; + + public RabbitMqIntegrationListenerService(IIntegrationHandler handler, + string routingKey, + string queueName, + string retryQueueName, + string deadLetterQueueName, + GlobalSettings globalSettings, + ILogger logger) + { + _handler = handler; + _routingKey = routingKey; + _retryRoutingKey = $"{_routingKey}-retry"; + _queueName = queueName; + _retryQueueName = retryQueueName; + _deadLetterQueueName = deadLetterQueueName; + _logger = logger; + _exchangeName = globalSettings.EventLogging.RabbitMq.IntegrationExchangeName; + _maxRetries = globalSettings.EventLogging.RabbitMq.MaxRetries; + _retryTiming = globalSettings.EventLogging.RabbitMq.RetryTiming; + + _factory = new ConnectionFactory + { + HostName = globalSettings.EventLogging.RabbitMq.HostName, + UserName = globalSettings.EventLogging.RabbitMq.Username, + Password = globalSettings.EventLogging.RabbitMq.Password + }; + } + + public override async Task StartAsync(CancellationToken cancellationToken) + { + _connection = await _factory.CreateConnectionAsync(cancellationToken); + _channel = await _connection.CreateChannelAsync(cancellationToken: cancellationToken); + + await _channel.ExchangeDeclareAsync(exchange: _exchangeName, + type: ExchangeType.Direct, + durable: true, + cancellationToken: cancellationToken); + + // Declare main queue + await _channel.QueueDeclareAsync(queue: _queueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: null, + cancellationToken: cancellationToken); + await _channel.QueueBindAsync(queue: _queueName, + exchange: _exchangeName, + routingKey: _routingKey, + cancellationToken: cancellationToken); + + // Declare retry queue (Configurable TTL, dead-letters back to main queue) + await _channel.QueueDeclareAsync(queue: _retryQueueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: new Dictionary + { + { "x-dead-letter-exchange", _exchangeName }, + { "x-dead-letter-routing-key", _routingKey }, + { "x-message-ttl", _retryTiming } + }, + cancellationToken: cancellationToken); + await _channel.QueueBindAsync(queue: _retryQueueName, + exchange: _exchangeName, + routingKey: _retryRoutingKey, + cancellationToken: cancellationToken); + + // Declare dead letter queue + await _channel.QueueDeclareAsync(queue: _deadLetterQueueName, + durable: true, + exclusive: false, + autoDelete: false, + arguments: null, + cancellationToken: cancellationToken); + await _channel.QueueBindAsync(queue: _deadLetterQueueName, + exchange: _exchangeName, + routingKey: _deadLetterRoutingKey, + cancellationToken: cancellationToken); + + await base.StartAsync(cancellationToken); + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + var consumer = new AsyncEventingBasicConsumer(_channel); + consumer.ReceivedAsync += async (_, ea) => + { + var json = Encoding.UTF8.GetString(ea.Body.Span); + + try + { + var result = await _handler.HandleAsync(json); + var message = result.Message; + + if (result.Success) + { + // Successful integration send. Acknowledge message delivery and return + await _channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); + return; + } + + if (result.Retryable) + { + // Integration failed, but is retryable - apply delay and check max retries + message.ApplyRetry(result.DelayUntilDate); + + if (message.RetryCount < _maxRetries) + { + // Publish message to the retry queue. It will be re-published for retry after a delay + await _channel.BasicPublishAsync( + exchange: _exchangeName, + routingKey: _retryRoutingKey, + body: Encoding.UTF8.GetBytes(message.ToJson()), + cancellationToken: cancellationToken); + } + else + { + // Exceeded the max number of retries; fail and send to dead letter queue + await PublishToDeadLetterAsync(message.ToJson()); + _logger.LogWarning("Max retry attempts reached. Sent to DLQ."); + } + } + else + { + // Fatal error (i.e. not retryable) occurred. Send message to dead letter queue without any retries + await PublishToDeadLetterAsync(message.ToJson()); + _logger.LogWarning("Non-retryable failure. Sent to DLQ."); + } + + // Message has been sent to retry or dead letter queues. + // Acknowledge receipt so Rabbit knows it's been processed + await _channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); + } + catch (Exception ex) + { + // Unknown error occurred. Acknowledge so Rabbit doesn't keep attempting. Log the error + _logger.LogError(ex, "Unhandled error processing integration message."); + await _channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken); + } + }; + + await _channel.BasicConsumeAsync(queue: _queueName, autoAck: false, consumer: consumer, cancellationToken: cancellationToken); + } + + private async Task PublishToDeadLetterAsync(string json) + { + await _channel.BasicPublishAsync( + exchange: _exchangeName, + routingKey: _deadLetterRoutingKey, + body: Encoding.UTF8.GetBytes(json)); + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + await _channel.CloseAsync(cancellationToken); + await _connection.CloseAsync(cancellationToken); + await base.StopAsync(cancellationToken); + } + + public override void Dispose() + { + _channel.Dispose(); + _connection.Dispose(); + base.Dispose(); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationPublisher.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationPublisher.cs new file mode 100644 index 0000000000..12801e3216 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationPublisher.cs @@ -0,0 +1,54 @@ +using System.Text; +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Enums; +using Bit.Core.Settings; +using RabbitMQ.Client; + +namespace Bit.Core.Services; + +public class RabbitMqIntegrationPublisher : IIntegrationPublisher, IAsyncDisposable +{ + private readonly ConnectionFactory _factory; + private readonly Lazy> _lazyConnection; + private readonly string _exchangeName; + + public RabbitMqIntegrationPublisher(GlobalSettings globalSettings) + { + _factory = new ConnectionFactory + { + HostName = globalSettings.EventLogging.RabbitMq.HostName, + UserName = globalSettings.EventLogging.RabbitMq.Username, + Password = globalSettings.EventLogging.RabbitMq.Password + }; + _exchangeName = globalSettings.EventLogging.RabbitMq.IntegrationExchangeName; + + _lazyConnection = new Lazy>(CreateConnectionAsync); + } + + public async Task PublishAsync(IIntegrationMessage message) + { + var routingKey = message.IntegrationType.ToRoutingKey(); + var connection = await _lazyConnection.Value; + await using var channel = await connection.CreateChannelAsync(); + + await channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Direct, durable: true); + + var body = Encoding.UTF8.GetBytes(message.ToJson()); + + await channel.BasicPublishAsync(exchange: _exchangeName, routingKey: routingKey, body: body); + } + + public async ValueTask DisposeAsync() + { + if (_lazyConnection.IsValueCreated) + { + var connection = await _lazyConnection.Value; + await connection.DisposeAsync(); + } + } + + private async Task CreateConnectionAsync() + { + return await _factory.CreateConnectionAsync(); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs b/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs index 3ddecc67f4..a767776c36 100644 --- a/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs @@ -1,7 +1,7 @@ using System.Text.Json; using System.Text.Json.Nodes; +using Bit.Core.AdminConsole.Models.Data.Integrations; using Bit.Core.Enums; -using Bit.Core.Models.Data.Integrations; using Bit.Core.Repositories; #nullable enable diff --git a/src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs new file mode 100644 index 0000000000..134e93e838 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs @@ -0,0 +1,19 @@ +using Bit.Core.AdminConsole.Models.Data.Integrations; + +namespace Bit.Core.Services; + +public class SlackIntegrationHandler( + ISlackService slackService) + : IntegrationHandlerBase +{ + public override async Task HandleAsync(IntegrationMessage message) + { + await slackService.SendSlackMessageByChannelIdAsync( + message.Configuration.token, + message.RenderedTemplate, + message.Configuration.channelId + ); + + return new IntegrationHandlerResult(success: true, message: message); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs b/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs index ec6924bb3e..97453497bc 100644 --- a/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs +++ b/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs @@ -1,8 +1,8 @@ using System.Text; using System.Text.Json; using System.Text.Json.Nodes; +using Bit.Core.AdminConsole.Models.Data.Integrations; using Bit.Core.Enums; -using Bit.Core.Models.Data.Integrations; using Bit.Core.Repositories; #nullable enable @@ -25,7 +25,7 @@ public class WebhookEventHandler( protected override async Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration, string renderedTemplate) { - var config = mergedConfiguration.Deserialize(); + var config = mergedConfiguration.Deserialize(); if (config is null || string.IsNullOrEmpty(config.url)) { return; diff --git a/src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs new file mode 100644 index 0000000000..5f9898afe8 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs @@ -0,0 +1,61 @@ +using System.Globalization; +using System.Net; +using System.Text; +using Bit.Core.AdminConsole.Models.Data.Integrations; + +#nullable enable + +namespace Bit.Core.Services; + +public class WebhookIntegrationHandler(IHttpClientFactory httpClientFactory) + : IntegrationHandlerBase +{ + private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName); + + public const string HttpClientName = "WebhookIntegrationHandlerHttpClient"; + + public override async Task HandleAsync(IntegrationMessage message) + { + var content = new StringContent(message.RenderedTemplate, Encoding.UTF8, "application/json"); + var response = await _httpClient.PostAsync(message.Configuration.url, content); + var result = new IntegrationHandlerResult(success: response.IsSuccessStatusCode, message); + + switch (response.StatusCode) + { + case HttpStatusCode.TooManyRequests: + case HttpStatusCode.RequestTimeout: + case HttpStatusCode.InternalServerError: + case HttpStatusCode.BadGateway: + case HttpStatusCode.ServiceUnavailable: + case HttpStatusCode.GatewayTimeout: + result.Retryable = true; + result.FailureReason = response.ReasonPhrase; + + if (response.Headers.TryGetValues("Retry-After", out var values)) + { + var value = values.FirstOrDefault(); + if (int.TryParse(value, out var seconds)) + { + // Retry-after was specified in seconds. Adjust DelayUntilDate by the requested number of seconds. + result.DelayUntilDate = DateTime.UtcNow.AddSeconds(seconds); + } + else if (DateTimeOffset.TryParseExact(value, + "r", // "r" is the round-trip format: RFC1123 + CultureInfo.InvariantCulture, + DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, + out var retryDate)) + { + // Retry-after was specified as a date. Adjust DelayUntilDate to the specified date. + result.DelayUntilDate = retryDate.UtcDateTime; + } + } + break; + default: + result.Retryable = false; + result.FailureReason = response.ReasonPhrase; + break; + } + + return result; + } +} diff --git a/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs b/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs index 4fb5c15e63..aab4e448e5 100644 --- a/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs +++ b/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs @@ -1,4 +1,6 @@ -using System.Text.RegularExpressions; +#nullable enable + +using System.Text.RegularExpressions; namespace Bit.Core.AdminConsole.Utilities; @@ -9,7 +11,7 @@ public static partial class IntegrationTemplateProcessor public static string ReplaceTokens(string template, object values) { - if (string.IsNullOrEmpty(template) || values == null) + if (string.IsNullOrEmpty(template)) { return template; } diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index d31e18b955..d3f4253908 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -312,11 +312,19 @@ public class GlobalSettings : IGlobalSettings private string _hostName; private string _username; private string _password; - private string _exchangeName; + private string _eventExchangeName; + private string _integrationExchangeName; + public int MaxRetries { get; set; } = 3; + public int RetryTiming { get; set; } = 30000; // 30s public virtual string EventRepositoryQueueName { get; set; } = "events-write-queue"; - public virtual string WebhookQueueName { get; set; } = "events-webhook-queue"; - public virtual string SlackQueueName { get; set; } = "events-slack-queue"; + public virtual string IntegrationDeadLetterQueueName { get; set; } = "integration-dead-letter-queue"; + public virtual string SlackEventsQueueName { get; set; } = "events-slack-queue"; + public virtual string SlackIntegrationQueueName { get; set; } = "integration-slack-queue"; + public virtual string SlackIntegrationRetryQueueName { get; set; } = "integration-slack-retry-queue"; + public virtual string WebhookEventsQueueName { get; set; } = "events-webhook-queue"; + public virtual string WebhookIntegrationQueueName { get; set; } = "integration-webhook-queue"; + public virtual string WebhookIntegrationRetryQueueName { get; set; } = "integration-webhook-retry-queue"; public string HostName { @@ -333,10 +341,15 @@ public class GlobalSettings : IGlobalSettings get => _password; set => _password = value.Trim('"'); } - public string ExchangeName + public string EventExchangeName { - get => _exchangeName; - set => _exchangeName = value.Trim('"'); + get => _eventExchangeName; + set => _eventExchangeName = value.Trim('"'); + } + public string IntegrationExchangeName + { + get => _integrationExchangeName; + set => _integrationExchangeName = value.Trim('"'); } } } diff --git a/src/Events/Startup.cs b/src/Events/Startup.cs index 366b562485..5fc12854b6 100644 --- a/src/Events/Startup.cs +++ b/src/Events/Startup.cs @@ -1,6 +1,4 @@ using System.Globalization; -using Bit.Core.AdminConsole.Services.Implementations; -using Bit.Core.AdminConsole.Services.NoopImplementations; using Bit.Core.Context; using Bit.Core.IdentityServer; using Bit.Core.Services; @@ -63,37 +61,7 @@ public class Startup services.AddSingleton(); } - if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString)) - { - services.AddKeyedSingleton("storage"); - - if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName)) - { - services.AddKeyedSingleton("broadcast"); - } - else - { - services.AddKeyedSingleton("broadcast"); - } - } - else - { - services.AddKeyedSingleton("storage"); - - if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName)) - { - services.AddKeyedSingleton("broadcast"); - } - else - { - services.AddKeyedSingleton("broadcast"); - } - } - services.AddScoped(); + services.AddEventWriteServices(globalSettings); services.AddScoped(); services.AddOptionality(); @@ -109,49 +77,7 @@ public class Startup services.AddHostedService(); } - // Optional RabbitMQ Listeners - if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName)) - { - services.AddSingleton(); - services.AddKeyedSingleton("persistent"); - services.AddSingleton(provider => - new RabbitMqEventListenerService( - provider.GetRequiredService(), - provider.GetRequiredService>(), - globalSettings, - globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName)); - - if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) && - CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) && - CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes)) - { - services.AddHttpClient(SlackService.HttpClientName); - services.AddSingleton(); - } - else - { - services.AddSingleton(); - } - services.AddSingleton(); - services.AddSingleton(provider => - new RabbitMqEventListenerService( - provider.GetRequiredService(), - provider.GetRequiredService>(), - globalSettings, - globalSettings.EventLogging.RabbitMq.SlackQueueName)); - - services.AddHttpClient(WebhookEventHandler.HttpClientName); - services.AddSingleton(); - services.AddSingleton(provider => - new RabbitMqEventListenerService( - provider.GetRequiredService(), - provider.GetRequiredService>(), - globalSettings, - globalSettings.EventLogging.RabbitMq.WebhookQueueName)); - } + services.AddRabbitMqListeners(globalSettings); } public void Configure( diff --git a/src/EventsProcessor/Startup.cs b/src/EventsProcessor/Startup.cs index e397bd326b..67676a8afc 100644 --- a/src/EventsProcessor/Startup.cs +++ b/src/EventsProcessor/Startup.cs @@ -1,12 +1,8 @@ using System.Globalization; -using Bit.Core.AdminConsole.Services.NoopImplementations; -using Bit.Core.Repositories; -using Bit.Core.Services; using Bit.Core.Settings; using Bit.Core.Utilities; using Bit.SharedWeb.Utilities; using Microsoft.IdentityModel.Logging; -using TableStorageRepos = Bit.Core.Repositories.TableStorage; namespace Bit.EventsProcessor; @@ -37,50 +33,7 @@ public class Startup services.AddDatabaseRepositories(globalSettings); // Hosted Services - - // Optional Azure Service Bus Listeners - if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName)) - { - services.AddSingleton(); - services.AddSingleton(); - services.AddKeyedSingleton("persistent"); - services.AddSingleton(provider => - new AzureServiceBusEventListenerService( - provider.GetRequiredService(), - provider.GetRequiredService>(), - globalSettings, - globalSettings.EventLogging.AzureServiceBus.EventRepositorySubscriptionName)); - - if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) && - CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) && - CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes)) - { - services.AddHttpClient(SlackService.HttpClientName); - services.AddSingleton(); - } - else - { - services.AddSingleton(); - } - services.AddSingleton(); - services.AddSingleton(provider => - new AzureServiceBusEventListenerService( - provider.GetRequiredService(), - provider.GetRequiredService>(), - globalSettings, - globalSettings.EventLogging.AzureServiceBus.SlackSubscriptionName)); - - services.AddSingleton(); - services.AddHttpClient(WebhookEventHandler.HttpClientName); - - services.AddSingleton(provider => - new AzureServiceBusEventListenerService( - provider.GetRequiredService(), - provider.GetRequiredService>(), - globalSettings, - globalSettings.EventLogging.AzureServiceBus.WebhookSubscriptionName)); - } + services.AddAzureServiceBusListeners(globalSettings); services.AddHostedService(); } diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 598d93b177..e425cf7254 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -5,6 +5,7 @@ using System.Security.Cryptography.X509Certificates; using AspNetCoreRateLimit; using Azure.Storage.Queues; using Bit.Core.AdminConsole.Models.Business.Tokenables; +using Bit.Core.AdminConsole.Models.Data.Integrations; using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services.Implementations; @@ -324,42 +325,7 @@ public static class ServiceCollectionExtensions services.AddSingleton(); } - if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString)) - { - services.AddKeyedSingleton("storage"); - - if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName)) - { - services.AddKeyedSingleton("broadcast"); - } - else - { - services.AddKeyedSingleton("broadcast"); - } - } - else if (globalSettings.SelfHosted) - { - services.AddKeyedSingleton("storage"); - - if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName)) - { - services.AddKeyedSingleton("broadcast"); - } - else - { - services.AddKeyedSingleton("broadcast"); - } - } - else - { - services.AddKeyedSingleton("storage"); - services.AddKeyedSingleton("broadcast"); - } - services.AddScoped(); + services.AddEventWriteServices(globalSettings); if (CoreHelpers.SettingHasValue(globalSettings.Attachment.ConnectionString)) { @@ -584,6 +550,193 @@ public static class ServiceCollectionExtensions return globalSettings; } + public static IServiceCollection AddEventWriteServices(this IServiceCollection services, GlobalSettings globalSettings) + { + if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString)) + { + services.AddKeyedSingleton("storage"); + + if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) && + CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName)) + { + services.AddKeyedSingleton("broadcast"); + } + else + { + services.AddKeyedSingleton("broadcast"); + } + } + else if (globalSettings.SelfHosted) + { + services.AddKeyedSingleton("storage"); + + if (IsRabbitMqEnabled(globalSettings)) + { + services.AddKeyedSingleton("broadcast"); + } + else + { + services.AddKeyedSingleton("broadcast"); + } + } + else + { + services.AddKeyedSingleton("storage"); + services.AddKeyedSingleton("broadcast"); + } + + services.AddScoped(); + return services; + } + + public static IServiceCollection AddAzureServiceBusListeners(this IServiceCollection services, GlobalSettings globalSettings) + { + if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) && + CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName)) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddKeyedSingleton("persistent"); + services.AddSingleton(provider => + new AzureServiceBusEventListenerService( + provider.GetRequiredService(), + provider.GetRequiredService>(), + globalSettings, + globalSettings.EventLogging.AzureServiceBus.EventRepositorySubscriptionName)); + + + services.AddSlackService(globalSettings); + services.AddSingleton(); + services.AddSingleton(provider => + new AzureServiceBusEventListenerService( + provider.GetRequiredService(), + provider.GetRequiredService>(), + globalSettings, + globalSettings.EventLogging.AzureServiceBus.SlackSubscriptionName)); + + services.AddSingleton(); + services.AddHttpClient(WebhookEventHandler.HttpClientName); + services.AddSingleton(provider => + new AzureServiceBusEventListenerService( + provider.GetRequiredService(), + provider.GetRequiredService>(), + globalSettings, + globalSettings.EventLogging.AzureServiceBus.WebhookSubscriptionName)); + } + + return services; + } + + public static IServiceCollection AddRabbitMqListeners(this IServiceCollection services, GlobalSettings globalSettings) + { + if (IsRabbitMqEnabled(globalSettings)) + { + services.AddRabbitMqEventRepositoryListener(globalSettings); + + services.AddSlackService(globalSettings); + services.AddRabbitMqIntegration( + globalSettings.EventLogging.RabbitMq.SlackEventsQueueName, + globalSettings.EventLogging.RabbitMq.SlackIntegrationQueueName, + globalSettings.EventLogging.RabbitMq.SlackIntegrationRetryQueueName, + globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName, + IntegrationType.Slack, + globalSettings); + + services.AddHttpClient(WebhookIntegrationHandler.HttpClientName); + services.AddRabbitMqIntegration( + globalSettings.EventLogging.RabbitMq.WebhookEventsQueueName, + globalSettings.EventLogging.RabbitMq.WebhookIntegrationQueueName, + globalSettings.EventLogging.RabbitMq.WebhookIntegrationRetryQueueName, + globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName, + IntegrationType.Webhook, + globalSettings); + } + + return services; + } + + public static IServiceCollection AddSlackService(this IServiceCollection services, GlobalSettings globalSettings) + { + if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) && + CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) && + CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes)) + { + services.AddHttpClient(SlackService.HttpClientName); + services.AddSingleton(); + } + else + { + services.AddSingleton(); + } + + return services; + } + + private static IServiceCollection AddRabbitMqEventRepositoryListener(this IServiceCollection services, GlobalSettings globalSettings) + { + services.AddSingleton(); + services.AddKeyedSingleton("persistent"); + + services.AddSingleton(provider => + new RabbitMqEventListenerService( + provider.GetRequiredService(), + provider.GetRequiredService>(), + globalSettings, + globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName)); + + return services; + } + + private static IServiceCollection AddRabbitMqIntegration(this IServiceCollection services, + string eventQueueName, + string integrationQueueName, + string integrationRetryQueueName, + string integrationDeadLetterQueueName, + IntegrationType integrationType, + GlobalSettings globalSettings) + where TConfig : class + where THandler : class, IIntegrationHandler + { + var routingKey = integrationType.ToRoutingKey(); + + services.AddSingleton(); + services.AddKeyedSingleton(routingKey, (provider, _) => + new EventIntegrationHandler( + integrationType, + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService())); + + services.AddSingleton(provider => + new RabbitMqEventListenerService( + provider.GetRequiredKeyedService(routingKey), + provider.GetRequiredService>(), + globalSettings, + eventQueueName)); + + services.AddSingleton, THandler>(); + services.AddSingleton(provider => + new RabbitMqIntegrationListenerService( + handler: provider.GetRequiredService>(), + routingKey: routingKey, + queueName: integrationQueueName, + retryQueueName: integrationRetryQueueName, + deadLetterQueueName: integrationDeadLetterQueueName, + globalSettings: globalSettings, + logger: provider.GetRequiredService>())); + + return services; + } + + private static bool IsRabbitMqEnabled(GlobalSettings settings) + { + return CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.HostName) && + CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Username) && + CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Password) && + CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.EventExchangeName); + } + public static void UseDefaultMiddleware(this IApplicationBuilder app, IWebHostEnvironment env, GlobalSettings globalSettings) { diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs index 8a33e17053..f7863401b5 100644 --- a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs +++ b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs @@ -3,10 +3,10 @@ using Bit.Api.AdminConsole.Controllers; using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Data.Integrations; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; -using Bit.Core.Models.Data.Integrations; using Bit.Core.Repositories; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; diff --git a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs index 0076d8bca1..77ce06f4f8 100644 --- a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs +++ b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs @@ -1,7 +1,7 @@ using System.Text.Json; using Bit.Api.AdminConsole.Models.Request.Organizations; +using Bit.Core.AdminConsole.Models.Data.Integrations; using Bit.Core.Enums; -using Bit.Core.Models.Data.Integrations; using Xunit; namespace Bit.Api.Test.AdminConsole.Models.Request.Organizations; diff --git a/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs b/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs new file mode 100644 index 0000000000..44774449c1 --- /dev/null +++ b/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs @@ -0,0 +1,53 @@ +using System.Text.Json; +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Enums; +using Xunit; + +namespace Bit.Core.Test.Models.Data.Integrations; + +public class IntegrationMessageTests +{ + [Fact] + public void ApplyRetry_IncrementsRetryCountAndSetsDelayUntilDate() + { + var message = new IntegrationMessage + { + RetryCount = 2, + DelayUntilDate = null + }; + + var baseline = DateTime.UtcNow; + message.ApplyRetry(baseline); + + Assert.Equal(3, message.RetryCount); + Assert.True(message.DelayUntilDate > baseline); + } + + [Fact] + public void FromToJson_SerializesCorrectly() + { + var message = new IntegrationMessage + { + Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"), + RenderedTemplate = "This is the message", + IntegrationType = IntegrationType.Webhook, + RetryCount = 2, + DelayUntilDate = null + }; + + var json = message.ToJson(); + var result = IntegrationMessage.FromJson(json); + + Assert.Equal(message.Configuration, result.Configuration); + Assert.Equal(message.RenderedTemplate, result.RenderedTemplate); + Assert.Equal(message.IntegrationType, result.IntegrationType); + Assert.Equal(message.RetryCount, result.RetryCount); + } + + [Fact] + public void FromJson_InvalidJson_ThrowsJsonException() + { + var json = "{ Invalid JSON"; + Assert.Throws(() => IntegrationMessage.FromJson(json)); + } +} diff --git a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs new file mode 100644 index 0000000000..f0a0d1d724 --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs @@ -0,0 +1,212 @@ +using System.Text.Json; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations; +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 NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class EventIntegrationHandlerTests +{ + private const string _templateBase = "Date: #Date#, Type: #Type#, UserId: #UserId#"; + private const string _templateWithOrganization = "Org: #OrganizationName#"; + private const string _templateWithUser = "#UserName#, #UserEmail#"; + private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#"; + private const string _url = "https://localhost"; + private const string _url2 = "https://example.com"; + private readonly IIntegrationPublisher _integrationPublisher = Substitute.For(); + + private SutProvider> GetSutProvider( + List configurations) + { + var configurationRepository = Substitute.For(); + configurationRepository.GetConfigurationDetailsAsync(Arg.Any(), + IntegrationType.Webhook, Arg.Any()).Returns(configurations); + + return new SutProvider>() + .SetDependency(configurationRepository) + .SetDependency(_integrationPublisher) + .SetDependency(IntegrationType.Webhook) + .Create(); + } + + private static IntegrationMessage expectedMessage(string template) + { + return new IntegrationMessage() + { + IntegrationType = IntegrationType.Webhook, + Configuration = new WebhookIntegrationConfigurationDetails(_url), + RenderedTemplate = template, + RetryCount = 0, + DelayUntilDate = null + }; + } + + private static List NoConfigurations() + { + return []; + } + + private static List OneConfiguration(string template) + { + var config = Substitute.For(); + config.Configuration = null; + config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url }); + config.Template = template; + + return [config]; + } + + private static List TwoConfigurations(string template) + { + var config = Substitute.For(); + config.Configuration = null; + config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url }); + config.Template = template; + var config2 = Substitute.For(); + config2.Configuration = null; + config2.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url2 }); + config2.Template = template; + + return [config, config2]; + } + + [Theory, BitAutoData] + public async Task HandleEventAsync_BaseTemplateNoConfigurations_DoesNothing(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(NoConfigurations()); + + await sutProvider.Sut.HandleEventAsync(eventMessage); + Assert.Empty(_integrationPublisher.ReceivedCalls()); + } + + [Theory, BitAutoData] + public async Task HandleEventAsync_BaseTemplateOneConfiguration_CallsProcessEventIntegrationAsync(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateBase)); + + await sutProvider.Sut.HandleEventAsync(eventMessage); + + var expectedMessage = EventIntegrationHandlerTests.expectedMessage( + $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" + ); + + Assert.Single(_integrationPublisher.ReceivedCalls()); + await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task HandleEventAsync_ActingUserTemplate_LoadsUserFromRepository(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser)); + var user = Substitute.For(); + user.Email = "test@example.com"; + user.Name = "Test"; + + sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(user); + await sutProvider.Sut.HandleEventAsync(eventMessage); + + var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"{user.Name}, {user.Email}"); + + Assert.Single(_integrationPublisher.ReceivedCalls()); + await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.ActingUserId ?? Guid.Empty); + } + + [Theory, BitAutoData] + public async Task HandleEventAsync_OrganizationTemplate_LoadsOrganizationFromRepository(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization)); + var organization = Substitute.For(); + organization.Name = "Test"; + + sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(organization); + await sutProvider.Sut.HandleEventAsync(eventMessage); + + Assert.Single(_integrationPublisher.ReceivedCalls()); + + var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"Org: {organization.Name}"); + + Assert.Single(_integrationPublisher.ReceivedCalls()); + await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.OrganizationId ?? Guid.Empty); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + } + + [Theory, BitAutoData] + public async Task HandleEventAsync_UserTemplate_LoadsUserFromRepository(EventMessage eventMessage) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser)); + var user = Substitute.For(); + user.Email = "test@example.com"; + user.Name = "Test"; + + sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(user); + await sutProvider.Sut.HandleEventAsync(eventMessage); + + var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"{user.Name}, {user.Email}"); + + Assert.Single(_integrationPublisher.ReceivedCalls()); + await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); + await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.UserId ?? Guid.Empty); + } + + [Theory, BitAutoData] + public async Task HandleManyEventsAsync_BaseTemplateNoConfigurations_DoesNothing(List eventMessages) + { + var sutProvider = GetSutProvider(NoConfigurations()); + + await sutProvider.Sut.HandleManyEventsAsync(eventMessages); + Assert.Empty(_integrationPublisher.ReceivedCalls()); + } + + [Theory, BitAutoData] + public async Task HandleManyEventsAsync_BaseTemplateOneConfiguration_CallsProcessEventIntegrationAsync(List eventMessages) + { + var sutProvider = GetSutProvider(OneConfiguration(_templateBase)); + + await sutProvider.Sut.HandleManyEventsAsync(eventMessages); + + foreach (var eventMessage in eventMessages) + { + var expectedMessage = EventIntegrationHandlerTests.expectedMessage( + $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" + ); + await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + } + } + + [Theory, BitAutoData] + public async Task HandleManyEventsAsync_BaseTemplateTwoConfigurations_CallsProcessEventIntegrationAsyncMultipleTimes( + List eventMessages) + { + var sutProvider = GetSutProvider(TwoConfigurations(_templateBase)); + + await sutProvider.Sut.HandleManyEventsAsync(eventMessages); + + foreach (var eventMessage in eventMessages) + { + var expectedMessage = EventIntegrationHandlerTests.expectedMessage( + $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}" + ); + await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + + expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_url2); + await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage))); + } + } +} diff --git a/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs new file mode 100644 index 0000000000..10f39665d5 --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs @@ -0,0 +1,41 @@ +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Enums; +using Bit.Core.Services; +using Xunit; + +namespace Bit.Core.Test.Services; + +public class IntegrationHandlerTests +{ + + [Fact] + public async Task HandleAsync_ConvertsJsonToTypedIntegrationMessage() + { + var sut = new TestIntegrationHandler(); + var expected = new IntegrationMessage() + { + Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"), + IntegrationType = IntegrationType.Webhook, + RenderedTemplate = "Template", + DelayUntilDate = null, + RetryCount = 0 + }; + + var result = await sut.HandleAsync(expected.ToJson()); + var typedResult = Assert.IsType>(result.Message); + + Assert.Equal(expected.Configuration, typedResult.Configuration); + Assert.Equal(expected.RenderedTemplate, typedResult.RenderedTemplate); + Assert.Equal(expected.IntegrationType, typedResult.IntegrationType); + } + + private class TestIntegrationHandler : IntegrationHandlerBase + { + public override Task HandleAsync( + IntegrationMessage message) + { + var result = new IntegrationHandlerResult(success: true, message: message); + return Task.FromResult(result); + } + } +} diff --git a/test/Core.Test/AdminConsole/Services/IntegrationTypeTests.cs b/test/Core.Test/AdminConsole/Services/IntegrationTypeTests.cs new file mode 100644 index 0000000000..98cf974df8 --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/IntegrationTypeTests.cs @@ -0,0 +1,30 @@ +using Bit.Core.Enums; +using Xunit; + +namespace Bit.Core.Test.Services; + +public class IntegrationTypeTests +{ + [Fact] + public void ToRoutingKey_Slack_Succeeds() + { + Assert.Equal("slack", IntegrationType.Slack.ToRoutingKey()); + } + [Fact] + public void ToRoutingKey_Webhook_Succeeds() + { + Assert.Equal("webhook", IntegrationType.Webhook.ToRoutingKey()); + } + + [Fact] + public void ToRoutingKey_CloudBillingSync_ThrowsException() + { + Assert.Throws(() => IntegrationType.CloudBillingSync.ToRoutingKey()); + } + + [Fact] + public void ToRoutingKey_Scim_ThrowsException() + { + Assert.Throws(() => IntegrationType.Scim.ToRoutingKey()); + } +} diff --git a/test/Core.Test/AdminConsole/Services/SlackIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/SlackIntegrationHandlerTests.cs new file mode 100644 index 0000000000..9f66e2eb2f --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/SlackIntegrationHandlerTests.cs @@ -0,0 +1,42 @@ +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class SlackIntegrationHandlerTests +{ + private readonly ISlackService _slackService = Substitute.For(); + private readonly string _channelId = "C12345"; + private readonly string _token = "xoxb-test-token"; + + private SutProvider GetSutProvider() + { + return new SutProvider() + .SetDependency(_slackService) + .Create(); + } + + [Theory, BitAutoData] + public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token); + + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.True(result.Success); + Assert.Equal(result.Message, message); + + await sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync( + Arg.Is(AssertHelper.AssertPropertyEqual(_token)), + Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)), + Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)) + ); + } +} diff --git a/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs new file mode 100644 index 0000000000..79c7569ea3 --- /dev/null +++ b/test/Core.Test/AdminConsole/Services/WebhookIntegrationHandlerTests.cs @@ -0,0 +1,139 @@ +using System.Net; +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using Bit.Test.Common.MockedHttpClient; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Services; + +[SutProviderCustomize] +public class WebhookIntegrationHandlerTests +{ + private readonly MockedHttpMessageHandler _handler; + private readonly HttpClient _httpClient; + private const string _webhookUrl = "http://localhost/test/event"; + + public WebhookIntegrationHandlerTests() + { + _handler = new MockedHttpMessageHandler(); + _handler.Fallback + .WithStatusCode(HttpStatusCode.OK) + .WithContent(new StringContent("testtest")); + _httpClient = _handler.ToHttpClient(); + } + + private SutProvider GetSutProvider() + { + var clientFactory = Substitute.For(); + clientFactory.CreateClient(WebhookIntegrationHandler.HttpClientName).Returns(_httpClient); + + return new SutProvider() + .SetDependency(clientFactory) + .Create(); + } + + [Theory, BitAutoData] + public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl); + + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.True(result.Success); + Assert.Equal(result.Message, message); + + sutProvider.GetDependency().Received(1).CreateClient( + Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName)) + ); + + Assert.Single(_handler.CapturedRequests); + var request = _handler.CapturedRequests[0]; + Assert.NotNull(request); + var returned = await request.Content.ReadAsStringAsync(); + + Assert.Equal(HttpMethod.Post, request.Method); + Assert.Equal(_webhookUrl, request.RequestUri.ToString()); + AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned); + } + + [Theory, BitAutoData] + public async Task HandleAsync_TooManyRequests_ReturnsFailureSetsNotBeforUtc(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl); + + _handler.Fallback + .WithStatusCode(HttpStatusCode.TooManyRequests) + .WithHeader("Retry-After", "60") + .WithContent(new StringContent("testtest")); + + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.False(result.Success); + Assert.True(result.Retryable); + Assert.Equal(result.Message, message); + Assert.True(result.DelayUntilDate.HasValue); + Assert.InRange(result.DelayUntilDate.Value, DateTime.UtcNow.AddSeconds(59), DateTime.UtcNow.AddSeconds(61)); + } + + [Theory, BitAutoData] + public async Task HandleAsync_TooManyRequestsWithDate_ReturnsFailureSetsNotBeforUtc(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl); + + _handler.Fallback + .WithStatusCode(HttpStatusCode.TooManyRequests) + .WithHeader("Retry-After", DateTime.UtcNow.AddSeconds(60).ToString("r")) // "r" is the round-trip format: RFC1123 + .WithContent(new StringContent("testtest")); + + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.False(result.Success); + Assert.True(result.Retryable); + Assert.Equal(result.Message, message); + Assert.True(result.DelayUntilDate.HasValue); + Assert.InRange(result.DelayUntilDate.Value, DateTime.UtcNow.AddSeconds(59), DateTime.UtcNow.AddSeconds(61)); + } + + [Theory, BitAutoData] + public async Task HandleAsync_InternalServerError_ReturnsFailureSetsRetryable(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl); + + _handler.Fallback + .WithStatusCode(HttpStatusCode.InternalServerError) + .WithContent(new StringContent("testtest")); + + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.False(result.Success); + Assert.True(result.Retryable); + Assert.Equal(result.Message, message); + Assert.False(result.DelayUntilDate.HasValue); + } + + [Theory, BitAutoData] + public async Task HandleAsync_UnexpectedRedirect_ReturnsFailureNotRetryable(IntegrationMessage message) + { + var sutProvider = GetSutProvider(); + message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl); + + _handler.Fallback + .WithStatusCode(HttpStatusCode.TemporaryRedirect) + .WithContent(new StringContent("testtest")); + + var result = await sutProvider.Sut.HandleAsync(message); + + Assert.False(result.Success); + Assert.False(result.Retryable); + Assert.Equal(result.Message, message); + Assert.Null(result.DelayUntilDate); + } +} diff --git a/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs b/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs index d117b5e999..155eceeb25 100644 --- a/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs +++ b/test/Core.Test/AdminConsole/Utilities/IntegrationTemplateProcessorTests.cs @@ -1,4 +1,6 @@ -using Bit.Core.AdminConsole.Utilities; +#nullable enable + +using Bit.Core.AdminConsole.Utilities; using Bit.Core.Models.Data; using Bit.Test.Common.AutoFixture.Attributes; using Xunit; @@ -76,18 +78,6 @@ public class IntegrationTemplateProcessorTests var expectedEmpty = ""; Assert.Equal(expectedEmpty, IntegrationTemplateProcessor.ReplaceTokens(emptyTemplate, eventMessage)); - Assert.Null(IntegrationTemplateProcessor.ReplaceTokens(null, eventMessage)); - } - - [Fact] - public void ReplaceTokens_DataObjectIsNull_ReturnsOriginalString() - { - var template = "Event #Type#, User (id: #UserId#)."; - var expected = "Event #Type#, User (id: #UserId#)."; - - var result = IntegrationTemplateProcessor.ReplaceTokens(template, null); - - Assert.Equal(expected, result); } [Theory] From fe0c14e8038e1adecfb945980612387e1059ca85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Tue, 27 May 2025 15:18:23 +0100 Subject: [PATCH 096/114] [PM-19282] Update SsoUser ExternalId column size to 300 (#5750) * [PM-19282] Update SsoUser ExternalId column size to 300 * [PM-19282] Add migration to update SsoUser ExternalId column size to 300 for MySQL, PostgreSQL, and SQLite * [PM-19282] Update SsoUser ExternalId column size conditionally based on existing schema * Bumped date on migration script name --- src/Core/Auth/Entities/SsoUser.cs | 2 +- .../dbo/Stored Procedures/SsoUser_Create.sql | 2 +- .../dbo/Stored Procedures/SsoUser_Update.sql | 2 +- src/Sql/Auth/dbo/Tables/SsoUser.sql | 2 +- .../DbScripts/2025-05-27_00_SsoExternalId.sql | 67 + .../20250429113731_SsoExternalId.Designer.cs | 3112 ++++++++++++++++ .../20250429113731_SsoExternalId.cs | 43 + .../DatabaseContextModelSnapshot.cs | 4 +- .../20250429113739_SsoExternalId.Designer.cs | 3118 +++++++++++++++++ .../20250429113739_SsoExternalId.cs | 43 + .../DatabaseContextModelSnapshot.cs | 4 +- .../20250429113747_SsoExternalId.Designer.cs | 3101 ++++++++++++++++ .../20250429113747_SsoExternalId.cs | 21 + .../DatabaseContextModelSnapshot.cs | 2 +- 14 files changed, 9514 insertions(+), 9 deletions(-) create mode 100644 util/Migrator/DbScripts/2025-05-27_00_SsoExternalId.sql create mode 100644 util/MySqlMigrations/Migrations/20250429113731_SsoExternalId.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20250429113731_SsoExternalId.cs create mode 100644 util/PostgresMigrations/Migrations/20250429113739_SsoExternalId.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20250429113739_SsoExternalId.cs create mode 100644 util/SqliteMigrations/Migrations/20250429113747_SsoExternalId.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20250429113747_SsoExternalId.cs diff --git a/src/Core/Auth/Entities/SsoUser.cs b/src/Core/Auth/Entities/SsoUser.cs index 3199f00221..2e457afbc6 100644 --- a/src/Core/Auth/Entities/SsoUser.cs +++ b/src/Core/Auth/Entities/SsoUser.cs @@ -8,7 +8,7 @@ public class SsoUser : ITableObject public long Id { get; set; } public Guid UserId { get; set; } public Guid? OrganizationId { get; set; } - [MaxLength(50)] + [MaxLength(300)] public string ExternalId { get; set; } public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; diff --git a/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Create.sql b/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Create.sql index 6979a10ae2..7c1b168f5c 100644 --- a/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Create.sql +++ b/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Create.sql @@ -2,7 +2,7 @@ @Id BIGINT OUTPUT, @UserId UNIQUEIDENTIFIER, @OrganizationId UNIQUEIDENTIFIER, - @ExternalId NVARCHAR(50), + @ExternalId NVARCHAR(300), @CreationDate DATETIME2(7) AS BEGIN diff --git a/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Update.sql b/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Update.sql index facbc44572..bbd0720ba2 100644 --- a/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Update.sql +++ b/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Update.sql @@ -2,7 +2,7 @@ @Id BIGINT OUTPUT, @UserId UNIQUEIDENTIFIER, @OrganizationId UNIQUEIDENTIFIER, - @ExternalId NVARCHAR(50), + @ExternalId NVARCHAR(300), @CreationDate DATETIME2(7) AS BEGIN diff --git a/src/Sql/Auth/dbo/Tables/SsoUser.sql b/src/Sql/Auth/dbo/Tables/SsoUser.sql index f6477f56c2..2f8cd2f190 100644 --- a/src/Sql/Auth/dbo/Tables/SsoUser.sql +++ b/src/Sql/Auth/dbo/Tables/SsoUser.sql @@ -2,7 +2,7 @@ [Id] BIGINT IDENTITY (1, 1) NOT NULL, [UserId] UNIQUEIDENTIFIER NOT NULL, [OrganizationId] UNIQUEIDENTIFIER NULL, - [ExternalId] NVARCHAR(50) NOT NULL, + [ExternalId] NVARCHAR(300) NOT NULL, [CreationDate] DATETIME2 (7) NOT NULL, CONSTRAINT [PK_SsoUser] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [FK_SsoUser_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE, diff --git a/util/Migrator/DbScripts/2025-05-27_00_SsoExternalId.sql b/util/Migrator/DbScripts/2025-05-27_00_SsoExternalId.sql new file mode 100644 index 0000000000..3307f9e535 --- /dev/null +++ b/util/Migrator/DbScripts/2025-05-27_00_SsoExternalId.sql @@ -0,0 +1,67 @@ +IF EXISTS ( + SELECT 1 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = 'dbo' + AND TABLE_NAME = 'SsoUser' + AND COLUMN_NAME = 'ExternalId' + AND DATA_TYPE = 'nvarchar' + AND CHARACTER_MAXIMUM_LENGTH < 300 +) +BEGIN + -- Update table ExternalId column size + ALTER TABLE [dbo].[SsoUser] + ALTER COLUMN [ExternalId] NVARCHAR(300) NOT NULL +END +GO + +-- Update stored procedures to handle the new ExternalId column size +CREATE OR ALTER PROCEDURE [dbo].[SsoUser_Create] + @Id BIGINT OUTPUT, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @ExternalId NVARCHAR(300), + @CreationDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[SsoUser] + ( + [UserId], + [OrganizationId], + [ExternalId], + [CreationDate] + ) + VALUES + ( + @UserId, + @OrganizationId, + @ExternalId, + @CreationDate + ) + + SET @Id = SCOPE_IDENTITY(); +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[SsoUser_Update] + @Id BIGINT OUTPUT, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @ExternalId NVARCHAR(300), + @CreationDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[SsoUser] + SET + [UserId] = @UserId, + [OrganizationId] = @OrganizationId, + [ExternalId] = @ExternalId, + [CreationDate] = @CreationDate + WHERE + [Id] = @Id +END +GO diff --git a/util/MySqlMigrations/Migrations/20250429113731_SsoExternalId.Designer.cs b/util/MySqlMigrations/Migrations/20250429113731_SsoExternalId.Designer.cs new file mode 100644 index 0000000000..5db28d0542 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250429113731_SsoExternalId.Designer.cs @@ -0,0 +1,3112 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20250429113731_SsoExternalId")] + partial class SsoExternalId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .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("LimitCollectionCreation") + .HasColumnType("tinyint(1)"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("LimitItemDeletion") + .HasColumnType("tinyint(1)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("int"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("int"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property("Plan") + .IsRequired() + .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("SmSeats") + .HasColumnType("int"); + + b.Property("SmServiceAccounts") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseCustomPermissions") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UsePasswordManager") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property("UseRiskInsights") + .HasColumnType("tinyint(1)"); + + b.Property("UseScim") + .HasColumnType("tinyint(1)"); + + b.Property("UseSecretsManager") + .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.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Configuration") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Template") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.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") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasColumnType("longtext"); + + b.Property("BillingPhone") + .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("DiscountId") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasColumnType("longtext"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("longtext"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("varchar(25)"); + + b.Property("Approved") + .HasColumnType("tinyint(1)"); + + b.Property("AuthenticationDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MasterPasswordHash") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ResponseDate") + .HasColumnType("datetime(6)"); + + b.Property("ResponseDeviceId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ConsumedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + 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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + MySqlPropertyBuilderExtensions.UseMySqlIdentityColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AaGuid") + .HasColumnType("char(36)"); + + b.Property("Counter") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("varchar(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SupportsPrf") + .HasColumnType("tinyint(1)"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AssignedSeats") + .HasColumnType("int"); + + b.Property("ClientId") + .HasColumnType("char(36)"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Created") + .HasColumnType("datetime(6)"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("Total") + .HasColumnType("decimal(65,30)"); + + b.Property("UsedSeats") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllocatedSeats") + .HasColumnType("int"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("PurchasedSeats") + .HasColumnType("int"); + + b.Property("SeatMinimum") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("varchar(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("datetime(6)"); + + b.Property("ExpiresAtTime") + .HasColumnType("datetime(6)"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longblob"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.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") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Active") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("longtext"); + + b.Property("EncryptedPublicKey") + .HasColumnType("longtext"); + + b.Property("EncryptedUserKey") + .HasColumnType("longtext"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .IsRequired() + .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("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.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("DomainName") + .HasColumnType("longtext"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InstallationId") + .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("SecretId") + .HasColumnType("char(36)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SystemUser") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .IsRequired() + .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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + 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("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("JobRunCount") + .HasColumnType("int"); + + b.Property("LastCheckedDate") + .HasColumnType("datetime(6)"); + + b.Property("NextRunDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VerifiedDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("IsAdminInitiated") + .HasColumnType("tinyint(1)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("Notes") + .HasColumnType("longtext"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessSecretsManager") + .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("smallint"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + 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("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .IsRequired() + .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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.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("ProviderId") + .HasColumnType("char(36)"); + + 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("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.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("AvatarColor") + .HasMaxLength(7) + .HasColumnType("varchar(7)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .IsRequired() + .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("FailedLoginCount") + .HasColumnType("int"); + + 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("KdfMemory") + .HasColumnType("int"); + + b.Property("KdfParallelism") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LastEmailChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastFailedLoginDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKdfChangeDate") + .HasColumnType("datetime(6)"); + + b.Property("LastKeyRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("datetime(6)"); + + 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("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("VerifyDevices") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("varchar(3000)"); + + b.Property("ClientType") + .HasColumnType("tinyint unsigned"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Global") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Priority") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("TaskId") + .HasColumnType("char(36)"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("NotificationId") + .HasColumnType("char(36)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("ReadDate") + .HasColumnType("datetime(6)"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.Property("LastActivityDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("varchar(34)"); + + b.Property("Read") + .HasColumnType("tinyint(1)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Write") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("varchar(128)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpireAt") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("char(36)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Note") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Value") + .HasColumnType("longtext"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Uri") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.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("Key") + .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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("char(36)"); + + b.Property("SecretsId") + .HasColumnType("char(36)"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("char(36)") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/MySqlMigrations/Migrations/20250429113731_SsoExternalId.cs b/util/MySqlMigrations/Migrations/20250429113731_SsoExternalId.cs new file mode 100644 index 0000000000..2e7937b910 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20250429113731_SsoExternalId.cs @@ -0,0 +1,43 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class SsoExternalId : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "ExternalId", + table: "SsoUser", + type: "varchar(300)", + maxLength: 300, + nullable: true, + oldClrType: typeof(string), + oldType: "varchar(50)", + oldMaxLength: 50, + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "ExternalId", + table: "SsoUser", + type: "varchar(50)", + maxLength: 50, + nullable: true, + oldClrType: typeof(string), + oldType: "varchar(300)", + oldMaxLength: 300, + oldNullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + .OldAnnotation("MySql:CharSet", "utf8mb4"); + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index 0dd533b8d9..48015e2cf4 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -679,8 +679,8 @@ namespace Bit.MySqlMigrations.Migrations .HasColumnType("datetime(6)"); b.Property("ExternalId") - .HasMaxLength(50) - .HasColumnType("varchar(50)"); + .HasMaxLength(300) + .HasColumnType("varchar(300)"); b.Property("OrganizationId") .HasColumnType("char(36)"); diff --git a/util/PostgresMigrations/Migrations/20250429113739_SsoExternalId.Designer.cs b/util/PostgresMigrations/Migrations/20250429113739_SsoExternalId.Designer.cs new file mode 100644 index 0000000000..253769ad49 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250429113739_SsoExternalId.Designer.cs @@ -0,0 +1,3118 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20250429113739_SsoExternalId")] + partial class SsoExternalId + { + /// + 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("ProductVersion", "8.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .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 with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with 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("LimitCollectionCreation") + .HasColumnType("boolean"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("boolean"); + + b.Property("LimitItemDeletion") + .HasColumnType("boolean"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("integer"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("integer"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp with time zone"); + + b.Property("Plan") + .IsRequired() + .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 with time zone"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("SelfHost") + .HasColumnType("boolean"); + + b.Property("SmSeats") + .HasColumnType("integer"); + + b.Property("SmServiceAccounts") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseCustomPermissions") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UsePasswordManager") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .HasColumnType("boolean"); + + b.Property("UseRiskInsights") + .HasColumnType("boolean"); + + b.Property("UseScim") + .HasColumnType("boolean"); + + b.Property("UseSecretsManager") + .HasColumnType("boolean"); + + b.Property("UseSso") + .HasColumnType("boolean"); + + b.Property("UseTotp") + .HasColumnType("boolean"); + + b.Property("UsersGetPremium") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Id", "Enabled"), new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Configuration") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Template") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasColumnType("text"); + + b.Property("BillingPhone") + .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 with time zone"); + + b.Property("DiscountId") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasColumnType("text"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("text"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with 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 with 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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("character varying(25)"); + + b.Property("Approved") + .HasColumnType("boolean"); + + b.Property("AuthenticationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MasterPasswordHash") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RequestDeviceType") + .HasColumnType("smallint"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ResponseDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ResponseDeviceId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with 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 with time zone"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with 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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ConsumedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("OrganizationId", "ExternalId"), new[] { "UserId" }); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AaGuid") + .HasColumnType("uuid"); + + b.Property("Counter") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SupportsPrf") + .HasColumnType("boolean"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AssignedSeats") + .HasColumnType("integer"); + + b.Property("ClientId") + .HasColumnType("uuid"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("Total") + .HasColumnType("numeric"); + + b.Property("UsedSeats") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllocatedSeats") + .HasColumnType("integer"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("PurchasedSeats") + .HasColumnType("integer"); + + b.Property("SeatMinimum") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("character varying(449)"); + + b.Property("AbsoluteExpiration") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAtTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("bigint"); + + b.Property("Value") + .IsRequired() + .HasColumnType("bytea"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("text"); + + b.Property("EncryptedPublicKey") + .HasColumnType("text"); + + b.Property("EncryptedUserKey") + .HasColumnType("text"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.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 with time zone"); + + b.Property("DeviceType") + .HasColumnType("smallint"); + + b.Property("DomainName") + .HasColumnType("text"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("InstallationId") + .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("SecretId") + .HasColumnType("uuid"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SystemUser") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("JobRunCount") + .HasColumnType("integer"); + + b.Property("LastCheckedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextRunDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("text"); + + b.Property("VerifiedDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("IsAdminInitiated") + .HasColumnType("boolean"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Notes") + .HasColumnType("text"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessSecretsManager") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp with 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 with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with 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 with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .IsRequired() + .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", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp with 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("ProviderId") + .HasColumnType("uuid"); + + 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("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("character varying(7)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Culture") + .IsRequired() + .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("FailedLoginCount") + .HasColumnType("integer"); + + 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("KdfMemory") + .HasColumnType("integer"); + + b.Property("KdfParallelism") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LastEmailChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastFailedLoginDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKdfChangeDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("timestamp with time zone"); + + 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 with time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with 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("UsesKeyConnector") + .HasColumnType("boolean"); + + b.Property("VerifyDevices") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("character varying(3000)"); + + b.Property("ClientType") + .HasColumnType("smallint"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Global") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Priority") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskId") + .HasColumnType("uuid"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("NotificationId") + .HasColumnType("uuid"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ReadDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.Property("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("character varying(34)"); + + b.Property("Read") + .HasColumnType("boolean"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Write") + .HasColumnType("boolean"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpireAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .IsRequired() + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ServiceAccountId") + .HasColumnType("uuid"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Note") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .HasColumnType("text"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Uri") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("uuid"); + + b.Property("SecretsId") + .HasColumnType("uuid"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("uuid") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/PostgresMigrations/Migrations/20250429113739_SsoExternalId.cs b/util/PostgresMigrations/Migrations/20250429113739_SsoExternalId.cs new file mode 100644 index 0000000000..76ffeff061 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20250429113739_SsoExternalId.cs @@ -0,0 +1,43 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class SsoExternalId : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "ExternalId", + table: "SsoUser", + type: "character varying(300)", + maxLength: 300, + nullable: true, + collation: "postgresIndetermanisticCollation", + oldClrType: typeof(string), + oldType: "character varying(50)", + oldMaxLength: 50, + oldNullable: true, + oldCollation: "postgresIndetermanisticCollation"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn( + name: "ExternalId", + table: "SsoUser", + type: "character varying(50)", + maxLength: 50, + nullable: true, + collation: "postgresIndetermanisticCollation", + oldClrType: typeof(string), + oldType: "character varying(300)", + oldMaxLength: 300, + oldNullable: true, + oldCollation: "postgresIndetermanisticCollation"); + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index 5f9377d99d..09f4e910c2 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -682,8 +682,8 @@ namespace Bit.PostgresMigrations.Migrations .HasColumnType("timestamp with time zone"); b.Property("ExternalId") - .HasMaxLength(50) - .HasColumnType("character varying(50)") + .HasMaxLength(300) + .HasColumnType("character varying(300)") .UseCollation("postgresIndetermanisticCollation"); b.Property("OrganizationId") diff --git a/util/SqliteMigrations/Migrations/20250429113747_SsoExternalId.Designer.cs b/util/SqliteMigrations/Migrations/20250429113747_SsoExternalId.Designer.cs new file mode 100644 index 0000000000..85e3707d82 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250429113747_SsoExternalId.Designer.cs @@ -0,0 +1,3101 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20250429113747_SsoExternalId")] + partial class SsoExternalId + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowAdminAccessToAllCollectionItems") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("BillingEmail") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("LimitCollectionCreation") + .HasColumnType("INTEGER"); + + b.Property("LimitCollectionDeletion") + .HasColumnType("INTEGER"); + + b.Property("LimitItemDeletion") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxAutoscaleSmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("MaxCollections") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("TEXT"); + + b.Property("Plan") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("SelfHost") + .HasColumnType("INTEGER"); + + b.Property("SmSeats") + .HasColumnType("INTEGER"); + + b.Property("SmServiceAccounts") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("Use2fa") + .HasColumnType("INTEGER"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("INTEGER"); + + b.Property("UseApi") + .HasColumnType("INTEGER"); + + b.Property("UseCustomPermissions") + .HasColumnType("INTEGER"); + + b.Property("UseDirectory") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.Property("UseGroups") + .HasColumnType("INTEGER"); + + b.Property("UseKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("UsePasswordManager") + .HasColumnType("INTEGER"); + + b.Property("UsePolicies") + .HasColumnType("INTEGER"); + + b.Property("UseResetPassword") + .HasColumnType("INTEGER"); + + b.Property("UseRiskInsights") + .HasColumnType("INTEGER"); + + b.Property("UseScim") + .HasColumnType("INTEGER"); + + b.Property("UseSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("UseSso") + .HasColumnType("INTEGER"); + + b.Property("UseTotp") + .HasColumnType("INTEGER"); + + b.Property("UsersGetPremium") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationIntegration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Configuration") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EventType") + .HasColumnType("INTEGER"); + + b.Property("OrganizationIntegrationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Template") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationIntegrationId"); + + b.ToTable("OrganizationIntegrationConfiguration", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "Type") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Policy", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("BillingEmail") + .HasColumnType("TEXT"); + + b.Property("BillingPhone") + .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("TEXT"); + + b.Property("DiscountId") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Provider", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Settings") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCode") + .HasMaxLength(25) + .HasColumnType("TEXT"); + + b.Property("Approved") + .HasColumnType("INTEGER"); + + b.Property("AuthenticationDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHash") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("RequestCountryName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceIdentifier") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RequestDeviceType") + .HasColumnType("INTEGER"); + + b.Property("RequestIpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ResponseDate") + .HasColumnType("TEXT"); + + b.Property("ResponseDeviceId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ResponseDeviceId"); + + b.HasIndex("UserId"); + + b.ToTable("AuthRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("GranteeId") + .HasColumnType("TEXT"); + + b.Property("GrantorId") + .HasColumnType("TEXT"); + + b.Property("KeyEncrypted") + .HasColumnType("TEXT"); + + b.Property("LastNotificationDate") + .HasColumnType("TEXT"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("WaitTimeDays") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ConsumedDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasName("PK_Grant") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("Grant", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId"); + + b.HasIndex("OrganizationId", "ExternalId") + .IsUnique() + .HasAnnotation("Npgsql:IndexInclude", new[] { "UserId" }) + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SsoUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AaGuid") + .HasColumnType("TEXT"); + + b.Property("Counter") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("CredentialId") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasMaxLength(2000) + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SupportsPrf") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("WebAuthnCredential", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ClientOrganizationMigrationRecord", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("GatewayCustomerId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("INTEGER"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Seats") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId", "OrganizationId") + .IsUnique(); + + b.ToTable("ClientOrganizationMigrationRecord", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("InstallationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInstallation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AssignedSeats") + .HasColumnType("INTEGER"); + + b.Property("ClientId") + .HasColumnType("TEXT"); + + b.Property("ClientName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Created") + .HasColumnType("TEXT"); + + b.Property("InvoiceId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("InvoiceNumber") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PlanName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Total") + .HasColumnType("TEXT"); + + b.Property("UsedSeats") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderInvoiceItem", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllocatedSeats") + .HasColumnType("INTEGER"); + + b.Property("PlanType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("PurchasedSeats") + .HasColumnType("INTEGER"); + + b.Property("SeatMinimum") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("Id", "PlanType") + .IsUnique(); + + b.ToTable("ProviderPlan", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cache", b => + { + b.Property("Id") + .HasMaxLength(449) + .HasColumnType("TEXT"); + + b.Property("AbsoluteExpiration") + .HasColumnType("TEXT"); + + b.Property("ExpiresAtTime") + .HasColumnType("TEXT"); + + b.Property("SlidingExpirationInSeconds") + .HasColumnType("INTEGER"); + + b.Property("Value") + .IsRequired() + .HasColumnType("BLOB"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ExpiresAtTime") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Cache", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPrivateKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedPublicKey") + .HasColumnType("TEXT"); + + b.Property("EncryptedUserKey") + .HasColumnType("TEXT"); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "Identifier") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Device", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ActingUserId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DeviceType") + .HasColumnType("INTEGER"); + + b.Property("DomainName") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("InstallationId") + .HasColumnType("TEXT"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("PolicyId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderUserId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SystemUser") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Event", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("OrganizationUserId") + .HasColumnType("TEXT"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.ToTable("GroupUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Config") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DomainName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("JobRunCount") + .HasColumnType("INTEGER"); + + b.Property("LastCheckedDate") + .HasColumnType("TEXT"); + + b.Property("NextRunDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Txt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VerifiedDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationDomain", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("IsAdminInitiated") + .HasColumnType("INTEGER"); + + b.Property("LastSyncDate") + .HasColumnType("TEXT"); + + b.Property("Notes") + .HasColumnType("TEXT"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("PlanSponsorshipType") + .HasColumnType("INTEGER"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("TEXT"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("ToDelete") + .HasColumnType("INTEGER"); + + b.Property("ValidUntil") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.HasIndex("SponsoringOrganizationUserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationSponsorship", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessSecretsManager") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Permissions") + .HasColumnType("TEXT"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationUser", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletionDate") + .HasColumnType("TEXT"); + + b.Property("Disabled") + .HasColumnType("INTEGER"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("HideEmail") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("MaxAccessCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeletionDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Send", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER"); + + b.Property("Country") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("PostalCode") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Rate") + .HasColumnType("TEXT"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("TaxRate", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Amount") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PaymentMethodType") + .HasColumnType("INTEGER"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("Refunded") + .HasColumnType("INTEGER"); + + b.Property("RefundedAmount") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "OrganizationId", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Transaction", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccountRevisionDate") + .HasColumnType("TEXT"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("TEXT"); + + b.Property("AvatarColor") + .HasMaxLength(7) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Culture") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("EmailVerified") + .HasColumnType("INTEGER"); + + b.Property("EquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("TEXT"); + + b.Property("FailedLoginCount") + .HasColumnType("INTEGER"); + + b.Property("ForcePasswordReset") + .HasColumnType("INTEGER"); + + b.Property("Gateway") + .HasColumnType("INTEGER"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Kdf") + .HasColumnType("INTEGER"); + + b.Property("KdfIterations") + .HasColumnType("INTEGER"); + + b.Property("KdfMemory") + .HasColumnType("INTEGER"); + + b.Property("KdfParallelism") + .HasColumnType("INTEGER"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("LastEmailChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastFailedLoginDate") + .HasColumnType("TEXT"); + + b.Property("LastKdfChangeDate") + .HasColumnType("TEXT"); + + b.Property("LastKeyRotationDate") + .HasColumnType("TEXT"); + + b.Property("LastPasswordChangeDate") + .HasColumnType("TEXT"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("TEXT"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("MaxStorageGb") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Premium") + .HasColumnType("INTEGER"); + + b.Property("PremiumExpirationDate") + .HasColumnType("TEXT"); + + b.Property("PrivateKey") + .HasColumnType("TEXT"); + + b.Property("PublicKey") + .HasColumnType("TEXT"); + + b.Property("ReferenceData") + .HasColumnType("TEXT"); + + b.Property("RenewalReminderDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("VerifyDevices") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Body") + .HasMaxLength(3000) + .HasColumnType("TEXT"); + + b.Property("ClientType") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Global") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Priority") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("TaskId") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("TaskId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ClientType", "Global", "UserId", "OrganizationId", "Priority", "CreationDate") + .IsDescending(false, false, false, false, true, true) + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Notification", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("NotificationId") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("ReadDate") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "NotificationId") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("NotificationId"); + + b.ToTable("NotificationStatus", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("TEXT"); + + b.Property("LastActivityDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Installation", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .IsRequired() + .HasMaxLength(34) + .HasColumnType("TEXT"); + + b.Property("Read") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Write") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.ToTable("AccessPolicy", (string)null); + + b.HasDiscriminator().HasValue("AccessPolicy"); + + b.UseTphMappingStrategy(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ClientSecretHash") + .HasMaxLength(128) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("EncryptedPayload") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpireAt") + .HasColumnType("TEXT"); + + b.Property("Key") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Scope") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ServiceAccountId") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("ServiceAccountId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ApiKey", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Project", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Value") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("DeletedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("Secret", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("ServiceAccount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PasswordHealthReportApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Attachments") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .HasColumnType("TEXT"); + + b.Property("DeletedDate") + .HasColumnType("TEXT"); + + b.Property("Favorites") + .HasColumnType("TEXT"); + + b.Property("Folders") + .HasColumnType("TEXT"); + + b.Property("Key") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Reprompt") + .HasColumnType("INTEGER"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CipherId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SecurityTask", (string)null); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.Property("ProjectsId") + .HasColumnType("TEXT"); + + b.Property("SecretsId") + .HasColumnType("TEXT"); + + b.HasKey("ProjectsId", "SecretsId"); + + b.HasIndex("SecretsId"); + + b.ToTable("ProjectSecret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("GroupId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GroupId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("GroupId"); + + b.HasDiscriminator().HasValue("group_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("ServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("ServiceAccountId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("ServiceAccountId"); + + b.HasDiscriminator().HasValue("service_account_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedProjectId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedProjectId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedProjectId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_project"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedSecretId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedSecretId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedSecretId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_secret"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasBaseType("Bit.Infrastructure.EntityFramework.SecretsManager.Models.AccessPolicy"); + + b.Property("GrantedServiceAccountId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("GrantedServiceAccountId"); + + b.Property("OrganizationUserId") + .ValueGeneratedOnUpdateSometimes() + .HasColumnType("TEXT") + .HasColumnName("OrganizationUserId"); + + b.HasIndex("GrantedServiceAccountId"); + + b.HasIndex("OrganizationUserId"); + + b.HasDiscriminator().HasValue("user_service_account"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.AuthRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "ResponseDevice") + .WithMany() + .HasForeignKey("ResponseDeviceId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("ResponseDevice"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Platform.Installation", "Installation") + .WithMany() + .HasForeignKey("InstallationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Installation"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderInvoiceItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.ProviderPlan", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Collections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("GroupUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationDomain", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Domains") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") + .WithMany() + .HasForeignKey("TaskId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("Task"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.NotificationStatus", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.NotificationCenter.Models.Notification", "Notification") + .WithMany() + .HasForeignKey("NotificationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Notification"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ApiKeys") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Tools.Models.PasswordHealthReportApplication", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") + .WithMany() + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("ProjectSecret", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", null) + .WithMany() + .HasForeignKey("ProjectsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", null) + .WithMany() + .HasForeignKey("SecretsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedProject"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedSecret"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.GroupServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("GroupAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany("ProjectAccessPolicies") + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedProject"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccountSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("ServiceAccountAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") + .WithMany() + .HasForeignKey("ServiceAccountId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("ServiceAccount"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserProjectAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", "GrantedProject") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedProjectId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedProject"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserSecretAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "GrantedSecret") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedSecretId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedSecret"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.UserServiceAccountAccessPolicy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "GrantedServiceAccount") + .WithMany("UserAccessPolicies") + .HasForeignKey("GrantedServiceAccountId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId"); + + b.Navigation("GrantedServiceAccount"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Collections"); + + b.Navigation("Connections"); + + b.Navigation("Domains"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("Folders"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Project", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", b => + { + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ServiceAccountAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("GroupAccessPolicies"); + + b.Navigation("ProjectAccessPolicies"); + + b.Navigation("UserAccessPolicies"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/SqliteMigrations/Migrations/20250429113747_SsoExternalId.cs b/util/SqliteMigrations/Migrations/20250429113747_SsoExternalId.cs new file mode 100644 index 0000000000..220c739a59 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20250429113747_SsoExternalId.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class SsoExternalId : Migration +{ + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index 428ad4bfa3..6068941f2b 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -668,7 +668,7 @@ namespace Bit.SqliteMigrations.Migrations .HasColumnType("TEXT"); b.Property("ExternalId") - .HasMaxLength(50) + .HasMaxLength(300) .HasColumnType("TEXT"); b.Property("OrganizationId") From c39b6dea98d6b3e6195ef9984e0e670fcdcfddab Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 11:31:18 -0400 Subject: [PATCH 097/114] [deps] DbOps: Update Microsoft.Azure.Cosmos to 3.51.0 (#5866) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- src/Core/Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 633c3452d9..ac69f31ffe 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -36,7 +36,7 @@ - + From 359d0028816eef28ec1796afa6bd99d8e1561552 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 May 2025 18:59:05 +0200 Subject: [PATCH 098/114] [deps] Tools: Update MailKit to 4.12.1 (#5881) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> --- src/Core/Core.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index ac69f31ffe..88ecaf8cef 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -34,7 +34,7 @@ - + From fbc8e06c998b6f73814f6b80af8d6d06195a4104 Mon Sep 17 00:00:00 2001 From: Vince Grassia <593223+vgrassia@users.noreply.github.com> Date: Tue, 27 May 2025 18:24:31 +0000 Subject: [PATCH 099/114] Update Renovate config (#5882) --- .github/renovate.json5 | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index ac34903c1b..5c1b259539 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -15,8 +15,7 @@ matchManagers: ["github-actions"], matchFileNames: [ ".github/workflows/publish.yml", - ".github/workflows/release.yml", - ".github/workflows/repository-management.yml" + ".github/workflows/release.yml" ], commitMessagePrefix: "[deps] BRE:", reviewers: ["team:dept-bre"], @@ -134,8 +133,8 @@ reviewers: ["team:dept-dbops"], }, { - matchPackageNames: ["CommandDotNet", "YamlDotNet"], - description: "DevOps owned dependencies", + matchPackageNames: ["YamlDotNet"], + description: "BRE owned dependencies", commitMessagePrefix: "[deps] BRE:", reviewers: ["team:dept-bre"], }, From 4f326da8daf99d1f5f0d00a281fbaf920170d329 Mon Sep 17 00:00:00 2001 From: Ike <137194738+ike-kottlowski@users.noreply.github.com> Date: Wed, 28 May 2025 10:27:34 -0400 Subject: [PATCH 100/114] [PM- 19438] emergency access docs (#5846) * doc: adding readme and comments to code for emergency access feature. * fix: renaming variable names to better match vocabulary around emergency access. --- .../Response/EmergencyAccessResponseModel.cs | 7 + src/Core/Auth/Enums/EmergencyAccessType.cs | 6 + .../EmergencyAccessService.cs | 104 +++++++------ .../IEmergencyAccessService.cs | 147 ++++++++++++++++++ .../Auth/Services/EmergencyAccess/readme.md | 95 +++++++++++ .../Auth/Services/IEmergencyAccessService.cs | 40 ----- 6 files changed, 309 insertions(+), 90 deletions(-) rename src/Core/Auth/Services/{Implementations => EmergencyAccess}/EmergencyAccessService.cs (82%) create mode 100644 src/Core/Auth/Services/EmergencyAccess/IEmergencyAccessService.cs create mode 100644 src/Core/Auth/Services/EmergencyAccess/readme.md delete mode 100644 src/Core/Auth/Services/IEmergencyAccessService.cs diff --git a/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs b/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs index 2fb9a67199..90b265715d 100644 --- a/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs +++ b/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs @@ -90,6 +90,13 @@ public class EmergencyAccessGrantorDetailsResponseModel : EmergencyAccessRespons public class EmergencyAccessTakeoverResponseModel : ResponseModel { + /// + /// Creates a new instance of the class. + /// + /// Consumed for the Encrypted Key value + /// consumed for the KDF configuration + /// name of the object + /// emergencyAccess cannot be null public EmergencyAccessTakeoverResponseModel(EmergencyAccess emergencyAccess, User grantor, string obj = "emergencyAccessTakeover") : base(obj) { if (emergencyAccess == null) diff --git a/src/Core/Auth/Enums/EmergencyAccessType.cs b/src/Core/Auth/Enums/EmergencyAccessType.cs index a3497cc287..6e4e6e7f56 100644 --- a/src/Core/Auth/Enums/EmergencyAccessType.cs +++ b/src/Core/Auth/Enums/EmergencyAccessType.cs @@ -2,6 +2,12 @@ public enum EmergencyAccessType : byte { + /// + /// Allows emergency contact to view the Grantor's data. + /// View = 0, + /// + /// Allows emergency contact to take over the Grantor's account by overwriting the Grantor's password. + /// Takeover = 1, } diff --git a/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs b/src/Core/Auth/Services/EmergencyAccess/EmergencyAccessService.cs similarity index 82% rename from src/Core/Auth/Services/Implementations/EmergencyAccessService.cs rename to src/Core/Auth/Services/EmergencyAccess/EmergencyAccessService.cs index 2418830ea7..6a8fe9dd17 100644 --- a/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs +++ b/src/Core/Auth/Services/EmergencyAccess/EmergencyAccessService.cs @@ -58,38 +58,38 @@ public class EmergencyAccessService : IEmergencyAccessService _removeOrganizationUserCommand = removeOrganizationUserCommand; } - public async Task InviteAsync(User invitingUser, string email, EmergencyAccessType type, int waitTime) + public async Task InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime) { - if (!await _userService.CanAccessPremium(invitingUser)) + if (!await _userService.CanAccessPremium(grantorUser)) { throw new BadRequestException("Not a premium user."); } - if (type == EmergencyAccessType.Takeover && invitingUser.UsesKeyConnector) + if (accessType == EmergencyAccessType.Takeover && grantorUser.UsesKeyConnector) { throw new BadRequestException("You cannot use Emergency Access Takeover because you are using Key Connector."); } var emergencyAccess = new EmergencyAccess { - GrantorId = invitingUser.Id, - Email = email.ToLowerInvariant(), + GrantorId = grantorUser.Id, + Email = emergencyContactEmail.ToLowerInvariant(), Status = EmergencyAccessStatusType.Invited, - Type = type, + Type = accessType, WaitTimeDays = waitTime, CreationDate = DateTime.UtcNow, RevisionDate = DateTime.UtcNow, }; await _emergencyAccessRepository.CreateAsync(emergencyAccess); - await SendInviteAsync(emergencyAccess, NameOrEmail(invitingUser)); + await SendInviteAsync(emergencyAccess, NameOrEmail(grantorUser)); return emergencyAccess; } - public async Task GetAsync(Guid emergencyAccessId, Guid userId) + public async Task GetAsync(Guid emergencyAccessId, Guid grantorId) { - var emergencyAccess = await _emergencyAccessRepository.GetDetailsByIdGrantorIdAsync(emergencyAccessId, userId); + var emergencyAccess = await _emergencyAccessRepository.GetDetailsByIdGrantorIdAsync(emergencyAccessId, grantorId); if (emergencyAccess == null) { throw new BadRequestException("Emergency Access not valid."); @@ -98,19 +98,19 @@ public class EmergencyAccessService : IEmergencyAccessService return emergencyAccess; } - public async Task ResendInviteAsync(User invitingUser, Guid emergencyAccessId) + public async Task ResendInviteAsync(User grantorUser, Guid emergencyAccessId) { var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); - if (emergencyAccess == null || emergencyAccess.GrantorId != invitingUser.Id || + if (emergencyAccess == null || emergencyAccess.GrantorId != grantorUser.Id || emergencyAccess.Status != EmergencyAccessStatusType.Invited) { throw new BadRequestException("Emergency Access not valid."); } - await SendInviteAsync(emergencyAccess, NameOrEmail(invitingUser)); + await SendInviteAsync(emergencyAccess, NameOrEmail(grantorUser)); } - public async Task AcceptUserAsync(Guid emergencyAccessId, User user, string token, IUserService userService) + public async Task AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService) { var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); if (emergencyAccess == null) @@ -123,7 +123,7 @@ public class EmergencyAccessService : IEmergencyAccessService throw new BadRequestException("Invalid token."); } - if (!data.IsValid(emergencyAccessId, user.Email)) + if (!data.IsValid(emergencyAccessId, granteeUser.Email)) { throw new BadRequestException("Invalid token."); } @@ -140,7 +140,7 @@ public class EmergencyAccessService : IEmergencyAccessService // TODO PM-21687 // Might not be reachable since the Tokenable.IsValid() does an email comparison if (string.IsNullOrWhiteSpace(emergencyAccess.Email) || - !emergencyAccess.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase)) + !emergencyAccess.Email.Equals(granteeUser.Email, StringComparison.InvariantCultureIgnoreCase)) { throw new BadRequestException("User email does not match invite."); } @@ -148,7 +148,7 @@ public class EmergencyAccessService : IEmergencyAccessService var granteeEmail = emergencyAccess.Email; emergencyAccess.Status = EmergencyAccessStatusType.Accepted; - emergencyAccess.GranteeId = user.Id; + emergencyAccess.GranteeId = granteeUser.Id; emergencyAccess.Email = null; var grantor = await userService.GetUserByIdAsync(emergencyAccess.GrantorId); @@ -172,16 +172,16 @@ public class EmergencyAccessService : IEmergencyAccessService await _emergencyAccessRepository.DeleteAsync(emergencyAccess); } - public async Task ConfirmUserAsync(Guid emergencyAccessId, string key, Guid confirmingUserId) + public async Task ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId) { var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); if (emergencyAccess == null || emergencyAccess.Status != EmergencyAccessStatusType.Accepted || - emergencyAccess.GrantorId != confirmingUserId) + emergencyAccess.GrantorId != grantorId) { throw new BadRequestException("Emergency Access not valid."); } - var grantor = await _userRepository.GetByIdAsync(confirmingUserId); + var grantor = await _userRepository.GetByIdAsync(grantorId); if (emergencyAccess.Type == EmergencyAccessType.Takeover && grantor.UsesKeyConnector) { throw new BadRequestException("You cannot use Emergency Access Takeover because you are using Key Connector."); @@ -198,14 +198,14 @@ public class EmergencyAccessService : IEmergencyAccessService return emergencyAccess; } - public async Task SaveAsync(EmergencyAccess emergencyAccess, User savingUser) + public async Task SaveAsync(EmergencyAccess emergencyAccess, User grantorUser) { - if (!await _userService.CanAccessPremium(savingUser)) + if (!await _userService.CanAccessPremium(grantorUser)) { throw new BadRequestException("Not a premium user."); } - if (emergencyAccess.GrantorId != savingUser.Id) + if (emergencyAccess.GrantorId != grantorUser.Id) { throw new BadRequestException("Emergency Access not valid."); } @@ -222,10 +222,11 @@ public class EmergencyAccessService : IEmergencyAccessService await _emergencyAccessRepository.ReplaceAsync(emergencyAccess); } - public async Task InitiateAsync(Guid id, User initiatingUser) + // TODO PM-21687: rename this to something like InitiateRecoveryAsync, and something similar for Approve and Reject + public async Task InitiateAsync(Guid emergencyAccessId, User granteeUser) { - var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); - if (emergencyAccess == null || emergencyAccess.GranteeId != initiatingUser.Id || + var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); + if (emergencyAccess == null || emergencyAccess.GranteeId != granteeUser.Id || emergencyAccess.Status != EmergencyAccessStatusType.Confirmed) { throw new BadRequestException("Emergency Access not valid."); @@ -245,14 +246,14 @@ public class EmergencyAccessService : IEmergencyAccessService emergencyAccess.LastNotificationDate = now; await _emergencyAccessRepository.ReplaceAsync(emergencyAccess); - await _mailService.SendEmergencyAccessRecoveryInitiated(emergencyAccess, NameOrEmail(initiatingUser), grantor.Email); + await _mailService.SendEmergencyAccessRecoveryInitiated(emergencyAccess, NameOrEmail(granteeUser), grantor.Email); } - public async Task ApproveAsync(Guid id, User approvingUser) + public async Task ApproveAsync(Guid emergencyAccessId, User grantorUser) { - var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); + var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); - if (emergencyAccess == null || emergencyAccess.GrantorId != approvingUser.Id || + if (emergencyAccess == null || emergencyAccess.GrantorId != grantorUser.Id || emergencyAccess.Status != EmergencyAccessStatusType.RecoveryInitiated) { throw new BadRequestException("Emergency Access not valid."); @@ -262,14 +263,14 @@ public class EmergencyAccessService : IEmergencyAccessService await _emergencyAccessRepository.ReplaceAsync(emergencyAccess); var grantee = await _userRepository.GetByIdAsync(emergencyAccess.GranteeId.Value); - await _mailService.SendEmergencyAccessRecoveryApproved(emergencyAccess, NameOrEmail(approvingUser), grantee.Email); + await _mailService.SendEmergencyAccessRecoveryApproved(emergencyAccess, NameOrEmail(grantorUser), grantee.Email); } - public async Task RejectAsync(Guid id, User rejectingUser) + public async Task RejectAsync(Guid emergencyAccessId, User grantorUser) { - var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); + var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); - if (emergencyAccess == null || emergencyAccess.GrantorId != rejectingUser.Id || + if (emergencyAccess == null || emergencyAccess.GrantorId != grantorUser.Id || (emergencyAccess.Status != EmergencyAccessStatusType.RecoveryInitiated && emergencyAccess.Status != EmergencyAccessStatusType.RecoveryApproved)) { @@ -280,17 +281,17 @@ public class EmergencyAccessService : IEmergencyAccessService await _emergencyAccessRepository.ReplaceAsync(emergencyAccess); var grantee = await _userRepository.GetByIdAsync(emergencyAccess.GranteeId.Value); - await _mailService.SendEmergencyAccessRecoveryRejected(emergencyAccess, NameOrEmail(rejectingUser), grantee.Email); + await _mailService.SendEmergencyAccessRecoveryRejected(emergencyAccess, NameOrEmail(grantorUser), grantee.Email); } - public async Task> GetPoliciesAsync(Guid id, User requestingUser) + public async Task> GetPoliciesAsync(Guid emergencyAccessId, User granteeUser) { // TODO PM-21687 // Should we look up policies here or just verify the EmergencyAccess is correct // and handle policy logic else where? Should this be a query/Command? - var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); + var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); - if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.Takeover)) + if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.Takeover)) { throw new BadRequestException("Emergency Access not valid."); } @@ -306,11 +307,12 @@ public class EmergencyAccessService : IEmergencyAccessService return policies; } - public async Task<(EmergencyAccess, User)> TakeoverAsync(Guid id, User requestingUser) + // TODO PM-21687: rename this to something like InitiateRecoveryTakeoverAsync + public async Task<(EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser) { - var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); + var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); - if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.Takeover)) + if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.Takeover)) { throw new BadRequestException("Emergency Access not valid."); } @@ -326,11 +328,12 @@ public class EmergencyAccessService : IEmergencyAccessService return (emergencyAccess, grantor); } - public async Task PasswordAsync(Guid id, User requestingUser, string newMasterPasswordHash, string key) + // TODO PM-21687: rename this to something like FinishRecoveryTakeoverAsync + public async Task PasswordAsync(Guid emergencyAccessId, User granteeUser, string newMasterPasswordHash, string key) { - var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); + var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); - if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.Takeover)) + if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.Takeover)) { throw new BadRequestException("Emergency Access not valid."); } @@ -392,11 +395,11 @@ public class EmergencyAccessService : IEmergencyAccessService } } - public async Task ViewAsync(Guid id, User requestingUser) + public async Task ViewAsync(Guid emergencyAccessId, User granteeUser) { - var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); + var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); - if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.View)) + if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.View)) { throw new BadRequestException("Emergency Access not valid."); } @@ -410,11 +413,11 @@ public class EmergencyAccessService : IEmergencyAccessService }; } - public async Task GetAttachmentDownloadAsync(Guid id, Guid cipherId, string attachmentId, User requestingUser) + public async Task GetAttachmentDownloadAsync(Guid emergencyAccessId, Guid cipherId, string attachmentId, User granteeUser) { - var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id); + var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId); - if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.View)) + if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.View)) { throw new BadRequestException("Emergency Access not valid."); } @@ -429,18 +432,19 @@ public class EmergencyAccessService : IEmergencyAccessService await _mailService.SendEmergencyAccessInviteEmailAsync(emergencyAccess, invitingUsersName, token); } + // TODO PM-21687: move this to the user entity -> User.GetNameOrEmail()? private static string NameOrEmail(User user) { return string.IsNullOrWhiteSpace(user.Name) ? user.Email : user.Name; } - /* * Checks if EmergencyAccess Object is null * Checks the requesting user is the same as the granteeUser (So we are checking for proper grantee action) * Status _must_ equal RecoveryApproved (This means the grantor has invited, the grantee has accepted, and the grantor has approved so the shared key exists but hasn't been exercised yet) * request type must equal the type of access requested (View or Takeover) */ + //TODO PM-21687: this IsValidRequest() checks the validity based on the granteeUser. There should be a complementary method for the grantorUser private static bool IsValidRequest( EmergencyAccess availableAccess, User requestingUser, diff --git a/src/Core/Auth/Services/EmergencyAccess/IEmergencyAccessService.cs b/src/Core/Auth/Services/EmergencyAccess/IEmergencyAccessService.cs new file mode 100644 index 0000000000..de695bbd7d --- /dev/null +++ b/src/Core/Auth/Services/EmergencyAccess/IEmergencyAccessService.cs @@ -0,0 +1,147 @@ +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Enums; +using Bit.Core.Auth.Models.Data; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Services; +using Bit.Core.Vault.Models.Data; + +namespace Bit.Core.Auth.Services; + +public interface IEmergencyAccessService +{ + /// + /// Invites a user via email to become an emergency contact for the Grantor user. The Grantor must have a premium subscription. + /// the grantor user must not be a member of the organization that uses KeyConnector. + /// + /// The user initiating the emergency contact request + /// Emergency contact + /// Type of emergency access allowed to the emergency contact + /// The amount of time to pass before the invite is auto confirmed + /// a new Emergency Access object + Task InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime); + /// + /// Sends an invite to the emergency contact associated with the emergency access id. + /// + /// The grantor. This must be the owner of the Emergency Access object + /// The Id of the emergency access being requested. + /// void + Task ResendInviteAsync(User grantorUser, Guid emergencyAccessId); + /// + /// A grantee user accepts the emergency contact request. This updates the emergency access status to be + /// "Accepted", this is the middle step before the grantor user confirms the request. + /// + /// Id of the emergency access object being acted on. + /// User being invited to be an emergency contact + /// the tokenable that was sent via email + /// service dependency + /// void + Task AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService); + /// + /// The creator of the emergency access request can delete the request. + /// + /// Id of the emergency access being acted on + /// Id of the owner trying to delete the emergency access request + /// void + Task DeleteAsync(Guid emergencyAccessId, Guid grantorId); + /// + /// The grantor user confirms the acceptance of the emergency contact request. This stores the encrypted key allowing the grantee + /// access based on the emergency access type. + /// + /// Id of the emergency access being acted on. + /// The grantor user key encrypted by the grantee public key; grantee.PubicKey(grantor.User.Key) + /// Id of grantor user + /// emergency access object associated with the Id passed in + Task ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId); + /// + /// Fetches an emergency access object. The grantor user must own the object being fetched. + /// + /// Id of emergency access object + /// Id of the owner of the emergency access object. + /// Details of the emergency access object + Task GetAsync(Guid emergencyAccessId, Guid grantorId); + /// + /// Updates the emergency access object. + /// + /// emergency access entity being updated + /// grantor user + /// void + Task SaveAsync(EmergencyAccess emergencyAccess, User grantorUser); + /// + /// Initiates the recovery process. For either Takeover or view. Will send an email to the Grantor User notifying of the initiation. + /// + /// EmergencyAccess Id + /// grantee user + /// void + Task InitiateAsync(Guid emergencyAccessId, User granteeUser); + /// + /// Approves a recovery request. Sets the EmergencyAccess.Status to RecoveryApproved. + /// + /// emergency access id + /// grantor user + /// void + Task ApproveAsync(Guid emergencyAccessId, User grantorUser); + /// + /// Rejects a recovery request. Sets the EmergencyAccess.Status to Confirmed. This does not remove the emergency access entity. The + /// Grantee user can still initiate another recovery request. + /// + /// emergency access id + /// grantor user + /// void + Task RejectAsync(Guid emergencyAccessId, User grantorUser); + /// + /// This request is made by the Grantee user to fetch the policies for the Grantor User. + /// The Grantor User has to be the owner of the organization. + /// If the Grantor user has OrganizationUserType.Owner then the policies for the _Grantor_ user + /// are returned. This is used to ensure the password is of the proper complexity for the organization. + /// + /// EmergencyAccess.Id being acted on + /// User making the request, this is the Grantee + /// null if the GrantorUser is not an organization owner; A list of policies otherwise. + Task> GetPoliciesAsync(Guid emergencyAccessId, User granteeUser); + /// + /// Fetches the emergency access entity and grantor user. The grantor user is returned so the correct KDF configuration is + /// used for the new password. + /// + /// Id of entity being accessed + /// grantee user of the emergency access entity + /// emergency access entity and the grantorUser + Task<(EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser); + /// + /// Updates the grantor's password hash and updates the key for the EmergencyAccess entity. + /// + /// Emergency Access Id being acted on + /// user making the request + /// new password hash set by grantee user + /// new encrypted user key + /// void + Task PasswordAsync(Guid emergencyAccessId, User granteeUser, string newMasterPasswordHash, string key); + /// + /// sends a reminder email that there is a pending request for recovery. + /// + /// void + Task SendNotificationsAsync(); + /// + /// This handles the auto approval of recovery requests started in the method. + /// An email will be sent to the Grantee and the Grantor notifying each the recovery has been approved. + /// + /// void + Task HandleTimedOutRequestsAsync(); + /// + /// Fetched ciphers from the grantors account for the grantee to view. + /// + /// Emergency access entity being acted on + /// user requesting cipher items + /// ciphers associated with the emergency access request + Task ViewAsync(Guid emergencyAccessId, User granteeUser); + /// + /// Returns attachment if the grantee user has access to the cipher through the emergency access entity. + /// + /// EmergencyAccess entity being acted on + /// cipher entity containing the attachment + /// Attachment entity + /// user making the request + /// attachment response + Task GetAttachmentDownloadAsync(Guid emergencyAccessId, Guid cipherId, string attachmentId, User granteeUser); +} diff --git a/src/Core/Auth/Services/EmergencyAccess/readme.md b/src/Core/Auth/Services/EmergencyAccess/readme.md new file mode 100644 index 0000000000..e2bdec3916 --- /dev/null +++ b/src/Core/Auth/Services/EmergencyAccess/readme.md @@ -0,0 +1,95 @@ +# Emergency Access System +This system allows users to share their `User.Key` with other users using public key exchange. An emergency contact (a grantee user) can view or takeover (reset the password) of the grantor user. + +When an account is taken over all two factor methods are turned off and device verification is disabled. + +This system is affected by the Key Rotation feature. The `EmergencyAccess.KeyEncrypted` is the `Grantor.UserKey` encrypted by the `Grantee.PublicKey`. So if the `User.Key` is rotated then all `EmergencyAccess` entities will need to be updated. + +## Special Cases +Users who use `KeyConnector` are not able to allow `Takeover` of their accounts. However, they can allow `View`. + +When a grantee user _takes over_ a grantor user's account, the grantor user will be removed from all organizations where the grantor user is not the `OrganizationUserType.Owner`. A grantor user will not be removed from organizations if the `EmergencyAccessType` is `View`. The grantee user will only be able to `View` the grantor user's ciphers, and not any of the organization ciphers, if any exist. + +## Step 1. Invitation + +A grantor user invites another user to be their emergency contact, the grantee. This will create a new `EmergencyAccess` entity in the database with the `EmergencyAccessStatusType` set to `Invited`. +The `EmergencyAccess.KeyEncrypted` field is empty, and the `GranteeId` is `null` since the user being invited via email may not have an account yet. + +### code +```csharp +// creates entity. +Task InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime); +// resend email to the EmergencyAccess.Email. +Task ResendInviteAsync(User grantorUser, Guid emergencyAccessId); +``` + +## Step 2. Acceptance + +The grantee user receives an email they have been invited to be an emergency contact for a grantor user. + +At this point the grantee user can accept the request. This will set the `EmergencyAccess.GranteeId` to the `User.Id` of the grantee user. The `EmergencyAccess.Status` is set to `Accepted`. + +If the grantee user does not have an account then they can create an account and accept the invitation. + +### Code +```csharp +// accepts the request to be an emergency contact. +Task AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService); +``` + +## Step 3. Confirmation + +Once the grantee user has accepted, the `EmergencyAccess.GranteeId` allows the grantor user the ability to query for the `GranteeUser.PublicKey`. With the `Grantee.PublicKey`, the grantor on the client is able to safely encrypt their `User.Key` and save the encrypted string to the database. + +The `EmergencyAccess.Status` is set to `Confirmed`, and the `EmergencyAccess.KeyEncrypted` is set. + +### Code +```csharp +Task ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId); +``` + +## Step 4. Recovery Approval + +The grantee user can now exercise the ability to view or takeover the account. This is done by initiating the recovery. Initiating recovery has a time delay specified by `EmergencyAccess.WaitTime`. `WaitTime` is set in the initial invite. The grantor user can approve the request before the `WaitTime`, but they _cannot_ reject the request _after_ the `WaitTime` has completed. If the recovery request is not rejected then once the `WaitTime` has passed the grantee user will be able to access the emergency access entity. + +### Code +```csharp +// Initiates the recovery process; Will set EmergencyAccess.Status to RecoveryInitiated. +Task InitiateAsync(Guid id, User granteeUser); +// Approved the recovery request; Will set EmergencyAccess.Status to RecoveryApproved. +Task ApproveAsync(Guid id, User approvingUser); +// Rejects the recovery request; Will set EmergencyAccess.Status to Confirmed. +Task RejectAsync(Guid id, User rejectingUser); +// Automatically set the EmergencyAccess.Status to RecoveryApproved after WaitTime has passed. +Task HandleTimedOutRequestsAsync(); +``` + +## Step 5. Recovering the account + +Once the `EmergencyAccess.Status` is `RecoveryApproved` the grantee user is able to exercise their ability to view or takeover the grantor account. Viewing allows the grantee user to view the vault data of the grantor user. Takeover allows the grantee to change the password of the grantor user. + +### Takeover +`TakeoverAsync(Guid, User)` returns the grantor user object along with the `EmergencyAccess` entity. The grantor user object is required since to update the password the client needs access to the grantor kdf configuration. Once the password has been set in the `PasswordAsync(Guid, User, string, string)` the account has been successfully recovered. + +Taking over the account will change the password of the grantor user, empty the two factor array on the grantor user, and disable device verification. + +```csharp +// Takeover returns the grantor user and the emergency access entity. +Task<(EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser); +// Password sets the password for the grantor user. +Task PasswordAsync(Guid emergencyAccessId, User granteeUser, string newMasterPasswordHash, string key); +// Returns Ciphers the Grantee is allowed to view based on the EmergencyAccess status. +Task ViewAsync(Guid emergencyAccessId, User granteeUser); +// Returns downloadable cipher attachments based on the EmergencyAccess status. +Task GetAttachmentDownloadAsync(Guid emergencyAccessId, Guid cipherId, string attachmentId, User granteeUser); +``` + +## Optional steps + +The grantor user is able to delete an emergency contact at anytime, at any point in the recovery process. + +### Code +```csharp +// deletes the associated EmergencyAccess Entity +Task DeleteAsync(Guid emergencyAccessId, Guid grantorId); +``` diff --git a/src/Core/Auth/Services/IEmergencyAccessService.cs b/src/Core/Auth/Services/IEmergencyAccessService.cs deleted file mode 100644 index 6dd17151e6..0000000000 --- a/src/Core/Auth/Services/IEmergencyAccessService.cs +++ /dev/null @@ -1,40 +0,0 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Auth.Entities; -using Bit.Core.Auth.Enums; -using Bit.Core.Auth.Models.Data; -using Bit.Core.Entities; -using Bit.Core.Enums; -using Bit.Core.Services; -using Bit.Core.Vault.Models.Data; - -namespace Bit.Core.Auth.Services; - -public interface IEmergencyAccessService -{ - Task InviteAsync(User invitingUser, string email, EmergencyAccessType type, int waitTime); - Task ResendInviteAsync(User invitingUser, Guid emergencyAccessId); - Task AcceptUserAsync(Guid emergencyAccessId, User user, string token, IUserService userService); - Task DeleteAsync(Guid emergencyAccessId, Guid grantorId); - Task ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId); - Task GetAsync(Guid emergencyAccessId, Guid userId); - Task SaveAsync(EmergencyAccess emergencyAccess, User savingUser); - Task InitiateAsync(Guid id, User initiatingUser); - Task ApproveAsync(Guid id, User approvingUser); - Task RejectAsync(Guid id, User rejectingUser); - /// - /// This request is made by the Grantee user to fetch the policies for the Grantor User. - /// The Grantor User has to be the owner of the organization. - /// If the Grantor user has OrganizationUserType.Owner then the policies for the _Grantor_ user - /// are returned. - /// - /// EmergencyAccess.Id being acted on - /// User making the request, this is the Grantee - /// null if the GrantorUser is not an organization owner; A list of policies otherwise. - Task> GetPoliciesAsync(Guid id, User requestingUser); - Task<(EmergencyAccess, User)> TakeoverAsync(Guid id, User initiatingUser); - Task PasswordAsync(Guid id, User user, string newMasterPasswordHash, string key); - Task SendNotificationsAsync(); - Task HandleTimedOutRequestsAsync(); - Task ViewAsync(Guid id, User user); - Task GetAttachmentDownloadAsync(Guid id, Guid cipherId, string attachmentId, User user); -} From 31b6b47eac8b3e6cd8139bf5b937bfa4c837c34f Mon Sep 17 00:00:00 2001 From: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Date: Wed, 28 May 2025 10:37:10 -0400 Subject: [PATCH 101/114] [PM-20650] Adding feature flag to server for Remove Card Item Type policy (#5830) * Adding feature flag to server for Remove Card Item Type policy * Updating new feature flag name --- src/Core/Constants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 1c31ffaab4..7a2b3c9ac7 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -202,6 +202,7 @@ public static class FeatureFlagKeys public const string EndUserNotifications = "pm-10609-end-user-notifications"; public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions"; public const string PhishingDetection = "phishing-detection"; + public const string RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy"; public static List GetAllKeys() { From cd994f72bf38326438bcb4057475472e00639c86 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Wed, 28 May 2025 12:06:25 -0400 Subject: [PATCH 102/114] change verbiage, add column to table (#5888) --- .../AdminConsole/Models/ProviderViewModel.cs | 4 +-- .../Views/Providers/Admins.cshtml | 29 +++++++++++++------ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/Admin/AdminConsole/Models/ProviderViewModel.cs b/src/Admin/AdminConsole/Models/ProviderViewModel.cs index 2d4ba5012c..e1277f8e87 100644 --- a/src/Admin/AdminConsole/Models/ProviderViewModel.cs +++ b/src/Admin/AdminConsole/Models/ProviderViewModel.cs @@ -19,7 +19,7 @@ public class ProviderViewModel { Provider = provider; UserCount = providerUsers.Count(); - ProviderAdmins = providerUsers.Where(u => u.Type == ProviderUserType.ProviderAdmin); + ProviderUsers = providerUsers; ProviderOrganizations = organizations.Where(o => o.ProviderId == provider.Id); if (Provider.Type == ProviderType.Msp) @@ -61,7 +61,7 @@ public class ProviderViewModel public int UserCount { get; set; } public Provider Provider { get; set; } - public IEnumerable ProviderAdmins { get; set; } + public IEnumerable ProviderUsers { get; set; } public IEnumerable ProviderOrganizations { get; set; } public List ProviderPlanViewModels { get; set; } = []; } diff --git a/src/Admin/AdminConsole/Views/Providers/Admins.cshtml b/src/Admin/AdminConsole/Views/Providers/Admins.cshtml index 86043f3a6d..29eddc8964 100644 --- a/src/Admin/AdminConsole/Views/Providers/Admins.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/Admins.cshtml @@ -7,7 +7,7 @@ var canResendEmailInvite = AccessControlService.UserHasPermission(Permission.Provider_ResendEmailInvite); } -

    Provider Admins

    +

    Administrators

    @@ -15,12 +15,13 @@ Email + Role Status - @if(!Model.ProviderAdmins.Any()) + @if(!Model.ProviderUsers.Any()) { No results to list. @@ -28,29 +29,39 @@ } else { - @foreach(var admin in Model.ProviderAdmins) + @foreach(var user in Model.ProviderUsers) { - @admin.Email + @user.Email - @admin.Status + @if(@user.Type == 0) + { + Provider Admin + } + else + { + Service User + } + + + @user.Status - @if(admin.Status.Equals(ProviderUserStatusType.Confirmed) + @if(user.Status.Equals(ProviderUserStatusType.Confirmed) && @Model.Provider.Status.Equals(ProviderStatusType.Pending) && canResendEmailInvite) { - @if(@TempData["InviteResentTo"] != null && @TempData["InviteResentTo"].ToString() == @admin.UserId.Value.ToString()) + @if(@TempData["InviteResentTo"] != null && @TempData["InviteResentTo"].ToString() == @user.UserId.Value.ToString()) { } else { Resend Setup Invite From 1e7d02bca27403c549aed4d87a341c6592ba9135 Mon Sep 17 00:00:00 2001 From: aj-bw <81774843+aj-bw@users.noreply.github.com> Date: Wed, 28 May 2025 12:43:15 -0400 Subject: [PATCH 103/114] BRE-857/collect-code-references-fails-against-tags (#5885) * update code-references file to also check tags * fix line --- .github/workflows/code-references.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/code-references.yml b/.github/workflows/code-references.yml index 30fbff32ed..359e64eb57 100644 --- a/.github/workflows/code-references.yml +++ b/.github/workflows/code-references.yml @@ -44,6 +44,7 @@ jobs: with: accessToken: ${{ secrets.LD_ACCESS_TOKEN }} projKey: default + allowTags: true - name: Add label if: steps.collect.outputs.any-changed == 'true' From e2419496017b17c163868dcc67359eca113dd373 Mon Sep 17 00:00:00 2001 From: Github Actions Date: Wed, 28 May 2025 17:04:11 +0000 Subject: [PATCH 104/114] Bumped version to 2025.5.2 --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index ac814ef8d8..f403c0f692 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -3,7 +3,7 @@ net8.0 - 2025.5.1 + 2025.5.2 Bit.$(MSBuildProjectName) enable From e19bee4195b89a7514b8282c9366877d6edd7c56 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 28 May 2025 10:21:55 -0700 Subject: [PATCH 105/114] [PM-22146] Replace JSON_PATH_EXISTS with JSON_QUERY to support older MSSQL versions (#5891) --- .../Cipher/Cipher_DeleteAttachment.sql | 2 +- ...05-28_00_RemoveUnsupportedJsonFunction.sql | 78 +++++++++++++++++++ 2 files changed, 79 insertions(+), 1 deletion(-) create mode 100644 util/Migrator/DbScripts/2025-05-28_00_RemoveUnsupportedJsonFunction.sql diff --git a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteAttachment.sql b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteAttachment.sql index 75a0468b42..4a2e3187c7 100644 --- a/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteAttachment.sql +++ b/src/Sql/Vault/dbo/Stored Procedures/Cipher/Cipher_DeleteAttachment.sql @@ -36,7 +36,7 @@ BEGIN END -- Check if the attachment exists before trying to remove it - IF JSON_PATH_EXISTS(@CurrentAttachments, @AttachmentIdPath) = 0 + IF JSON_QUERY(@CurrentAttachments, @AttachmentIdPath) IS NULL BEGIN -- Attachment doesn't exist, nothing to do RETURN; diff --git a/util/Migrator/DbScripts/2025-05-28_00_RemoveUnsupportedJsonFunction.sql b/util/Migrator/DbScripts/2025-05-28_00_RemoveUnsupportedJsonFunction.sql new file mode 100644 index 0000000000..3d5cc8dd84 --- /dev/null +++ b/util/Migrator/DbScripts/2025-05-28_00_RemoveUnsupportedJsonFunction.sql @@ -0,0 +1,78 @@ +CREATE OR ALTER PROCEDURE [dbo].[Cipher_DeleteAttachment] + @Id UNIQUEIDENTIFIER, + @AttachmentId VARCHAR(50) +AS +BEGIN + SET NOCOUNT ON + + DECLARE @AttachmentIdKey VARCHAR(50) = CONCAT('"', @AttachmentId, '"') + DECLARE @AttachmentIdPath VARCHAR(50) = CONCAT('$.', @AttachmentIdKey) + + DECLARE @UserId UNIQUEIDENTIFIER + DECLARE @OrganizationId UNIQUEIDENTIFIER + DECLARE @CurrentAttachments NVARCHAR(MAX) + DECLARE @NewAttachments NVARCHAR(MAX) + + -- Get current cipher data + SELECT + @UserId = [UserId], + @OrganizationId = [OrganizationId], + @CurrentAttachments = [Attachments] + FROM + [dbo].[Cipher] + WHERE [Id] = @Id + + -- If there are no attachments, nothing to do + IF @CurrentAttachments IS NULL + BEGIN + RETURN; + END + + -- Validate the initial JSON + IF ISJSON(@CurrentAttachments) = 0 + BEGIN + THROW 50000, 'Current initial attachments data is not valid JSON', 1; + RETURN; + END + + -- Check if the attachment exists before trying to remove it + IF JSON_QUERY(@CurrentAttachments, @AttachmentIdPath) IS NULL + BEGIN + -- Attachment doesn't exist, nothing to do + RETURN; + END + + -- Create the new attachments JSON with the specified attachment removed + SET @NewAttachments = JSON_MODIFY(@CurrentAttachments, @AttachmentIdPath, NULL) + + -- Validate the resulting JSON + IF ISJSON(@NewAttachments) = 0 + BEGIN + THROW 50000, 'Failed to create valid JSON when removing attachment', 1; + RETURN; + END + + -- Check if we've removed all attachments and have an empty object + IF @NewAttachments = '{}' + BEGIN + -- If we have an empty JSON object, set to NULL instead + SET @NewAttachments = NULL; + END + + -- Update with validated JSON + UPDATE [dbo].[Cipher] + SET [Attachments] = @NewAttachments + WHERE [Id] = @Id + + IF @OrganizationId IS NOT NULL + BEGIN + EXEC [dbo].[Organization_UpdateStorage] @OrganizationId + EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId + END + ELSE IF @UserId IS NOT NULL + BEGIN + EXEC [dbo].[User_UpdateStorage] @UserId + EXEC [dbo].[User_BumpAccountRevisionDate] @UserId + END +END +GO From 9ad2d61303c4ac762cf5961dc3ef53715d6dd478 Mon Sep 17 00:00:00 2001 From: Shane Melton Date: Wed, 28 May 2025 11:31:07 -0700 Subject: [PATCH 106/114] [PM-22146] Remove reference to JSON_PATH_EXISTS for old migration scripts for SH instances running older SQL Server versions (#5894) --- .../DbScripts/2025-04-16_00_AttachmentJsonValidation.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/util/Migrator/DbScripts/2025-04-16_00_AttachmentJsonValidation.sql b/util/Migrator/DbScripts/2025-04-16_00_AttachmentJsonValidation.sql index a501b28574..9398845dc4 100644 --- a/util/Migrator/DbScripts/2025-04-16_00_AttachmentJsonValidation.sql +++ b/util/Migrator/DbScripts/2025-04-16_00_AttachmentJsonValidation.sql @@ -129,7 +129,7 @@ BEGIN END -- Check if the attachment exists before trying to remove it - IF JSON_PATH_EXISTS(@CurrentAttachments, @AttachmentIdPath) = 0 + IF JSON_QUERY(@CurrentAttachments, @AttachmentIdPath) IS NULL BEGIN -- Attachment doesn't exist, nothing to do RETURN; From fe6181f55f2066f4b164bb98749565f1d29b5dc2 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Wed, 28 May 2025 16:44:18 -0400 Subject: [PATCH 107/114] fix(identity): [PM-21975] Add Security Stamp claim to persisted grant * Added Security Stamp claim to refresh_token * Linting * Added better comments. * Added clarification to naming of new method. * Updated comments. * Added more comments. * Misspelling --- src/Core/Utilities/CoreHelpers.cs | 1 + src/Identity/IdentityServer/ProfileService.cs | 4 + .../RequestValidators/BaseRequestValidator.cs | 111 ++++++++++++------ 3 files changed, 83 insertions(+), 33 deletions(-) diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index eebcb00738..ab1537afd5 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -712,6 +712,7 @@ public static class CoreHelpers new(Claims.Premium, isPremium ? "true" : "false"), new(JwtClaimTypes.Email, user.Email), new(JwtClaimTypes.EmailVerified, user.EmailVerified ? "true" : "false"), + // TODO: [https://bitwarden.atlassian.net/browse/PM-22171] Remove this since it is already added from the persisted grant new(Claims.SecurityStamp, user.SecurityStamp), }; diff --git a/src/Identity/IdentityServer/ProfileService.cs b/src/Identity/IdentityServer/ProfileService.cs index 09866c6b57..d7d6708374 100644 --- a/src/Identity/IdentityServer/ProfileService.cs +++ b/src/Identity/IdentityServer/ProfileService.cs @@ -72,6 +72,10 @@ public class ProfileService : IProfileService public async Task IsActiveAsync(IsActiveContext context) { + // We add the security stamp claim to the persisted grant when we issue the refresh token. + // IdentityServer will add this claim to the subject, and here we evaluate whether the security stamp that + // was persisted matches the current security stamp of the user. If it does not match, then the user has performed + // an operation that we want to invalidate the refresh token. var securityTokenClaim = context.Subject?.Claims.FirstOrDefault(c => c.Type == Claims.SecurityStamp); var user = await _userService.GetUserByPrincipalAsync(context.Subject); diff --git a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs index 9afdcacf14..45c0c26b17 100644 --- a/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs +++ b/src/Identity/IdentityServer/RequestValidators/BaseRequestValidator.cs @@ -199,46 +199,26 @@ public abstract class BaseRequestValidator where T : class protected abstract Task ValidateContextAsync(T context, CustomValidatorRequestContext validatorContext); + + /// + /// Responsible for building the response to the client when the user has successfully authenticated. + /// + /// The authenticated user. + /// The current request context. + /// The device used for authentication. + /// Whether to send a 2FA remember token. protected async Task BuildSuccessResultAsync(User user, T context, Device device, bool sendRememberToken) { await _eventService.LogUserEventAsync(user.Id, EventType.User_LoggedIn); - var claims = new List(); + var claims = this.BuildSubjectClaims(user, context, device); - if (device != null) - { - claims.Add(new Claim(Claims.Device, device.Identifier)); - claims.Add(new Claim(Claims.DeviceType, device.Type.ToString())); - } - - var customResponse = new Dictionary(); - if (!string.IsNullOrWhiteSpace(user.PrivateKey)) - { - customResponse.Add("PrivateKey", user.PrivateKey); - } - - if (!string.IsNullOrWhiteSpace(user.Key)) - { - customResponse.Add("Key", user.Key); - } - - customResponse.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user)); - customResponse.Add("ForcePasswordReset", user.ForcePasswordReset); - customResponse.Add("ResetMasterPassword", string.IsNullOrWhiteSpace(user.MasterPassword)); - customResponse.Add("Kdf", (byte)user.Kdf); - customResponse.Add("KdfIterations", user.KdfIterations); - customResponse.Add("KdfMemory", user.KdfMemory); - customResponse.Add("KdfParallelism", user.KdfParallelism); - customResponse.Add("UserDecryptionOptions", await CreateUserDecryptionOptionsAsync(user, device, GetSubject(context))); - - if (sendRememberToken) - { - var token = await _userManager.GenerateTwoFactorTokenAsync(user, - CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember)); - customResponse.Add("TwoFactorToken", token); - } + var customResponse = await BuildCustomResponse(user, context, device, sendRememberToken); await ResetFailedAuthDetailsAsync(user); + + // Once we've built the claims and custom response, we can set the success result. + // We delegate this to the derived classes, as the implementation varies based on the grant type. await SetSuccessResult(context, user, claims, customResponse); } @@ -392,6 +372,71 @@ public abstract class BaseRequestValidator where T : class return new MasterPasswordPolicyResponseModel(await PolicyService.GetMasterPasswordPolicyForUserAsync(user)); } + /// + /// Builds the claims that will be stored on the persisted grant. + /// These claims are supplemented by the claims in the ProfileService when the access token is returned to the client. + /// + /// The authenticated user. + /// The current request context. + /// The device used for authentication. + private List BuildSubjectClaims(User user, T context, Device device) + { + // We are adding the security stamp claim to the list of claims that will be stored in the persisted grant. + // We need this because we check for changes in the stamp to determine if we need to invalidate token refresh requests, + // in the `ProfileService.IsActiveAsync` method. + // If we don't store the security stamp in the persisted grant, we won't have the previous value to compare against. + var claims = new List + { + new Claim(Claims.SecurityStamp, user.SecurityStamp) + }; + + if (device != null) + { + claims.Add(new Claim(Claims.Device, device.Identifier)); + claims.Add(new Claim(Claims.DeviceType, device.Type.ToString())); + } + return claims; + } + + /// + /// Builds the custom response that will be sent to the client upon successful authentication, which + /// includes the information needed for the client to initialize the user's account in state. + /// + /// The authenticated user. + /// The current request context. + /// The device used for authentication. + /// Whether to send a 2FA remember token. + private async Task> BuildCustomResponse(User user, T context, Device device, bool sendRememberToken) + { + var customResponse = new Dictionary(); + if (!string.IsNullOrWhiteSpace(user.PrivateKey)) + { + customResponse.Add("PrivateKey", user.PrivateKey); + } + + if (!string.IsNullOrWhiteSpace(user.Key)) + { + customResponse.Add("Key", user.Key); + } + + customResponse.Add("MasterPasswordPolicy", await GetMasterPasswordPolicyAsync(user)); + customResponse.Add("ForcePasswordReset", user.ForcePasswordReset); + customResponse.Add("ResetMasterPassword", string.IsNullOrWhiteSpace(user.MasterPassword)); + customResponse.Add("Kdf", (byte)user.Kdf); + customResponse.Add("KdfIterations", user.KdfIterations); + customResponse.Add("KdfMemory", user.KdfMemory); + customResponse.Add("KdfParallelism", user.KdfParallelism); + customResponse.Add("UserDecryptionOptions", await CreateUserDecryptionOptionsAsync(user, device, GetSubject(context))); + + if (sendRememberToken) + { + var token = await _userManager.GenerateTwoFactorTokenAsync(user, + CoreHelpers.CustomProviderName(TwoFactorProviderType.Remember)); + customResponse.Add("TwoFactorToken", token); + } + return customResponse; + } + #nullable enable /// /// Used to create a list of all possible ways the newly authenticated user can decrypt their vault contents From c7b0c30370ad320e9b974912a79b48473e4bd599 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Thu, 29 May 2025 07:30:23 +1000 Subject: [PATCH 108/114] Bump account revision date in OrgUserRepository (#5884) Match equivalent MSSQL logic. Also ensures that the revision date is bumped when the user is revoked. --- .../Repositories/OrganizationUserRepository.cs | 10 +++------- .../OrganizationUserReplaceTests.cs | 3 --- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs index 5ef59d51db..fc5626631a 100644 --- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs @@ -1,5 +1,4 @@ -using System.Diagnostics; -using AutoMapper; +using AutoMapper; using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.Enums; @@ -446,15 +445,12 @@ public class OrganizationUserRepository : Repository - /// - /// - /// [DatabaseTheory, DatabaseData] public async Task ReplaceAsync_WithCollectionAccess_WhenUserIsConfirmed_Success( IUserRepository userRepository, From 829ce8606603dc6b9a4ef4834a4cd0af49efd189 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rui=20Tom=C3=A9?= <108268980+r-tome@users.noreply.github.com> Date: Thu, 29 May 2025 07:40:30 +0100 Subject: [PATCH 109/114] =?UTF-8?q?[PM-18238]=C2=A0Add=20RequireTwoFactorP?= =?UTF-8?q?olicyRequirement=20(#5840)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add RequireTwoFactorPolicyRequirement and its factory with unit tests * Implemented RequireTwoFactorPolicyRequirement to enforce two-factor authentication policies. * Created RequireTwoFactorPolicyRequirementFactory to generate policy requirements based on user status. * Added unit tests for the factory to validate behavior with various user statuses and policy details. * Enhance AcceptOrgUserCommand to use IPolicyRequirementQuery for two-factor authentication validation * Update ConfirmOrganizationUserCommand to use RequireTwoFactorPolicyRequirement to check for 2FA requirement * Implement CanAcceptInvitation and CanBeConfirmed methods in RequireTwoFactorPolicyRequirement; update tests to reflect new logic for two-factor authentication policy handling. * Refactor AcceptOrgUserCommand to enforce two-factor authentication policy based on feature flag; update validation logic and tests accordingly. * Enhance ConfirmOrganizationUserCommand to validate two-factor authentication policy based on feature flag; refactor validation logic and update related tests for improved policy handling. * Remove unused method and its dependencies from OrganizationService. * Implement CanBeRestored method in RequireTwoFactorPolicyRequirement to determine user restoration eligibility based on two-factor authentication status; add corresponding unit tests for various scenarios. * Update RestoreOrganizationUserCommand to use IPolicyRequirementQuery for two-factor authentication policies checks * Remove redundant vNext tests * Add TwoFactorPoliciesForActiveMemberships property to RequireTwoFactorPolicyRequirement and corresponding unit tests for policy retrieval based on user status * Refactor UserService to integrate IPolicyRequirementQuery for two-factor authentication policy checks * Add XML documentation for TwoFactorPoliciesForActiveMemberships property in RequireTwoFactorPolicyRequirement to clarify its purpose and return value. * Add exception documentation for ValidateTwoFactorAuthenticationPolicyAsync method in ConfirmOrganizationUserCommand to clarify error handling for users without two-step login enabled. * Update comments in AcceptOrgUserCommand and ConfirmOrganizationUserCommand to clarify handling of two-step login and 2FA policy checks. * Add RequireTwoFactorPolicyRequirementFactory to PolicyServiceCollectionExtensions * Refactor two-factor authentication policy checks in AcceptOrgUserCommand and ConfirmOrganizationUserCommand to streamline validation logic and improve clarity. Update RequireTwoFactorPolicyRequirement to provide a method for checking if two-factor authentication is required for an organization. Adjust related unit tests accordingly. * Add PolicyRequirements namespace * Update comments in AcceptOrgUserCommand and ConfirmOrganizationUserCommand to clarify two-factor authentication policy requirements and exception handling. * Refactor RequireTwoFactorPolicyRequirement to return tuples of (OrganizationId, OrganizationUserId) for active memberships requiring two-factor authentication. Update UserService and related tests to reflect this change. * Refactor AcceptOrgUserCommand: delegate feature flag check to the ValidateTwoFactorAuthenticationPolicyAsync method * Skip policy check if two-step login is enabled for the user * Refactor ConfirmOrganizationUserCommand to streamline two-factor authentication policy validation logic * Refactor AcceptOrgUserCommand to simplify two-factor authentication check by removing intermediate variable * Update documentation in RequireTwoFactorPolicyRequirement to clarify the purpose of the IsTwoFactorRequiredForOrganization * Refactor AcceptOrgUserCommandTests to remove redundant two-factor authentication checks and simplify test setup * Refactor AcceptOrgUserCommand and ConfirmOrganizationUserCommand to streamline two-factor authentication checks by removing redundant conditions and simplifying logic flow. * Rename removeOrgUserTasks variable in UserService * Refactor RestoreOrganizationUserCommand to simplify two-factor authentication compliance checks by consolidating logic into a new method, IsTwoFactorRequiredForOrganizationAsync. * Remove outdated two-factor authentication validation documentation from AcceptOrgUserCommand * Invert two-factor compliance check in RestoreOrganizationUserCommand to ensure correct validation of organization user policies. * Refactor UserService to enhance two-factor compliance checks by optimizing organization retrieval and logging when no organizations require two-factor authentication. --- .../OrganizationUsers/AcceptOrgUserCommand.cs | 50 +++- .../ConfirmOrganizationUserCommand.cs | 50 +++- .../v1/RestoreOrganizationUserCommand.cs | 26 +- .../RequireTwoFactorPolicyRequirement.cs | 52 ++++ .../PolicyServiceCollectionExtensions.cs | 1 + .../Implementations/OrganizationService.cs | 73 ----- .../Services/Implementations/UserService.cs | 42 ++- .../AcceptOrgUserCommandTests.cs | 111 +++++++- .../ConfirmOrganizationUserCommandTests.cs | 121 ++++++++ .../RestoreOrganizationUserCommandTests.cs | 269 +++++++++++++----- ...eTwoFactorPolicyRequirementFactoryTests.cs | 117 ++++++++ test/Core.Test/Services/UserServiceTests.cs | 128 ++++++++- 12 files changed, 852 insertions(+), 188 deletions(-) create mode 100644 src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirement.cs create mode 100644 test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirementFactoryTests.cs diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs index f3426efddc..3770d867cf 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommand.cs @@ -1,4 +1,6 @@ using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; @@ -27,6 +29,8 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand private readonly IUserRepository _userRepository; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory; + private readonly IFeatureService _featureService; + private readonly IPolicyRequirementQuery _policyRequirementQuery; public AcceptOrgUserCommand( IDataProtectionProvider dataProtectionProvider, @@ -37,9 +41,10 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand IMailService mailService, IUserRepository userRepository, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, - IDataProtectorTokenFactory orgUserInviteTokenDataFactory) + IDataProtectorTokenFactory orgUserInviteTokenDataFactory, + IFeatureService featureService, + IPolicyRequirementQuery policyRequirementQuery) { - // TODO: remove data protector when old token validation removed _dataProtector = dataProtectionProvider.CreateProtector(OrgUserInviteTokenable.DataProtectorPurpose); _globalSettings = globalSettings; @@ -50,6 +55,8 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand _userRepository = userRepository; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory; + _featureService = featureService; + _policyRequirementQuery = policyRequirementQuery; } public async Task AcceptOrgUserByEmailTokenAsync(Guid organizationUserId, User user, string emailToken, @@ -196,15 +203,7 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand } // Enforce Two Factor Authentication Policy of organization user is trying to join - if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user)) - { - var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, - PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited); - if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId)) - { - throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account."); - } - } + await ValidateTwoFactorAuthenticationPolicyAsync(user, orgUser.OrganizationId); orgUser.Status = OrganizationUserStatusType.Accepted; orgUser.UserId = user.Id; @@ -224,4 +223,33 @@ public class AcceptOrgUserCommand : IAcceptOrgUserCommand return orgUser; } + private async Task ValidateTwoFactorAuthenticationPolicyAsync(User user, Guid organizationId) + { + if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) + { + if (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user)) + { + // If the user has two-step login enabled, we skip checking the 2FA policy + return; + } + + var twoFactorPolicyRequirement = await _policyRequirementQuery.GetAsync(user.Id); + if (twoFactorPolicyRequirement.IsTwoFactorRequiredForOrganization(organizationId)) + { + throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account."); + } + + return; + } + + if (!await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user)) + { + var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, + PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited); + if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == organizationId)) + { + throw new BadRequestException("You cannot join this organization until you enable two-step login on your user account."); + } + } + } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs index 9bfe8f791e..806cf5a533 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommand.cs @@ -1,5 +1,7 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Enums; @@ -24,6 +26,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand private readonly IPushRegistrationService _pushRegistrationService; private readonly IPolicyService _policyService; private readonly IDeviceRepository _deviceRepository; + private readonly IPolicyRequirementQuery _policyRequirementQuery; + private readonly IFeatureService _featureService; public ConfirmOrganizationUserCommand( IOrganizationRepository organizationRepository, @@ -35,7 +39,9 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand IPushNotificationService pushNotificationService, IPushRegistrationService pushRegistrationService, IPolicyService policyService, - IDeviceRepository deviceRepository) + IDeviceRepository deviceRepository, + IPolicyRequirementQuery policyRequirementQuery, + IFeatureService featureService) { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; @@ -47,6 +53,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand _pushRegistrationService = pushRegistrationService; _policyService = policyService; _deviceRepository = deviceRepository; + _policyRequirementQuery = policyRequirementQuery; + _featureService = featureService; } public async Task ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, @@ -118,8 +126,8 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand } } - var twoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled; - await CheckPoliciesAsync(organizationId, user, orgUsers, twoFactorEnabled); + var userTwoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled; + await CheckPoliciesAsync(organizationId, user, orgUsers, userTwoFactorEnabled); orgUser.Status = OrganizationUserStatusType.Confirmed; orgUser.Key = keys[orgUser.Id]; orgUser.Email = null; @@ -142,15 +150,10 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand } private async Task CheckPoliciesAsync(Guid organizationId, User user, - ICollection userOrgs, bool twoFactorEnabled) + ICollection userOrgs, bool userTwoFactorEnabled) { // Enforce Two Factor Authentication Policy for this organization - var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication)) - .Any(p => p.OrganizationId == organizationId); - if (orgRequiresTwoFactor && !twoFactorEnabled) - { - throw new BadRequestException("User does not have two-step login enabled."); - } + await ValidateTwoFactorAuthenticationPolicyAsync(user, organizationId, userTwoFactorEnabled); var hasOtherOrgs = userOrgs.Any(ou => ou.OrganizationId != organizationId); var singleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg); @@ -168,6 +171,33 @@ public class ConfirmOrganizationUserCommand : IConfirmOrganizationUserCommand } } + private async Task ValidateTwoFactorAuthenticationPolicyAsync(User user, Guid organizationId, bool userTwoFactorEnabled) + { + if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) + { + if (userTwoFactorEnabled) + { + // If the user has two-step login enabled, we skip checking the 2FA policy + return; + } + + var twoFactorPolicyRequirement = await _policyRequirementQuery.GetAsync(user.Id); + if (twoFactorPolicyRequirement.IsTwoFactorRequiredForOrganization(organizationId)) + { + throw new BadRequestException("User does not have two-step login enabled."); + } + + return; + } + + var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication)) + .Any(p => p.OrganizationId == organizationId); + if (orgRequiresTwoFactor && !userTwoFactorEnabled) + { + throw new BadRequestException("User does not have two-step login enabled."); + } + } + private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId) { var devices = await GetUserDeviceIdsAsync(userId); diff --git a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/v1/RestoreOrganizationUserCommand.cs b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/v1/RestoreOrganizationUserCommand.cs index 74165a5a71..fe19cd1389 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/v1/RestoreOrganizationUserCommand.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/v1/RestoreOrganizationUserCommand.cs @@ -1,5 +1,7 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Enums; @@ -22,7 +24,9 @@ public class RestoreOrganizationUserCommand( ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IPolicyService policyService, IUserRepository userRepository, - IOrganizationService organizationService) : IRestoreOrganizationUserCommand + IOrganizationService organizationService, + IFeatureService featureService, + IPolicyRequirementQuery policyRequirementQuery) : IRestoreOrganizationUserCommand { public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId) { @@ -270,12 +274,7 @@ public class RestoreOrganizationUserCommand( // Enforce 2FA Policy of organization user is trying to join if (!userHasTwoFactorEnabled) { - var invitedTwoFactorPolicies = await policyService.GetPoliciesApplicableToUserAsync(userId, - PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked); - if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId)) - { - twoFactorCompliant = false; - } + twoFactorCompliant = !await IsTwoFactorRequiredForOrganizationAsync(userId, orgUser.OrganizationId); } var user = await userRepository.GetByIdAsync(userId); @@ -299,4 +298,17 @@ public class RestoreOrganizationUserCommand( throw new BadRequestException(user.Email + " is not compliant with the two-step login policy"); } } + + private async Task IsTwoFactorRequiredForOrganizationAsync(Guid userId, Guid organizationId) + { + if (featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) + { + var requirement = await policyRequirementQuery.GetAsync(userId); + return requirement.IsTwoFactorRequiredForOrganization(organizationId); + } + + var invitedTwoFactorPolicies = await policyService.GetPoliciesApplicableToUserAsync(userId, + PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked); + return invitedTwoFactorPolicies.Any(p => p.OrganizationId == organizationId); + } } diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirement.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirement.cs new file mode 100644 index 0000000000..bbc997a83d --- /dev/null +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirement.cs @@ -0,0 +1,52 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.Enums; + +namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +/// +/// Policy requirements for the Require Two-Factor Authentication policy. +/// +public class RequireTwoFactorPolicyRequirement : IPolicyRequirement +{ + private readonly IEnumerable _policyDetails; + + public RequireTwoFactorPolicyRequirement(IEnumerable policyDetails) + { + _policyDetails = policyDetails; + } + + /// + /// Checks if two-factor authentication is required for the organization due to an active policy. + /// + /// The ID of the organization to check. + /// True if two-factor authentication is required for the organization, false otherwise. + /// + /// This should be used to check whether the member needs to have 2FA enabled before being + /// accepted, confirmed, or restored to the organization. + /// + public bool IsTwoFactorRequiredForOrganization(Guid organizationId) => + _policyDetails.Any(p => p.OrganizationId == organizationId); + + /// + /// Returns tuples of (OrganizationId, OrganizationUserId) for active memberships where two-factor authentication is required. + /// Users should be revoked from these organizations if they disable all 2FA methods. + /// + public IEnumerable<(Guid OrganizationId, Guid OrganizationUserId)> OrganizationsRequiringTwoFactor => + _policyDetails + .Where(p => p.OrganizationUserStatus is + OrganizationUserStatusType.Accepted or + OrganizationUserStatusType.Confirmed) + .Select(p => (p.OrganizationId, p.OrganizationUserId)); +} + +public class RequireTwoFactorPolicyRequirementFactory : BasePolicyRequirementFactory +{ + public override PolicyType PolicyType => PolicyType.TwoFactorAuthentication; + protected override IEnumerable ExemptStatuses => []; + + public override RequireTwoFactorPolicyRequirement Create(IEnumerable policyDetails) + { + return new RequireTwoFactorPolicyRequirement(policyDetails); + } +} diff --git a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs index 1be0e61af7..f98135b70d 100644 --- a/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs +++ b/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyServiceCollectionExtensions.cs @@ -36,5 +36,6 @@ public static class PolicyServiceCollectionExtensions services.AddScoped, ResetPasswordPolicyRequirementFactory>(); services.AddScoped, PersonalOwnershipPolicyRequirementFactory>(); services.AddScoped, RequireSsoPolicyRequirementFactory>(); + services.AddScoped, RequireTwoFactorPolicyRequirementFactory>(); } } diff --git a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs index 26ff421328..7640a82fcb 100644 --- a/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs +++ b/src/Core/AdminConsole/Services/Implementations/OrganizationService.cs @@ -14,7 +14,6 @@ using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; using Bit.Core.Auth.Repositories; -using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Constants; using Bit.Core.Billing.Enums; using Bit.Core.Billing.Extensions; @@ -45,7 +44,6 @@ public class OrganizationService : IOrganizationService private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; private readonly ICollectionRepository _collectionRepository; - private readonly IUserRepository _userRepository; private readonly IGroupRepository _groupRepository; private readonly IMailService _mailService; private readonly IPushNotificationService _pushNotificationService; @@ -69,7 +67,6 @@ public class OrganizationService : IOrganizationService private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand; private readonly IProviderRepository _providerRepository; private readonly IFeatureService _featureService; - private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery; private readonly IPricingClient _pricingClient; private readonly IPolicyRequirementQuery _policyRequirementQuery; @@ -79,7 +76,6 @@ public class OrganizationService : IOrganizationService IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, ICollectionRepository collectionRepository, - IUserRepository userRepository, IGroupRepository groupRepository, IMailService mailService, IPushNotificationService pushNotificationService, @@ -103,7 +99,6 @@ public class OrganizationService : IOrganizationService IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand, IProviderRepository providerRepository, IFeatureService featureService, - ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery, IPricingClient pricingClient, IPolicyRequirementQuery policyRequirementQuery, @@ -113,7 +108,6 @@ public class OrganizationService : IOrganizationService _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; _collectionRepository = collectionRepository; - _userRepository = userRepository; _groupRepository = groupRepository; _mailService = mailService; _pushNotificationService = pushNotificationService; @@ -137,7 +131,6 @@ public class OrganizationService : IOrganizationService _updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand; _providerRepository = providerRepository; _featureService = featureService; - _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery; _pricingClient = pricingClient; _policyRequirementQuery = policyRequirementQuery; @@ -1722,72 +1715,6 @@ public class OrganizationService : IOrganizationService return result; } - private async Task CheckPoliciesBeforeRestoreAsync(OrganizationUser orgUser, bool userHasTwoFactorEnabled) - { - // An invited OrganizationUser isn't linked with a user account yet, so these checks are irrelevant - // The user will be subject to the same checks when they try to accept the invite - if (GetPriorActiveOrganizationUserStatusType(orgUser) == OrganizationUserStatusType.Invited) - { - return; - } - - var userId = orgUser.UserId.Value; - - // Enforce Single Organization Policy of organization user is being restored to - var allOrgUsers = await _organizationUserRepository.GetManyByUserAsync(userId); - var hasOtherOrgs = allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId); - var singleOrgPoliciesApplyingToRevokedUsers = await _policyService.GetPoliciesApplicableToUserAsync(userId, - PolicyType.SingleOrg, OrganizationUserStatusType.Revoked); - var singleOrgPolicyApplies = singleOrgPoliciesApplyingToRevokedUsers.Any(p => p.OrganizationId == orgUser.OrganizationId); - - var singleOrgCompliant = true; - var belongsToOtherOrgCompliant = true; - var twoFactorCompliant = true; - - if (hasOtherOrgs && singleOrgPolicyApplies) - { - singleOrgCompliant = false; - } - - // Enforce Single Organization Policy of other organizations user is a member of - var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId, - PolicyType.SingleOrg); - if (anySingleOrgPolicies) - { - belongsToOtherOrgCompliant = false; - } - - // Enforce Two Factor Authentication Policy of organization user is trying to join - if (!userHasTwoFactorEnabled) - { - var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId, - PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked); - if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId)) - { - twoFactorCompliant = false; - } - } - - var user = await _userRepository.GetByIdAsync(userId); - - if (!singleOrgCompliant && !twoFactorCompliant) - { - throw new BadRequestException(user.Email + " is not compliant with the single organization and two-step login polciy"); - } - else if (!singleOrgCompliant) - { - throw new BadRequestException(user.Email + " is not compliant with the single organization policy"); - } - else if (!belongsToOtherOrgCompliant) - { - throw new BadRequestException(user.Email + " belongs to an organization that doesn't allow them to join multiple organizations"); - } - else if (!twoFactorCompliant) - { - throw new BadRequestException(user.Email + " is not compliant with the two-step login policy"); - } - } - public static OrganizationUserStatusType GetPriorActiveOrganizationUserStatusType(OrganizationUser organizationUser) { // Determine status to revert back to diff --git a/src/Core/Services/Implementations/UserService.cs b/src/Core/Services/Implementations/UserService.cs index 76520b4085..f0c97b8589 100644 --- a/src/Core/Services/Implementations/UserService.cs +++ b/src/Core/Services/Implementations/UserService.cs @@ -6,6 +6,8 @@ using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Models.Data; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; @@ -81,6 +83,7 @@ public class UserService : UserManager, IUserService, IDisposable private readonly IRevokeNonCompliantOrganizationUserCommand _revokeNonCompliantOrganizationUserCommand; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly IDistributedCache _distributedCache; + private readonly IPolicyRequirementQuery _policyRequirementQuery; public UserService( IUserRepository userRepository, @@ -119,7 +122,8 @@ public class UserService : UserManager, IUserService, IDisposable IRemoveOrganizationUserCommand removeOrganizationUserCommand, IRevokeNonCompliantOrganizationUserCommand revokeNonCompliantOrganizationUserCommand, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, - IDistributedCache distributedCache) + IDistributedCache distributedCache, + IPolicyRequirementQuery policyRequirementQuery) : base( store, optionsAccessor, @@ -164,6 +168,7 @@ public class UserService : UserManager, IUserService, IDisposable _revokeNonCompliantOrganizationUserCommand = revokeNonCompliantOrganizationUserCommand; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _distributedCache = distributedCache; + _policyRequirementQuery = policyRequirementQuery; } public Guid? GetProperUserId(ClaimsPrincipal principal) @@ -1394,9 +1399,40 @@ public class UserService : UserManager, IUserService, IDisposable private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user) { + if (_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)) + { + var requirement = await _policyRequirementQuery.GetAsync(user.Id); + if (!requirement.OrganizationsRequiringTwoFactor.Any()) + { + Logger.LogInformation("No organizations requiring two factor for user {userId}.", user.Id); + return; + } + + var organizationIds = requirement.OrganizationsRequiringTwoFactor.Select(o => o.OrganizationId).ToList(); + var organizations = await _organizationRepository.GetManyByIdsAsync(organizationIds); + var organizationLookup = organizations.ToDictionary(org => org.Id); + + var revokeOrgUserTasks = requirement.OrganizationsRequiringTwoFactor + .Where(o => organizationLookup.ContainsKey(o.OrganizationId)) + .Select(async o => + { + var organization = organizationLookup[o.OrganizationId]; + await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync( + new RevokeOrganizationUsersRequest( + o.OrganizationId, + [new OrganizationUserUserDetails { Id = o.OrganizationUserId, OrganizationId = o.OrganizationId }], + new SystemUser(EventSystemUser.TwoFactorDisabled))); + await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email); + }).ToArray(); + + await Task.WhenAll(revokeOrgUserTasks); + + return; + } + var twoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication); - var removeOrgUserTasks = twoFactorPolicies.Select(async p => + var legacyRevokeOrgUserTasks = twoFactorPolicies.Select(async p => { var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId); await _revokeNonCompliantOrganizationUserCommand.RevokeNonCompliantOrganizationUsersAsync( @@ -1407,7 +1443,7 @@ public class UserService : UserManager, IUserService, IDisposable await _mailService.SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization.DisplayName(), user.Email); }).ToArray(); - await Task.WhenAll(removeOrgUserTasks); + await Task.WhenAll(legacyRevokeOrgUserTasks); } public override async Task ConfirmEmailAsync(User user, string token) diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs index baf844acae..540bac4d1c 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/AcceptOrgUserCommandTests.cs @@ -1,5 +1,8 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; @@ -29,7 +32,6 @@ namespace Bit.Core.Test.OrganizationFeatures.OrganizationUsers; public class AcceptOrgUserCommandTests { private readonly IUserService _userService = Substitute.For(); - private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery = Substitute.For(); private readonly IOrgUserInviteTokenableFactory _orgUserInviteTokenableFactory = Substitute.For(); private readonly IDataProtectorTokenFactory _orgUserInviteTokenDataFactory = new FakeDataProtectorTokenFactory(); @@ -166,9 +168,6 @@ public class AcceptOrgUserCommandTests // Arrange SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); - // User doesn't have 2FA enabled - _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false); - // Organization they are trying to join requires 2FA var twoFactorPolicy = new OrganizationUserPolicyDetails { OrganizationId = orgUser.OrganizationId }; sutProvider.GetDependency() @@ -185,6 +184,107 @@ public class AcceptOrgUserCommandTests exception.Message); } + [Theory] + [BitAutoData] + public async Task AcceptOrgUserAsync_WithPolicyRequirementsEnabled_UserWithout2FAJoining2FARequiredOrg_ThrowsBadRequest( + SutProvider sutProvider, + User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) + { + SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + + // Organization they are trying to join requires 2FA + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = orgUser.OrganizationId, + OrganizationUserStatus = OrganizationUserStatusType.Invited, + PolicyType = PolicyType.TwoFactorAuthentication, + } + ])); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService)); + + Assert.Equal("You cannot join this organization until you enable two-step login on your user account.", + exception.Message); + } + + [Theory] + [BitAutoData] + public async Task AcceptOrgUserAsync_WithPolicyRequirementsEnabled_UserWith2FAJoining2FARequiredOrg_Succeeds( + SutProvider sutProvider, + User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) + { + SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + + // User has 2FA enabled + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(user) + .Returns(true); + + // Organization they are trying to join requires 2FA + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = orgUser.OrganizationId, + OrganizationUserStatus = OrganizationUserStatusType.Invited, + PolicyType = PolicyType.TwoFactorAuthentication, + } + ])); + + await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(ou => ou.Status == OrganizationUserStatusType.Accepted)); + } + + [Theory] + [BitAutoData] + public async Task AcceptOrgUserAsync_WithPolicyRequirementsEnabled_UserJoiningOrgWithout2FARequirement_Succeeds( + SutProvider sutProvider, + User user, Organization org, OrganizationUser orgUser, OrganizationUserUserDetails adminUserDetails) + { + SetupCommonAcceptOrgUserMocks(sutProvider, user, org, orgUser, adminUserDetails); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + + // Organization they are trying to join doesn't require 2FA + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = Guid.NewGuid(), + OrganizationUserStatus = OrganizationUserStatusType.Invited, + PolicyType = PolicyType.TwoFactorAuthentication, + } + ])); + + await sutProvider.Sut.AcceptOrgUserAsync(orgUser, user, _userService); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(ou => ou.Status == OrganizationUserStatusType.Accepted)); + } + [Theory] [BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Owner)] @@ -647,9 +747,6 @@ public class AcceptOrgUserCommandTests .AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg) .Returns(false); - // User doesn't have 2FA enabled - _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user).Returns(false); - // Org does not require 2FA sutProvider.GetDependency().GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited) diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs index 06335f668d..366d8cb2d6 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/ConfirmOrganizationUserCommandTests.cs @@ -1,6 +1,9 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Enums; @@ -321,4 +324,122 @@ public class ConfirmOrganizationUserCommandTests Assert.Contains("User does not have two-step login enabled.", result[1].Item2); Assert.Contains("Cannot confirm this member to the organization until they leave or remove all other organizations.", result[2].Item2); } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_WithPolicyRequirementsEnabled_AndTwoFactorRequired_ThrowsBadRequestException( + Organization org, OrganizationUser confirmingUser, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + var organizationRepository = sutProvider.GetDependency(); + var userRepository = sutProvider.GetDependency(); + var featureService = sutProvider.GetDependency(); + var policyRequirementQuery = sutProvider.GetDependency(); + var twoFactorIsEnabledQuery = sutProvider.GetDependency(); + + org.PlanType = PlanType.EnterpriseAnnually; + orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; + orgUser.UserId = user.Id; + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); + organizationRepository.GetByIdAsync(org.Id).Returns(org); + userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); + featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + policyRequirementQuery.GetAsync(user.Id) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = org.Id, + OrganizationUserStatus = OrganizationUserStatusType.Accepted, + PolicyType = PolicyType.TwoFactorAuthentication + } + ])); + twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is>(ids => ids.Contains(user.Id))) + .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) }); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, "key", confirmingUser.Id)); + Assert.Contains("User does not have two-step login enabled.", exception.Message); + } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_WithPolicyRequirementsEnabled_AndTwoFactorNotRequired_Succeeds( + Organization org, OrganizationUser confirmingUser, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + var organizationRepository = sutProvider.GetDependency(); + var userRepository = sutProvider.GetDependency(); + var featureService = sutProvider.GetDependency(); + var policyRequirementQuery = sutProvider.GetDependency(); + var twoFactorIsEnabledQuery = sutProvider.GetDependency(); + + org.PlanType = PlanType.EnterpriseAnnually; + orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; + orgUser.UserId = user.Id; + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); + organizationRepository.GetByIdAsync(org.Id).Returns(org); + userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); + featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + policyRequirementQuery.GetAsync(user.Id) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = Guid.NewGuid(), + OrganizationUserStatus = OrganizationUserStatusType.Invited, + PolicyType = PolicyType.TwoFactorAuthentication, + } + ])); + twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is>(ids => ids.Contains(user.Id))) + .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, false) }); + + await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, "key", confirmingUser.Id); + + await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed); + await sutProvider.GetDependency().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, orgUser.AccessSecretsManager); + await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is>(users => users.Contains(orgUser) && users.Count == 1)); + } + + [Theory, BitAutoData] + public async Task ConfirmUserAsync_WithPolicyRequirementsEnabled_AndTwoFactorEnabled_Succeeds( + Organization org, OrganizationUser confirmingUser, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser orgUser, User user, + SutProvider sutProvider) + { + var organizationUserRepository = sutProvider.GetDependency(); + var organizationRepository = sutProvider.GetDependency(); + var userRepository = sutProvider.GetDependency(); + var featureService = sutProvider.GetDependency(); + var policyRequirementQuery = sutProvider.GetDependency(); + var twoFactorIsEnabledQuery = sutProvider.GetDependency(); + + org.PlanType = PlanType.EnterpriseAnnually; + orgUser.OrganizationId = confirmingUser.OrganizationId = org.Id; + orgUser.UserId = user.Id; + organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { orgUser }); + organizationRepository.GetByIdAsync(org.Id).Returns(org); + userRepository.GetManyAsync(default).ReturnsForAnyArgs(new[] { user }); + featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true); + policyRequirementQuery.GetAsync(user.Id) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = org.Id, + OrganizationUserStatus = OrganizationUserStatusType.Accepted, + PolicyType = PolicyType.TwoFactorAuthentication + } + ])); + twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(Arg.Is>(ids => ids.Contains(user.Id))) + .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (user.Id, true) }); + + await sutProvider.Sut.ConfirmUserAsync(orgUser.OrganizationId, orgUser.Id, "key", confirmingUser.Id); + + await sutProvider.GetDependency().Received(1).LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed); + await sutProvider.GetDependency().Received(1).SendOrganizationConfirmedEmailAsync(org.DisplayName(), user.Email, orgUser.AccessSecretsManager); + await organizationUserRepository.Received(1).ReplaceManyAsync(Arg.Is>(users => users.Contains(orgUser) && users.Count == 1)); + } } diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/RestoreOrganizationUserCommandTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/RestoreOrganizationUserCommandTests.cs index d6880a3a12..fbd711307c 100644 --- a/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/RestoreOrganizationUserCommandTests.cs +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/OrganizationUsers/RestoreUser/RestoreOrganizationUserCommandTests.cs @@ -1,6 +1,9 @@ using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Billing.Enums; @@ -208,6 +211,57 @@ public class RestoreOrganizationUserCommandTests .PushSyncOrgKeysAsync(Arg.Any()); } + [Theory, BitAutoData] + public async Task RestoreUser_WithPolicyRequirementsEnabled_With2FAPolicyEnabled_WithoutUser2FAConfigured_Fails( + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, + [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, + SutProvider sutProvider) + { + organizationUser.Email = null; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Is>(i => i.Contains(organizationUser.UserId.Value))) + .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, false) }); + + RestoreUser_Setup(organization, owner, organizationUser, sutProvider); + + sutProvider.GetDependency() + .GetAsync(organizationUser.UserId.Value) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = organizationUser.OrganizationId, + OrganizationUserStatus = OrganizationUserStatusType.Revoked, + PolicyType = PolicyType.TwoFactorAuthentication + } + ])); + + var user = new User(); + user.Email = "test@bitwarden.com"; + sutProvider.GetDependency().GetByIdAsync(organizationUser.UserId.Value).Returns(user); + + var exception = await Assert.ThrowsAsync( + () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); + + Assert.Contains("test@bitwarden.com is not compliant with the two-step login policy", exception.Message.ToLowerInvariant()); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .RestoreAsync(Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .PushSyncOrgKeysAsync(Arg.Any()); + } + [Theory, BitAutoData] public async Task RestoreUser_With2FAPolicyEnabled_WithUser2FAConfigured_Success( Organization organization, @@ -235,6 +289,46 @@ public class RestoreOrganizationUserCommandTests .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored); } + [Theory, BitAutoData] + public async Task RestoreUser_WithPolicyRequirementsEnabled_With2FAPolicyEnabled_WithUser2FAConfigured_Success( + Organization organization, + [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, + [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, + SutProvider sutProvider) + { + organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + + RestoreUser_Setup(organization, owner, organizationUser, sutProvider); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Is>(i => i.Contains(organizationUser.UserId.Value))) + .Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) }); + sutProvider.GetDependency() + .GetAsync(organizationUser.UserId.Value) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = organizationUser.OrganizationId, + OrganizationUserStatus = OrganizationUserStatusType.Revoked, + PolicyType = PolicyType.TwoFactorAuthentication + } + ])); + + await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id); + + await sutProvider.GetDependency() + .Received(1) + .RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed); + await sutProvider.GetDependency() + .Received(1) + .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored); + } + [Theory, BitAutoData] public async Task RestoreUser_WithSingleOrgPolicyEnabled_Fails( Organization organization, @@ -277,45 +371,6 @@ public class RestoreOrganizationUserCommandTests .PushSyncOrgKeysAsync(Arg.Any()); } - [Theory, BitAutoData] - public async Task RestoreUser_vNext_WithOtherOrganizationSingleOrgPolicyEnabled_Fails( - Organization organization, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, - [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, - [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser, - SutProvider sutProvider) - { - organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke - secondOrganizationUser.UserId = organizationUser.UserId; - RestoreUser_Setup(organization, owner, organizationUser, sutProvider); - - sutProvider.GetDependency() - .TwoFactorIsEnabledAsync(Arg.Is>(i => i.Contains(organizationUser.UserId.Value))) - .Returns(new List<(Guid userId, bool twoFactorIsEnabled)> { (organizationUser.UserId.Value, true) }); - - sutProvider.GetDependency() - .AnyPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any()) - .Returns(true); - - var user = new User { Email = "test@bitwarden.com" }; - sutProvider.GetDependency().GetByIdAsync(organizationUser.UserId.Value).Returns(user); - - var exception = await Assert.ThrowsAsync( - () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); - - Assert.Contains("test@bitwarden.com belongs to an organization that doesn't allow them to join multiple organizations", exception.Message.ToLowerInvariant()); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .RestoreAsync(Arg.Any(), Arg.Any()); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .LogOrganizationUserEventAsync(Arg.Any(), Arg.Any(), Arg.Any()); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .PushSyncOrgKeysAsync(Arg.Any()); - } - [Theory, BitAutoData] public async Task RestoreUser_WithSingleOrgPolicyEnabled_And_2FA_Policy_Fails( Organization organization, @@ -364,20 +419,42 @@ public class RestoreOrganizationUserCommandTests } [Theory, BitAutoData] - public async Task RestoreUser_vNext_With2FAPolicyEnabled_WithoutUser2FAConfigured_Fails( + public async Task RestoreUser_WithPolicyRequirementsEnabled_WithSingleOrgPolicyEnabled_And_2FA_Policy_Fails( Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, + [OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser, SutProvider sutProvider) { - organizationUser.Email = null; - + organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke + secondOrganizationUser.UserId = organizationUser.UserId; RestoreUser_Setup(organization, owner, organizationUser, sutProvider); + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + + sutProvider.GetDependency() + .GetManyByUserAsync(organizationUser.UserId.Value) + .Returns(new[] { organizationUser, secondOrganizationUser }); sutProvider.GetDependency() - .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any()) - .Returns([new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } - ]); + .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any()) + .Returns(new[] + { + new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.SingleOrg, OrganizationUserStatus = OrganizationUserStatusType.Revoked } + }); + + sutProvider.GetDependency() + .GetAsync(organizationUser.UserId.Value) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = organizationUser.OrganizationId, + OrganizationUserStatus = OrganizationUserStatusType.Revoked, + PolicyType = PolicyType.TwoFactorAuthentication + } + ])); var user = new User { Email = "test@bitwarden.com" }; sutProvider.GetDependency().GetByIdAsync(organizationUser.UserId.Value).Returns(user); @@ -385,7 +462,7 @@ public class RestoreOrganizationUserCommandTests var exception = await Assert.ThrowsAsync( () => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id)); - Assert.Contains("test@bitwarden.com is not compliant with the two-step login policy", exception.Message.ToLowerInvariant()); + Assert.Contains("test@bitwarden.com is not compliant with the single organization and two-step login policy", exception.Message.ToLowerInvariant()); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() @@ -398,35 +475,6 @@ public class RestoreOrganizationUserCommandTests .PushSyncOrgKeysAsync(Arg.Any()); } - [Theory, BitAutoData] - public async Task RestoreUser_vNext_With2FAPolicyEnabled_WithUser2FAConfigured_Success( - Organization organization, - [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, - [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, - SutProvider sutProvider) - { - organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke - RestoreUser_Setup(organization, owner, organizationUser, sutProvider); - - sutProvider.GetDependency() - .GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any()) - .Returns([new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } - ]); - - sutProvider.GetDependency() - .TwoFactorIsEnabledAsync(Arg.Is>(i => i.Contains(organizationUser.UserId.Value))) - .Returns(new List<(Guid userId, bool twoFactorIsEnabled)> { (organizationUser.UserId.Value, true) }); - - await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id); - - await sutProvider.GetDependency() - .Received(1) - .RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed); - await sutProvider.GetDependency() - .Received(1) - .LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored); - } - [Theory, BitAutoData] public async Task RestoreUser_WhenUserOwningAnotherFreeOrganization_ThenRestoreUserFails( Organization organization, @@ -672,6 +720,77 @@ public class RestoreOrganizationUserCommandTests .RestoreAsync(orgUser3.Id, OrganizationUserStatusType.Invited); } + [Theory, BitAutoData] + public async Task RestoreUsers_WithPolicyRequirementsEnabled_With2FAPolicy_BlocksNonCompliantUser(Organization organization, + [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, + [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser1, + [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2, + [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser3, + SutProvider sutProvider) + { + // Arrange + RestoreUser_Setup(organization, owner, orgUser1, sutProvider); + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + + var organizationUserRepository = sutProvider.GetDependency(); + var userRepository = sutProvider.GetDependency(); + var policyService = sutProvider.GetDependency(); + var userService = Substitute.For(); + + orgUser1.Email = orgUser2.Email = null; + orgUser3.UserId = null; + orgUser3.Key = null; + orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = organization.Id; + organizationUserRepository + .GetManyAsync(Arg.Is>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id) && ids.Contains(orgUser3.Id))) + .Returns(new[] { orgUser1, orgUser2, orgUser3 }); + + userRepository.GetByIdAsync(orgUser2.UserId!.Value).Returns(new User { Email = "test@example.com" }); + + // Setup 2FA policy + sutProvider.GetDependency() + .GetAsync(Arg.Any()) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = organization.Id, + OrganizationUserStatus = OrganizationUserStatusType.Revoked, + PolicyType = PolicyType.TwoFactorAuthentication + } + ])); + + // User1 has 2FA, User2 doesn't + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(Arg.Is>(ids => ids.Contains(orgUser1.UserId!.Value) && ids.Contains(orgUser2.UserId!.Value))) + .Returns(new List<(Guid userId, bool twoFactorIsEnabled)> + { + (orgUser1.UserId!.Value, true), + (orgUser2.UserId!.Value, false) + }); + + // Act + var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, [orgUser1.Id, orgUser2.Id, orgUser3.Id], owner.Id, userService); + + // Assert + Assert.Equal(3, result.Count); + Assert.Empty(result[0].Item2); // First user should succeed + Assert.Contains("two-step login", result[1].Item2); // Second user should fail + Assert.Empty(result[2].Item2); // Third user should succeed + await organizationUserRepository + .Received(1) + .RestoreAsync(orgUser1.Id, OrganizationUserStatusType.Confirmed); + await organizationUserRepository + .DidNotReceive() + .RestoreAsync(orgUser2.Id, Arg.Any()); + await organizationUserRepository + .Received(1) + .RestoreAsync(orgUser3.Id, OrganizationUserStatusType.Invited); + } + [Theory, BitAutoData] public async Task RestoreUsers_UserOwnsAnotherFreeOrganization_BlocksOwnerUserFromBeingRestored(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner, diff --git a/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirementFactoryTests.cs b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirementFactoryTests.cs new file mode 100644 index 0000000000..c20ea494ab --- /dev/null +++ b/test/Core.Test/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/RequireTwoFactorPolicyRequirementFactoryTests.cs @@ -0,0 +1,117 @@ +using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; +using Bit.Core.Enums; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; + +[SutProviderCustomize] +public class RequireTwoFactorPolicyRequirementFactoryTests +{ + [Theory] + [BitAutoData] + public void IsTwoFactorRequiredForOrganization_WithNoPolicies_ReturnsFalse( + Guid organizationId, + SutProvider sutProvider) + { + var actual = sutProvider.Sut.Create([]); + + Assert.False(actual.IsTwoFactorRequiredForOrganization(organizationId)); + } + + [Theory] + [BitAutoData] + public void IsTwoFactorRequiredForOrganization_WithOrganizationPolicy_ReturnsTrue( + Guid organizationId, + SutProvider sutProvider) + { + var actual = sutProvider.Sut.Create( + [ + new PolicyDetails + { + OrganizationId = organizationId, + PolicyType = PolicyType.TwoFactorAuthentication, + } + ]); + + Assert.True(actual.IsTwoFactorRequiredForOrganization(organizationId)); + } + + [Theory] + [BitAutoData] + public void IsTwoFactorRequiredForOrganization_WithOtherOrganizationPolicy_ReturnsFalse( + Guid organizationId, + SutProvider sutProvider) + { + var actual = sutProvider.Sut.Create( + [ + new PolicyDetails + { + OrganizationId = Guid.NewGuid(), + PolicyType = PolicyType.TwoFactorAuthentication, + }, + ]); + + Assert.False(actual.IsTwoFactorRequiredForOrganization(organizationId)); + } + + [Theory, BitAutoData] + public void OrganizationsRequiringTwoFactor_WithNoPolicies_ReturnsEmptyCollection( + SutProvider sutProvider) + { + var actual = sutProvider.Sut.Create([]); + + Assert.Empty(actual.OrganizationsRequiringTwoFactor); + } + + [Theory, BitAutoData] + public void OrganizationsRequiringTwoFactor_WithMultiplePolicies_ReturnsActiveMemberships( + Guid orgId1, Guid orgUserId1, Guid orgId2, Guid orgUserId2, + Guid orgId3, Guid orgUserId3, Guid orgId4, Guid orgUserId4, + SutProvider sutProvider) + { + var policies = new[] + { + new PolicyDetails + { + OrganizationId = orgId1, + OrganizationUserId = orgUserId1, + PolicyType = PolicyType.TwoFactorAuthentication, + OrganizationUserStatus = OrganizationUserStatusType.Accepted + }, + new PolicyDetails + { + OrganizationId = orgId2, + OrganizationUserId = orgUserId2, + PolicyType = PolicyType.TwoFactorAuthentication, + OrganizationUserStatus = OrganizationUserStatusType.Confirmed + }, + new PolicyDetails + { + OrganizationId = orgId3, + OrganizationUserId = orgUserId3, + PolicyType = PolicyType.TwoFactorAuthentication, + OrganizationUserStatus = OrganizationUserStatusType.Invited + }, + new PolicyDetails + { + OrganizationId = orgId4, + OrganizationUserId = orgUserId4, + PolicyType = PolicyType.TwoFactorAuthentication, + OrganizationUserStatus = OrganizationUserStatusType.Revoked + } + }; + + var actual = sutProvider.Sut.Create(policies); + + var result = actual.OrganizationsRequiringTwoFactor.ToList(); + Assert.Equal(2, result.Count); + Assert.Contains(result, p => p.OrganizationId == orgId1 && p.OrganizationUserId == orgUserId1); + Assert.Contains(result, p => p.OrganizationId == orgId2 && p.OrganizationUserId == orgUserId2); + Assert.DoesNotContain(result, p => p.OrganizationId == orgId3 && p.OrganizationUserId == orgUserId3); + Assert.DoesNotContain(result, p => p.OrganizationId == orgId4 && p.OrganizationUserId == orgUserId4); + } +} diff --git a/test/Core.Test/Services/UserServiceTests.cs b/test/Core.Test/Services/UserServiceTests.cs index ac7f6e4018..a5bfe35152 100644 --- a/test/Core.Test/Services/UserServiceTests.cs +++ b/test/Core.Test/Services/UserServiceTests.cs @@ -2,8 +2,11 @@ using System.Text.Json; using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Enums; +using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Requests; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies; +using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements; using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Services; using Bit.Core.Auth.Enums; @@ -326,7 +329,8 @@ public class UserServiceTests sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), - sutProvider.GetDependency() + sutProvider.GetDependency(), + sutProvider.GetDependency() ); var actualIsVerified = await sut.VerifySecretAsync(user, secret); @@ -462,6 +466,78 @@ public class UserServiceTests .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization2.DisplayName(), user.Email); } + [Theory, BitAutoData] + public async Task DisableTwoFactorProviderAsync_WithPolicyRequirementsEnabled_WhenOrganizationHas2FAPolicyEnabled_DisablingAllProviders_RevokesUserAndSendsEmail( + SutProvider sutProvider, User user, + Organization organization1, Guid organizationUserId1, + Organization organization2, Guid organizationUserId2) + { + user.SetTwoFactorProviders(new Dictionary + { + [TwoFactorProviderType.Email] = new() { Enabled = true } + }); + organization1.Enabled = organization2.Enabled = true; + organization1.UseSso = organization2.UseSso = true; + + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = organization1.Id, + OrganizationUserId = organizationUserId1, + OrganizationUserStatus = OrganizationUserStatusType.Accepted, + PolicyType = PolicyType.TwoFactorAuthentication + }, + new PolicyDetails + { + OrganizationId = organization2.Id, + OrganizationUserId = organizationUserId2, + OrganizationUserStatus = OrganizationUserStatusType.Confirmed, + PolicyType = PolicyType.TwoFactorAuthentication + } + ])); + sutProvider.GetDependency() + .GetManyByIdsAsync(Arg.Is>(ids => ids.Contains(organization1.Id) && ids.Contains(organization2.Id))) + .Returns(new[] { organization1, organization2 }); + var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary(), JsonHelpers.LegacyEnumKeyResolver); + + await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders)); + await sutProvider.GetDependency() + .Received(1) + .LogUserEventAsync(user.Id, EventType.User_Disabled2fa); + + // Revoke the user from the first organization + await sutProvider.GetDependency() + .Received(1) + .RevokeNonCompliantOrganizationUsersAsync( + Arg.Is(r => r.OrganizationId == organization1.Id && + r.OrganizationUsers.First().Id == organizationUserId1 && + r.OrganizationUsers.First().OrganizationId == organization1.Id)); + await sutProvider.GetDependency() + .Received(1) + .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization1.DisplayName(), user.Email); + + // Remove the user from the second organization + await sutProvider.GetDependency() + .Received(1) + .RevokeNonCompliantOrganizationUsersAsync( + Arg.Is(r => r.OrganizationId == organization2.Id && + r.OrganizationUsers.First().Id == organizationUserId2 && + r.OrganizationUsers.First().OrganizationId == organization2.Id)); + await sutProvider.GetDependency() + .Received(1) + .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(organization2.DisplayName(), user.Email); + } + [Theory, BitAutoData] public async Task DisableTwoFactorProviderAsync_UserHasOneProviderEnabled_DoesNotRevokeUserFromOrganization( SutProvider sutProvider, User user, Organization organization) @@ -509,6 +585,53 @@ public class UserServiceTests .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(default, default); } + [Theory, BitAutoData] + public async Task DisableTwoFactorProviderAsync_WithPolicyRequirementsEnabled_UserHasOneProviderEnabled_DoesNotRevokeUserFromOrganization( + SutProvider sutProvider, User user, Organization organization) + { + user.SetTwoFactorProviders(new Dictionary + { + [TwoFactorProviderType.Email] = new() { Enabled = true }, + [TwoFactorProviderType.Remember] = new() { Enabled = true } + }); + sutProvider.GetDependency() + .IsEnabled(FeatureFlagKeys.PolicyRequirements) + .Returns(true); + sutProvider.GetDependency() + .GetAsync(user.Id) + .Returns(new RequireTwoFactorPolicyRequirement( + [ + new PolicyDetails + { + OrganizationId = organization.Id, + OrganizationUserStatus = OrganizationUserStatusType.Accepted, + PolicyType = PolicyType.TwoFactorAuthentication + } + ])); + sutProvider.GetDependency() + .GetByIdAsync(organization.Id) + .Returns(organization); + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(user) + .Returns(true); + var expectedSavedProviders = JsonHelpers.LegacySerialize(new Dictionary + { + [TwoFactorProviderType.Remember] = new() { Enabled = true } + }, JsonHelpers.LegacyEnumKeyResolver); + + await sutProvider.Sut.DisableTwoFactorProviderAsync(user, TwoFactorProviderType.Email); + + await sutProvider.GetDependency() + .Received(1) + .ReplaceAsync(Arg.Is(u => u.Id == user.Id && u.TwoFactorProviders == expectedSavedProviders)); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .RevokeNonCompliantOrganizationUsersAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendOrganizationUserRevokedForTwoFactorPolicyEmailAsync(default, default); + } + [Theory, BitAutoData] public async Task ResendNewDeviceVerificationEmail_UserNull_SendTwoFactorEmailAsyncNotCalled( SutProvider sutProvider, string email, string secret) @@ -800,7 +923,8 @@ public class UserServiceTests sutProvider.GetDependency(), sutProvider.GetDependency(), sutProvider.GetDependency(), - sutProvider.GetDependency() + sutProvider.GetDependency(), + sutProvider.GetDependency() ); } } From 6486354fbc39bb81222cdfc0b5c4bf10aa9508f8 Mon Sep 17 00:00:00 2001 From: Brant DeBow <125889545+brant-livefront@users.noreply.github.com> Date: Thu, 29 May 2025 08:22:11 -0400 Subject: [PATCH 110/114] [PM-17562] Add Azure Service Bus support for event integration retries (#5880) * [PM-17562] Add Azure Service Bus support for event integration retries * Cleanup AzureServiceBusIntegrationListenerService.cs; add nullable * Removed IntegrationHandlerBase* since it is no longer used (We removed the subclasses previously) * Changed strategy to assume ApplyRetry always gives us a non-null DelayUntilDate; Added test to confirm as well --- dev/servicebusemulator_config.json | 33 +++ .../AzureServiceBusEventListenerService.cs | 2 +- .../AzureServiceBusEventWriteService.cs | 2 +- ...ureServiceBusIntegrationListenerService.cs | 101 ++++++++ .../AzureServiceBusIntegrationPublisher.cs | 36 +++ .../IntegrationEventHandlerBase.cs | 66 ----- .../Implementations/SlackEventHandler.cs | 35 --- .../Implementations/WebhookEventHandler.cs | 38 --- src/Core/Settings/GlobalSettings.cs | 22 +- .../Utilities/ServiceCollectionExtensions.cs | 196 +++++++++------ .../Integrations/IntegrationMessageTests.cs | 1 + .../IntegrationEventHandlerBaseTests.cs | 219 ---------------- .../Services/SlackEventHandlerTests.cs | 181 -------------- .../Services/WebhookEventHandlerTests.cs | 235 ------------------ 14 files changed, 309 insertions(+), 858 deletions(-) create mode 100644 src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs create mode 100644 src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationPublisher.cs delete mode 100644 src/Core/AdminConsole/Services/Implementations/IntegrationEventHandlerBase.cs delete mode 100644 src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs delete mode 100644 src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs delete mode 100644 test/Core.Test/AdminConsole/Services/IntegrationEventHandlerBaseTests.cs delete mode 100644 test/Core.Test/AdminConsole/Services/SlackEventHandlerTests.cs delete mode 100644 test/Core.Test/AdminConsole/Services/WebhookEventHandlerTests.cs diff --git a/dev/servicebusemulator_config.json b/dev/servicebusemulator_config.json index 073a44618f..b107bc6190 100644 --- a/dev/servicebusemulator_config.json +++ b/dev/servicebusemulator_config.json @@ -33,6 +33,39 @@ "Name": "events-webhook-subscription" } ] + }, + { + "Name": "event-integrations", + "Subscriptions": [ + { + "Name": "integration-slack-subscription", + "Rules": [ + { + "Name": "slack-integration-filter", + "Properties": { + "FilterType": "Correlation", + "CorrelationFilter": { + "Label": "slack" + } + } + } + ] + }, + { + "Name": "integration-webhook-subscription", + "Rules": [ + { + "Name": "webhook-integration-filter", + "Properties": { + "FilterType": "Correlation", + "CorrelationFilter": { + "Label": "webhook" + } + } + } + ] + } + ] } ] } diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs index 4cd71ae77e..2ab10418a3 100644 --- a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventListenerService.cs @@ -20,7 +20,7 @@ public class AzureServiceBusEventListenerService : EventLoggingListenerService string subscriptionName) : base(handler) { _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); - _processor = _client.CreateProcessor(globalSettings.EventLogging.AzureServiceBus.TopicName, subscriptionName, new ServiceBusProcessorOptions()); + _processor = _client.CreateProcessor(globalSettings.EventLogging.AzureServiceBus.EventTopicName, subscriptionName, new ServiceBusProcessorOptions()); _logger = logger; } diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs index fc865b327c..224f86a802 100644 --- a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusEventWriteService.cs @@ -14,7 +14,7 @@ public class AzureServiceBusEventWriteService : IEventWriteService, IAsyncDispos public AzureServiceBusEventWriteService(GlobalSettings globalSettings) { _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); - _sender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.TopicName); + _sender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.EventTopicName); } public async Task CreateAsync(IEvent e) diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs new file mode 100644 index 0000000000..8244f39c09 --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationListenerService.cs @@ -0,0 +1,101 @@ +#nullable enable + +using Azure.Messaging.ServiceBus; +using Bit.Core.Settings; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.Services; + +public class AzureServiceBusIntegrationListenerService : BackgroundService +{ + private readonly int _maxRetries; + private readonly string _subscriptionName; + private readonly string _topicName; + private readonly IIntegrationHandler _handler; + private readonly ServiceBusClient _client; + private readonly ServiceBusProcessor _processor; + private readonly ServiceBusSender _sender; + private readonly ILogger _logger; + + public AzureServiceBusIntegrationListenerService( + IIntegrationHandler handler, + string subscriptionName, + GlobalSettings globalSettings, + ILogger logger) + { + _handler = handler; + _logger = logger; + _maxRetries = globalSettings.EventLogging.AzureServiceBus.MaxRetries; + _topicName = globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName; + _subscriptionName = subscriptionName; + + _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); + _processor = _client.CreateProcessor(_topicName, _subscriptionName, new ServiceBusProcessorOptions()); + _sender = _client.CreateSender(_topicName); + } + + protected override async Task ExecuteAsync(CancellationToken cancellationToken) + { + _processor.ProcessMessageAsync += HandleMessageAsync; + _processor.ProcessErrorAsync += args => + { + _logger.LogError(args.Exception, "Azure Service Bus error"); + return Task.CompletedTask; + }; + + await _processor.StartProcessingAsync(cancellationToken); + } + + public override async Task StopAsync(CancellationToken cancellationToken) + { + await _processor.StopProcessingAsync(cancellationToken); + await _processor.DisposeAsync(); + await _sender.DisposeAsync(); + await _client.DisposeAsync(); + await base.StopAsync(cancellationToken); + } + + private async Task HandleMessageAsync(ProcessMessageEventArgs args) + { + var json = args.Message.Body.ToString(); + + try + { + var result = await _handler.HandleAsync(json); + var message = result.Message; + + if (result.Success) + { + await args.CompleteMessageAsync(args.Message); + return; + } + + message.ApplyRetry(result.DelayUntilDate); + + if (result.Retryable && message.RetryCount < _maxRetries) + { + var scheduledTime = (DateTime)message.DelayUntilDate!; + var retryMsg = new ServiceBusMessage(message.ToJson()) + { + Subject = args.Message.Subject, + ScheduledEnqueueTime = scheduledTime + }; + + await _sender.SendMessageAsync(retryMsg); + } + else + { + await args.DeadLetterMessageAsync(args.Message, "Retry limit exceeded or non-retryable"); + return; + } + + await args.CompleteMessageAsync(args.Message); + } + catch (Exception ex) + { + _logger.LogError(ex, "Unhandled error processing ASB message"); + await args.CompleteMessageAsync(args.Message); + } + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationPublisher.cs b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationPublisher.cs new file mode 100644 index 0000000000..4a906e719f --- /dev/null +++ b/src/Core/AdminConsole/Services/Implementations/AzureServiceBusIntegrationPublisher.cs @@ -0,0 +1,36 @@ +using Azure.Messaging.ServiceBus; +using Bit.Core.AdminConsole.Models.Data.Integrations; +using Bit.Core.Enums; +using Bit.Core.Settings; + +namespace Bit.Core.Services; + +public class AzureServiceBusIntegrationPublisher : IIntegrationPublisher, IAsyncDisposable +{ + private readonly ServiceBusClient _client; + private readonly ServiceBusSender _sender; + + public AzureServiceBusIntegrationPublisher(GlobalSettings globalSettings) + { + _client = new ServiceBusClient(globalSettings.EventLogging.AzureServiceBus.ConnectionString); + _sender = _client.CreateSender(globalSettings.EventLogging.AzureServiceBus.IntegrationTopicName); + } + + public async Task PublishAsync(IIntegrationMessage message) + { + var json = message.ToJson(); + + var serviceBusMessage = new ServiceBusMessage(json) + { + Subject = message.IntegrationType.ToRoutingKey(), + }; + + await _sender.SendMessageAsync(serviceBusMessage); + } + + public async ValueTask DisposeAsync() + { + await _sender.DisposeAsync(); + await _client.DisposeAsync(); + } +} diff --git a/src/Core/AdminConsole/Services/Implementations/IntegrationEventHandlerBase.cs b/src/Core/AdminConsole/Services/Implementations/IntegrationEventHandlerBase.cs deleted file mode 100644 index 4df2d25b1b..0000000000 --- a/src/Core/AdminConsole/Services/Implementations/IntegrationEventHandlerBase.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System.Text.Json.Nodes; -using Bit.Core.AdminConsole.Models.Data.Integrations; -using Bit.Core.AdminConsole.Utilities; -using Bit.Core.Enums; -using Bit.Core.Models.Data; -using Bit.Core.Repositories; - -namespace Bit.Core.Services; - -public abstract class IntegrationEventHandlerBase( - IUserRepository userRepository, - IOrganizationRepository organizationRepository, - IOrganizationIntegrationConfigurationRepository configurationRepository) - : IEventMessageHandler -{ - public async Task HandleEventAsync(EventMessage eventMessage) - { - var organizationId = eventMessage.OrganizationId ?? Guid.Empty; - var configurations = await configurationRepository.GetConfigurationDetailsAsync( - organizationId, - GetIntegrationType(), - eventMessage.Type); - - foreach (var configuration in configurations) - { - var context = await BuildContextAsync(eventMessage, configuration.Template); - var renderedTemplate = IntegrationTemplateProcessor.ReplaceTokens(configuration.Template, context); - - await ProcessEventIntegrationAsync(configuration.MergedConfiguration, renderedTemplate); - } - } - - public async Task HandleManyEventsAsync(IEnumerable eventMessages) - { - foreach (var eventMessage in eventMessages) - { - await HandleEventAsync(eventMessage); - } - } - - private async Task BuildContextAsync(EventMessage eventMessage, string template) - { - var context = new IntegrationTemplateContext(eventMessage); - - if (IntegrationTemplateProcessor.TemplateRequiresUser(template) && eventMessage.UserId.HasValue) - { - context.User = await userRepository.GetByIdAsync(eventMessage.UserId.Value); - } - - if (IntegrationTemplateProcessor.TemplateRequiresActingUser(template) && eventMessage.ActingUserId.HasValue) - { - context.ActingUser = await userRepository.GetByIdAsync(eventMessage.ActingUserId.Value); - } - - if (IntegrationTemplateProcessor.TemplateRequiresOrganization(template) && eventMessage.OrganizationId.HasValue) - { - context.Organization = await organizationRepository.GetByIdAsync(eventMessage.OrganizationId.Value); - } - - return context; - } - - protected abstract IntegrationType GetIntegrationType(); - - protected abstract Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration, string renderedTemplate); -} diff --git a/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs b/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs deleted file mode 100644 index a767776c36..0000000000 --- a/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Nodes; -using Bit.Core.AdminConsole.Models.Data.Integrations; -using Bit.Core.Enums; -using Bit.Core.Repositories; - -#nullable enable - -namespace Bit.Core.Services; - -public class SlackEventHandler( - IUserRepository userRepository, - IOrganizationRepository organizationRepository, - IOrganizationIntegrationConfigurationRepository configurationRepository, - ISlackService slackService) - : IntegrationEventHandlerBase(userRepository, organizationRepository, configurationRepository) -{ - protected override IntegrationType GetIntegrationType() => IntegrationType.Slack; - - protected override async Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration, - string renderedTemplate) - { - var config = mergedConfiguration.Deserialize(); - if (config is null) - { - return; - } - - await slackService.SendSlackMessageByChannelIdAsync( - config.token, - renderedTemplate, - config.channelId - ); - } -} diff --git a/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs b/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs deleted file mode 100644 index 97453497bc..0000000000 --- a/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Text; -using System.Text.Json; -using System.Text.Json.Nodes; -using Bit.Core.AdminConsole.Models.Data.Integrations; -using Bit.Core.Enums; -using Bit.Core.Repositories; - -#nullable enable - -namespace Bit.Core.Services; - -public class WebhookEventHandler( - IHttpClientFactory httpClientFactory, - IUserRepository userRepository, - IOrganizationRepository organizationRepository, - IOrganizationIntegrationConfigurationRepository configurationRepository) - : IntegrationEventHandlerBase(userRepository, organizationRepository, configurationRepository) -{ - private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName); - - public const string HttpClientName = "WebhookEventHandlerHttpClient"; - - protected override IntegrationType GetIntegrationType() => IntegrationType.Webhook; - - protected override async Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration, - string renderedTemplate) - { - var config = mergedConfiguration.Deserialize(); - if (config is null || string.IsNullOrEmpty(config.url)) - { - return; - } - - var content = new StringContent(renderedTemplate, Encoding.UTF8, "application/json"); - var response = await _httpClient.PostAsync(config.url, content); - response.EnsureSuccessStatusCode(); - } -} diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index d3f4253908..e228218a29 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -288,11 +288,15 @@ public class GlobalSettings : IGlobalSettings public class AzureServiceBusSettings { private string _connectionString; - private string _topicName; + private string _eventTopicName; + private string _integrationTopicName; + public int MaxRetries { get; set; } = 3; public virtual string EventRepositorySubscriptionName { get; set; } = "events-write-subscription"; - public virtual string SlackSubscriptionName { get; set; } = "events-slack-subscription"; - public virtual string WebhookSubscriptionName { get; set; } = "events-webhook-subscription"; + public virtual string SlackEventSubscriptionName { get; set; } = "events-slack-subscription"; + public virtual string SlackIntegrationSubscriptionName { get; set; } = "integration-slack-subscription"; + public virtual string WebhookEventSubscriptionName { get; set; } = "events-webhook-subscription"; + public virtual string WebhookIntegrationSubscriptionName { get; set; } = "integration-webhook-subscription"; public string ConnectionString { @@ -300,10 +304,16 @@ public class GlobalSettings : IGlobalSettings set => _connectionString = value.Trim('"'); } - public string TopicName + public string EventTopicName { - get => _topicName; - set => _topicName = value.Trim('"'); + get => _eventTopicName; + set => _eventTopicName = value.Trim('"'); + } + + public string IntegrationTopicName + { + get => _integrationTopicName; + set => _integrationTopicName = value.Trim('"'); } } diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index e425cf7254..247d4c5d43 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -557,7 +557,7 @@ public static class ServiceCollectionExtensions services.AddKeyedSingleton("storage"); if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName)) + CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.EventTopicName)) { services.AddKeyedSingleton("broadcast"); } @@ -589,86 +589,83 @@ public static class ServiceCollectionExtensions return services; } + private static IServiceCollection AddAzureServiceBusEventRepositoryListener(this IServiceCollection services, GlobalSettings globalSettings) + { + services.AddSingleton(); + services.AddSingleton(); + services.AddKeyedSingleton("persistent"); + + services.AddSingleton(provider => + new AzureServiceBusEventListenerService( + handler: provider.GetRequiredService(), + logger: provider.GetRequiredService>(), + globalSettings: globalSettings, + subscriptionName: globalSettings.EventLogging.AzureServiceBus.EventRepositorySubscriptionName)); + + return services; + } + + private static IServiceCollection AddAzureServiceBusIntegration( + this IServiceCollection services, + string eventSubscriptionName, + string integrationSubscriptionName, + IntegrationType integrationType, + GlobalSettings globalSettings) + where TConfig : class + where THandler : class, IIntegrationHandler + { + var routingKey = integrationType.ToRoutingKey(); + + services.AddSingleton(); + + services.AddKeyedSingleton(routingKey, (provider, _) => + new EventIntegrationHandler( + integrationType, + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService(), + provider.GetRequiredService())); + + services.AddSingleton(provider => + new AzureServiceBusEventListenerService( + handler: provider.GetRequiredKeyedService(routingKey), + logger: provider.GetRequiredService>(), + globalSettings: globalSettings, + subscriptionName: eventSubscriptionName)); + + services.AddSingleton, THandler>(); + + services.AddSingleton(provider => + new AzureServiceBusIntegrationListenerService( + handler: provider.GetRequiredService>(), + subscriptionName: integrationSubscriptionName, + logger: provider.GetRequiredService>(), + globalSettings: globalSettings)); + + return services; + } + public static IServiceCollection AddAzureServiceBusListeners(this IServiceCollection services, GlobalSettings globalSettings) { - if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) && - CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName)) - { - services.AddSingleton(); - services.AddSingleton(); - services.AddKeyedSingleton("persistent"); - services.AddSingleton(provider => - new AzureServiceBusEventListenerService( - provider.GetRequiredService(), - provider.GetRequiredService>(), - globalSettings, - globalSettings.EventLogging.AzureServiceBus.EventRepositorySubscriptionName)); + if (!CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) || + !CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.EventTopicName)) + return services; + services.AddAzureServiceBusEventRepositoryListener(globalSettings); - services.AddSlackService(globalSettings); - services.AddSingleton(); - services.AddSingleton(provider => - new AzureServiceBusEventListenerService( - provider.GetRequiredService(), - provider.GetRequiredService>(), - globalSettings, - globalSettings.EventLogging.AzureServiceBus.SlackSubscriptionName)); - - services.AddSingleton(); - services.AddHttpClient(WebhookEventHandler.HttpClientName); - services.AddSingleton(provider => - new AzureServiceBusEventListenerService( - provider.GetRequiredService(), - provider.GetRequiredService>(), - globalSettings, - globalSettings.EventLogging.AzureServiceBus.WebhookSubscriptionName)); - } - - return services; - } - - public static IServiceCollection AddRabbitMqListeners(this IServiceCollection services, GlobalSettings globalSettings) - { - if (IsRabbitMqEnabled(globalSettings)) - { - services.AddRabbitMqEventRepositoryListener(globalSettings); - - services.AddSlackService(globalSettings); - services.AddRabbitMqIntegration( - globalSettings.EventLogging.RabbitMq.SlackEventsQueueName, - globalSettings.EventLogging.RabbitMq.SlackIntegrationQueueName, - globalSettings.EventLogging.RabbitMq.SlackIntegrationRetryQueueName, - globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName, - IntegrationType.Slack, - globalSettings); - - services.AddHttpClient(WebhookIntegrationHandler.HttpClientName); - services.AddRabbitMqIntegration( - globalSettings.EventLogging.RabbitMq.WebhookEventsQueueName, - globalSettings.EventLogging.RabbitMq.WebhookIntegrationQueueName, - globalSettings.EventLogging.RabbitMq.WebhookIntegrationRetryQueueName, - globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName, - IntegrationType.Webhook, - globalSettings); - } - - return services; - } - - public static IServiceCollection AddSlackService(this IServiceCollection services, GlobalSettings globalSettings) - { - if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) && - CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) && - CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes)) - { - services.AddHttpClient(SlackService.HttpClientName); - services.AddSingleton(); - } - else - { - services.AddSingleton(); - } + services.AddSlackService(globalSettings); + services.AddAzureServiceBusIntegration( + eventSubscriptionName: globalSettings.EventLogging.AzureServiceBus.SlackEventSubscriptionName, + integrationSubscriptionName: globalSettings.EventLogging.AzureServiceBus.SlackIntegrationSubscriptionName, + integrationType: IntegrationType.Slack, + globalSettings: globalSettings); + services.AddHttpClient(WebhookIntegrationHandler.HttpClientName); + services.AddAzureServiceBusIntegration( + eventSubscriptionName: globalSettings.EventLogging.AzureServiceBus.WebhookEventSubscriptionName, + integrationSubscriptionName: globalSettings.EventLogging.AzureServiceBus.WebhookIntegrationSubscriptionName, + integrationType: IntegrationType.Webhook, + globalSettings: globalSettings); return services; } @@ -729,6 +726,36 @@ public static class ServiceCollectionExtensions return services; } + public static IServiceCollection AddRabbitMqListeners(this IServiceCollection services, GlobalSettings globalSettings) + { + if (!IsRabbitMqEnabled(globalSettings)) + { + return services; + } + + services.AddRabbitMqEventRepositoryListener(globalSettings); + + services.AddSlackService(globalSettings); + services.AddRabbitMqIntegration( + globalSettings.EventLogging.RabbitMq.SlackEventsQueueName, + globalSettings.EventLogging.RabbitMq.SlackIntegrationQueueName, + globalSettings.EventLogging.RabbitMq.SlackIntegrationRetryQueueName, + globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName, + IntegrationType.Slack, + globalSettings); + + services.AddHttpClient(WebhookIntegrationHandler.HttpClientName); + services.AddRabbitMqIntegration( + globalSettings.EventLogging.RabbitMq.WebhookEventsQueueName, + globalSettings.EventLogging.RabbitMq.WebhookIntegrationQueueName, + globalSettings.EventLogging.RabbitMq.WebhookIntegrationRetryQueueName, + globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName, + IntegrationType.Webhook, + globalSettings); + + return services; + } + private static bool IsRabbitMqEnabled(GlobalSettings settings) { return CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.HostName) && @@ -737,6 +764,23 @@ public static class ServiceCollectionExtensions CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.EventExchangeName); } + public static IServiceCollection AddSlackService(this IServiceCollection services, GlobalSettings globalSettings) + { + if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) && + CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) && + CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes)) + { + services.AddHttpClient(SlackService.HttpClientName); + services.AddSingleton(); + } + else + { + services.AddSingleton(); + } + + return services; + } + public static void UseDefaultMiddleware(this IApplicationBuilder app, IWebHostEnvironment env, GlobalSettings globalSettings) { diff --git a/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs b/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs index 44774449c1..0946841347 100644 --- a/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs +++ b/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs @@ -20,6 +20,7 @@ public class IntegrationMessageTests message.ApplyRetry(baseline); Assert.Equal(3, message.RetryCount); + Assert.NotNull(message.DelayUntilDate); Assert.True(message.DelayUntilDate > baseline); } diff --git a/test/Core.Test/AdminConsole/Services/IntegrationEventHandlerBaseTests.cs b/test/Core.Test/AdminConsole/Services/IntegrationEventHandlerBaseTests.cs deleted file mode 100644 index e1a2fbff68..0000000000 --- a/test/Core.Test/AdminConsole/Services/IntegrationEventHandlerBaseTests.cs +++ /dev/null @@ -1,219 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Nodes; -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Entities; -using Bit.Core.Enums; -using Bit.Core.Models.Data; -using Bit.Core.Models.Data.Organizations; -using Bit.Core.Repositories; -using Bit.Core.Services; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using NSubstitute; -using Xunit; - -namespace Bit.Core.Test.Services; - -[SutProviderCustomize] -public class IntegrationEventHandlerBaseEventHandlerTests -{ - private const string _templateBase = "Date: #Date#, Type: #Type#, UserId: #UserId#"; - private const string _templateWithOrganization = "Org: #OrganizationName#"; - private const string _templateWithUser = "#UserName#, #UserEmail#"; - private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#"; - private const string _url = "https://localhost"; - - private SutProvider GetSutProvider( - List configurations) - { - var configurationRepository = Substitute.For(); - configurationRepository.GetConfigurationDetailsAsync(Arg.Any(), - IntegrationType.Webhook, Arg.Any()).Returns(configurations); - - return new SutProvider() - .SetDependency(configurationRepository) - .Create(); - } - - private static List NoConfigurations() - { - return []; - } - - private static List OneConfiguration(string template) - { - var config = Substitute.For(); - config.Configuration = null; - config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url }); - config.Template = template; - - return [config]; - } - - private static List TwoConfigurations(string template) - { - var config = Substitute.For(); - config.Configuration = null; - config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url }); - config.Template = template; - var config2 = Substitute.For(); - config2.Configuration = null; - config2.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url }); - config2.Template = template; - - return [config, config2]; - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_BaseTemplateNoConfigurations_DoesNothing(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(NoConfigurations()); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - Assert.Empty(sutProvider.Sut.CapturedCalls); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_BaseTemplateOneConfiguration_CallsProcessEventIntegrationAsync(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(OneConfiguration(_templateBase)); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - - Assert.Single(sutProvider.Sut.CapturedCalls); - - var expectedTemplate = $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"; - - Assert.Equal(expectedTemplate, sutProvider.Sut.CapturedCalls.Single().RenderedTemplate); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_ActingUserTemplate_LoadsUserFromRepository(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser)); - var user = Substitute.For(); - user.Email = "test@example.com"; - user.Name = "Test"; - - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(user); - await sutProvider.Sut.HandleEventAsync(eventMessage); - - Assert.Single(sutProvider.Sut.CapturedCalls); - - var expectedTemplate = $"{user.Name}, {user.Email}"; - Assert.Equal(expectedTemplate, sutProvider.Sut.CapturedCalls.Single().RenderedTemplate); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.ActingUserId ?? Guid.Empty); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_OrganizationTemplate_LoadsOrganizationFromRepository(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization)); - var organization = Substitute.For(); - organization.Name = "Test"; - - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(organization); - await sutProvider.Sut.HandleEventAsync(eventMessage); - - Assert.Single(sutProvider.Sut.CapturedCalls); - - var expectedTemplate = $"Org: {organization.Name}"; - Assert.Equal(expectedTemplate, sutProvider.Sut.CapturedCalls.Single().RenderedTemplate); - await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.OrganizationId ?? Guid.Empty); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_UserTemplate_LoadsUserFromRepository(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser)); - var user = Substitute.For(); - user.Email = "test@example.com"; - user.Name = "Test"; - - sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(user); - await sutProvider.Sut.HandleEventAsync(eventMessage); - - Assert.Single(sutProvider.Sut.CapturedCalls); - - var expectedTemplate = $"{user.Name}, {user.Email}"; - Assert.Equal(expectedTemplate, sutProvider.Sut.CapturedCalls.Single().RenderedTemplate); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any()); - await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.UserId ?? Guid.Empty); - } - - [Theory, BitAutoData] - public async Task HandleManyEventsAsync_BaseTemplateNoConfigurations_DoesNothing(List eventMessages) - { - var sutProvider = GetSutProvider(NoConfigurations()); - - await sutProvider.Sut.HandleManyEventsAsync(eventMessages); - Assert.Empty(sutProvider.Sut.CapturedCalls); - } - - [Theory, BitAutoData] - public async Task HandleManyEventsAsync_BaseTemplateOneConfiguration_CallsProcessEventIntegrationAsync(List eventMessages) - { - var sutProvider = GetSutProvider(OneConfiguration(_templateBase)); - - await sutProvider.Sut.HandleManyEventsAsync(eventMessages); - - Assert.Equal(eventMessages.Count, sutProvider.Sut.CapturedCalls.Count); - var index = 0; - foreach (var call in sutProvider.Sut.CapturedCalls) - { - var expected = eventMessages[index]; - var expectedTemplate = $"Date: {expected.Date}, Type: {expected.Type}, UserId: {expected.UserId}"; - - Assert.Equal(expectedTemplate, call.RenderedTemplate); - index++; - } - } - - [Theory, BitAutoData] - public async Task HandleManyEventsAsync_BaseTemplateTwoConfigurations_CallsProcessEventIntegrationAsyncMultipleTimes( - List eventMessages) - { - var sutProvider = GetSutProvider(TwoConfigurations(_templateBase)); - - await sutProvider.Sut.HandleManyEventsAsync(eventMessages); - - Assert.Equal(eventMessages.Count * 2, sutProvider.Sut.CapturedCalls.Count); - - var capturedCalls = sutProvider.Sut.CapturedCalls.GetEnumerator(); - foreach (var eventMessage in eventMessages) - { - var expectedTemplate = $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"; - - Assert.True(capturedCalls.MoveNext()); - var call = capturedCalls.Current; - Assert.Equal(expectedTemplate, call.RenderedTemplate); - - Assert.True(capturedCalls.MoveNext()); - call = capturedCalls.Current; - Assert.Equal(expectedTemplate, call.RenderedTemplate); - } - } - - private class TestIntegrationEventHandlerBase : IntegrationEventHandlerBase - { - public TestIntegrationEventHandlerBase(IUserRepository userRepository, - IOrganizationRepository organizationRepository, - IOrganizationIntegrationConfigurationRepository configurationRepository) - : base(userRepository, organizationRepository, configurationRepository) - { } - - public List<(JsonObject MergedConfiguration, string RenderedTemplate)> CapturedCalls { get; } = new(); - - protected override IntegrationType GetIntegrationType() => IntegrationType.Webhook; - - protected override Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration, string renderedTemplate) - { - CapturedCalls.Add((mergedConfiguration, renderedTemplate)); - return Task.CompletedTask; - } - } -} diff --git a/test/Core.Test/AdminConsole/Services/SlackEventHandlerTests.cs b/test/Core.Test/AdminConsole/Services/SlackEventHandlerTests.cs deleted file mode 100644 index 558bded8b3..0000000000 --- a/test/Core.Test/AdminConsole/Services/SlackEventHandlerTests.cs +++ /dev/null @@ -1,181 +0,0 @@ -using System.Text.Json; -using Bit.Core.Enums; -using Bit.Core.Models.Data; -using Bit.Core.Models.Data.Organizations; -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 NSubstitute; -using Xunit; - -namespace Bit.Core.Test.Services; - -[SutProviderCustomize] -public class SlackEventHandlerTests -{ - private readonly IOrganizationIntegrationConfigurationRepository _repository = Substitute.For(); - private readonly ISlackService _slackService = Substitute.For(); - private readonly string _channelId = "C12345"; - private readonly string _channelId2 = "C67890"; - private readonly string _token = "xoxb-test-token"; - private readonly string _token2 = "xoxb-another-test-token"; - - private SutProvider GetSutProvider( - List integrationConfigurations) - { - _repository.GetConfigurationDetailsAsync(Arg.Any(), - IntegrationType.Slack, Arg.Any()) - .Returns(integrationConfigurations); - - return new SutProvider() - .SetDependency(_repository) - .SetDependency(_slackService) - .Create(); - } - - private List NoConfigurations() - { - return []; - } - - private List OneConfiguration() - { - var config = Substitute.For(); - config.Configuration = JsonSerializer.Serialize(new { token = _token }); - config.IntegrationConfiguration = JsonSerializer.Serialize(new { channelId = _channelId }); - config.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#"; - - return [config]; - } - - private List TwoConfigurations() - { - var config = Substitute.For(); - config.Configuration = JsonSerializer.Serialize(new { token = _token }); - config.IntegrationConfiguration = JsonSerializer.Serialize(new { channelId = _channelId }); - config.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#"; - var config2 = Substitute.For(); - config2.Configuration = JsonSerializer.Serialize(new { token = _token2 }); - config2.IntegrationConfiguration = JsonSerializer.Serialize(new { channelId = _channelId2 }); - config2.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#"; - - return [config, config2]; - } - - private List WrongConfiguration() - { - var config = Substitute.For(); - config.Configuration = JsonSerializer.Serialize(new { }); - config.IntegrationConfiguration = JsonSerializer.Serialize(new { }); - config.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#"; - - return [config]; - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_NoConfigurations_DoesNothing(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(NoConfigurations()); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - sutProvider.GetDependency().DidNotReceiveWithAnyArgs(); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_OneConfiguration_SendsEventViaSlackService(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(OneConfiguration()); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - await sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync( - Arg.Is(AssertHelper.AssertPropertyEqual(_token)), - Arg.Is(AssertHelper.AssertPropertyEqual( - $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")), - Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)) - ); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_TwoConfigurations_SendsMultipleEvents(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(TwoConfigurations()); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - await sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync( - Arg.Is(AssertHelper.AssertPropertyEqual(_token)), - Arg.Is(AssertHelper.AssertPropertyEqual( - $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")), - Arg.Is(AssertHelper.AssertPropertyEqual(_channelId)) - ); - await sutProvider.GetDependency().Received(1).SendSlackMessageByChannelIdAsync( - Arg.Is(AssertHelper.AssertPropertyEqual(_token2)), - Arg.Is(AssertHelper.AssertPropertyEqual( - $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")), - Arg.Is(AssertHelper.AssertPropertyEqual(_channelId2)) - ); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_WrongConfiguration_DoesNothing(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(WrongConfiguration()); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - sutProvider.GetDependency().DidNotReceiveWithAnyArgs(); - } - - [Theory, BitAutoData] - public async Task HandleManyEventsAsync_OneConfiguration_SendsEventsViaSlackService(List eventMessages) - { - var sutProvider = GetSutProvider(OneConfiguration()); - - await sutProvider.Sut.HandleManyEventsAsync(eventMessages); - - var received = sutProvider.GetDependency().ReceivedCalls(); - using var calls = received.GetEnumerator(); - - Assert.Equal(eventMessages.Count, received.Count()); - - foreach (var eventMessage in eventMessages) - { - Assert.True(calls.MoveNext()); - var arguments = calls.Current.GetArguments(); - Assert.Equal(_token, arguments[0] as string); - Assert.Equal($"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}", - arguments[1] as string); - Assert.Equal(_channelId, arguments[2] as string); - } - } - - [Theory, BitAutoData] - public async Task HandleManyEventsAsync_TwoConfigurations_SendsMultipleEvents(List eventMessages) - { - var sutProvider = GetSutProvider(TwoConfigurations()); - - await sutProvider.Sut.HandleManyEventsAsync(eventMessages); - - var received = sutProvider.GetDependency().ReceivedCalls(); - using var calls = received.GetEnumerator(); - - Assert.Equal(eventMessages.Count * 2, received.Count()); - - foreach (var eventMessage in eventMessages) - { - Assert.True(calls.MoveNext()); - var arguments = calls.Current.GetArguments(); - Assert.Equal(_token, arguments[0] as string); - Assert.Equal($"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}", - arguments[1] as string); - Assert.Equal(_channelId, arguments[2] as string); - - Assert.True(calls.MoveNext()); - var arguments2 = calls.Current.GetArguments(); - Assert.Equal(_token2, arguments2[0] as string); - Assert.Equal($"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}", - arguments2[1] as string); - Assert.Equal(_channelId2, arguments2[2] as string); - } - } -} diff --git a/test/Core.Test/AdminConsole/Services/WebhookEventHandlerTests.cs b/test/Core.Test/AdminConsole/Services/WebhookEventHandlerTests.cs deleted file mode 100644 index c426f8eaad..0000000000 --- a/test/Core.Test/AdminConsole/Services/WebhookEventHandlerTests.cs +++ /dev/null @@ -1,235 +0,0 @@ -using System.Net; -using System.Net.Http.Json; -using System.Text.Json; -using Bit.Core.Enums; -using Bit.Core.Models.Data; -using Bit.Core.Models.Data.Organizations; -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 Bit.Test.Common.MockedHttpClient; -using NSubstitute; -using Xunit; - -namespace Bit.Core.Test.Services; - -[SutProviderCustomize] -public class WebhookEventHandlerTests -{ - private readonly MockedHttpMessageHandler _handler; - private readonly HttpClient _httpClient; - - private const string _template = - """ - { - "Date": "#Date#", - "Type": "#Type#", - "UserId": "#UserId#" - } - """; - private const string _webhookUrl = "http://localhost/test/event"; - private const string _webhookUrl2 = "http://localhost/another/event"; - - public WebhookEventHandlerTests() - { - _handler = new MockedHttpMessageHandler(); - _handler.Fallback - .WithStatusCode(HttpStatusCode.OK) - .WithContent(new StringContent("testtest")); - _httpClient = _handler.ToHttpClient(); - } - - private SutProvider GetSutProvider( - List configurations) - { - var clientFactory = Substitute.For(); - clientFactory.CreateClient(WebhookEventHandler.HttpClientName).Returns(_httpClient); - - var repository = Substitute.For(); - repository.GetConfigurationDetailsAsync(Arg.Any(), - IntegrationType.Webhook, Arg.Any()).Returns(configurations); - - return new SutProvider() - .SetDependency(repository) - .SetDependency(clientFactory) - .Create(); - } - - private static List NoConfigurations() - { - return []; - } - - private static List OneConfiguration() - { - var config = Substitute.For(); - config.Configuration = null; - config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _webhookUrl }); - config.Template = _template; - - return [config]; - } - - private static List TwoConfigurations() - { - var config = Substitute.For(); - config.Configuration = null; - config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _webhookUrl }); - config.Template = _template; - var config2 = Substitute.For(); - config2.Configuration = null; - config2.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _webhookUrl2 }); - config2.Template = _template; - - return [config, config2]; - } - - private static List WrongConfiguration() - { - var config = Substitute.For(); - config.Configuration = null; - config.IntegrationConfiguration = JsonSerializer.Serialize(new { error = string.Empty }); - config.Template = _template; - - return [config]; - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_NoConfigurations_DoesNothing(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(NoConfigurations()); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - sutProvider.GetDependency().Received(1).CreateClient( - Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName)) - ); - - Assert.Empty(_handler.CapturedRequests); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_OneConfiguration_PostsEventToUrl(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(OneConfiguration()); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - sutProvider.GetDependency().Received(1).CreateClient( - Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName)) - ); - - Assert.Single(_handler.CapturedRequests); - var request = _handler.CapturedRequests[0]; - Assert.NotNull(request); - var returned = await request.Content.ReadFromJsonAsync(); - var expected = MockEvent.From(eventMessage); - - Assert.Equal(HttpMethod.Post, request.Method); - Assert.Equal(_webhookUrl, request.RequestUri.ToString()); - AssertHelper.AssertPropertyEqual(expected, returned); - } - - [Theory, BitAutoData] - public async Task HandleEventAsync_WrongConfigurations_DoesNothing(EventMessage eventMessage) - { - var sutProvider = GetSutProvider(WrongConfiguration()); - - await sutProvider.Sut.HandleEventAsync(eventMessage); - sutProvider.GetDependency().Received(1).CreateClient( - Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName)) - ); - - Assert.Empty(_handler.CapturedRequests); - } - - [Theory, BitAutoData] - public async Task HandleManyEventsAsync_NoConfigurations_DoesNothing(List eventMessages) - { - var sutProvider = GetSutProvider(NoConfigurations()); - - await sutProvider.Sut.HandleManyEventsAsync(eventMessages); - sutProvider.GetDependency().Received(1).CreateClient( - Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName)) - ); - - Assert.Empty(_handler.CapturedRequests); - } - - - [Theory, BitAutoData] - public async Task HandleManyEventsAsync_OneConfiguration_PostsEventsToUrl(List eventMessages) - { - var sutProvider = GetSutProvider(OneConfiguration()); - - await sutProvider.Sut.HandleManyEventsAsync(eventMessages); - sutProvider.GetDependency().Received(1).CreateClient( - Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName)) - ); - - Assert.Equal(eventMessages.Count, _handler.CapturedRequests.Count); - var index = 0; - foreach (var request in _handler.CapturedRequests) - { - Assert.NotNull(request); - var returned = await request.Content.ReadFromJsonAsync(); - var expected = MockEvent.From(eventMessages[index]); - - Assert.Equal(HttpMethod.Post, request.Method); - Assert.Equal(_webhookUrl, request.RequestUri.ToString()); - AssertHelper.AssertPropertyEqual(expected, returned); - index++; - } - } - - [Theory, BitAutoData] - public async Task HandleManyEventsAsync_TwoConfigurations_PostsEventsToMultipleUrls(List eventMessages) - { - var sutProvider = GetSutProvider(TwoConfigurations()); - - await sutProvider.Sut.HandleManyEventsAsync(eventMessages); - sutProvider.GetDependency().Received(1).CreateClient( - Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName)) - ); - - using var capturedRequests = _handler.CapturedRequests.GetEnumerator(); - Assert.Equal(eventMessages.Count * 2, _handler.CapturedRequests.Count); - - foreach (var eventMessage in eventMessages) - { - var expected = MockEvent.From(eventMessage); - - Assert.True(capturedRequests.MoveNext()); - var request = capturedRequests.Current; - Assert.NotNull(request); - Assert.Equal(HttpMethod.Post, request.Method); - Assert.Equal(_webhookUrl, request.RequestUri.ToString()); - var returned = await request.Content.ReadFromJsonAsync(); - AssertHelper.AssertPropertyEqual(expected, returned); - - Assert.True(capturedRequests.MoveNext()); - request = capturedRequests.Current; - Assert.NotNull(request); - Assert.Equal(HttpMethod.Post, request.Method); - Assert.Equal(_webhookUrl2, request.RequestUri.ToString()); - returned = await request.Content.ReadFromJsonAsync(); - AssertHelper.AssertPropertyEqual(expected, returned); - } - } -} - -public class MockEvent(string date, string type, string userId) -{ - public string Date { get; set; } = date; - public string Type { get; set; } = type; - public string UserId { get; set; } = userId; - - public static MockEvent From(EventMessage eventMessage) - { - return new MockEvent( - eventMessage.Date.ToString(), - eventMessage.Type.ToString(), - eventMessage.UserId.ToString() - ); - } -} From 930fe29c8234c69946affcdf7c494abae7182493 Mon Sep 17 00:00:00 2001 From: Brandon Treston Date: Thu, 29 May 2025 09:55:33 -0400 Subject: [PATCH 111/114] replace owner/admins list with table (#5892) --- .../Models/OrganizationViewModel.cs | 4 ++ .../Organizations/_ViewInformation.cshtml | 52 ++++++++++++++++--- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/src/Admin/AdminConsole/Models/OrganizationViewModel.cs b/src/Admin/AdminConsole/Models/OrganizationViewModel.cs index 69486bdcd2..412b17b3d7 100644 --- a/src/Admin/AdminConsole/Models/OrganizationViewModel.cs +++ b/src/Admin/AdminConsole/Models/OrganizationViewModel.cs @@ -44,6 +44,8 @@ public class OrganizationViewModel orgUsers .Where(u => u.Type == OrganizationUserType.Admin && u.Status == organizationUserStatus) .Select(u => u.Email)); + OwnersDetails = orgUsers.Where(u => u.Type == OrganizationUserType.Owner && u.Status == organizationUserStatus); + AdminsDetails = orgUsers.Where(u => u.Type == OrganizationUserType.Admin && u.Status == organizationUserStatus); SecretsCount = secretsCount; ProjectsCount = projectCount; ServiceAccountsCount = serviceAccountsCount; @@ -70,4 +72,6 @@ public class OrganizationViewModel public int OccupiedSmSeatsCount { get; set; } public bool UseSecretsManager => Organization.UseSecretsManager; public bool UseRiskInsights => Organization.UseRiskInsights; + public IEnumerable OwnersDetails { get; set; } + public IEnumerable AdminsDetails { get; set; } } diff --git a/src/Admin/AdminConsole/Views/Organizations/_ViewInformation.cshtml b/src/Admin/AdminConsole/Views/Organizations/_ViewInformation.cshtml index a0d421235d..9b2f7d69f8 100644 --- a/src/Admin/AdminConsole/Views/Organizations/_ViewInformation.cshtml +++ b/src/Admin/AdminConsole/Views/Organizations/_ViewInformation.cshtml @@ -19,12 +19,6 @@ @Model.UserConfirmedCount) -
    Owners
    -
    @(string.IsNullOrWhiteSpace(Model.Owners) ? "None" : Model.Owners)
    - -
    Admins
    -
    @(string.IsNullOrWhiteSpace(Model.Admins) ? "None" : Model.Admins)
    -
    Using 2FA
    @(Model.Organization.TwoFactorIsEnabled() ? "Yes" : "No")
    @@ -76,3 +70,49 @@
    Secrets Manager Seats
    @(Model.UseSecretsManager ? Model.OccupiedSmSeatsCount: "N/A" )
    + +

    Administrators

    +
    +
    +
    + + + + + + + + + + @if(!Model.Admins.Any() && !Model.Owners.Any()) + { + + + + } + else + { + @foreach(var owner in Model.OwnersDetails) + { + + + + + + } + + @foreach(var admin in Model.AdminsDetails) + { + + + + + + + } + } + +
    EmailRoleStatus
    No results to list.
    @owner.EmailOwner@owner.Status
    @admin.EmailAdmin@admin.Status
    +
    +
    +
    From 5972ac147ec300e67b0b572d65f48525c69c08de Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Thu, 29 May 2025 15:06:25 +0100 Subject: [PATCH 112/114] [PM-21603]Invite Member sub text seat count does not account for sponsorships (#5889) * Add Occupied Seats at part metadata * resolve the failing test Signed-off-by: Cy Okeke --------- Signed-off-by: Cy Okeke --- .../Models/Responses/OrganizationMetadataResponse.cs | 6 ++++-- src/Core/Billing/Models/OrganizationMetadata.cs | 6 ++++-- .../Services/Implementations/OrganizationBillingService.cs | 6 +++++- .../Controllers/OrganizationBillingControllerTests.cs | 2 +- 4 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs b/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs index 1dfc79be21..341dbceadf 100644 --- a/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs +++ b/src/Api/Billing/Models/Responses/OrganizationMetadataResponse.cs @@ -12,7 +12,8 @@ public record OrganizationMetadataResponse( bool IsSubscriptionCanceled, DateTime? InvoiceDueDate, DateTime? InvoiceCreatedDate, - DateTime? SubPeriodEndDate) + DateTime? SubPeriodEndDate, + int OrganizationOccupiedSeats) { public static OrganizationMetadataResponse From(OrganizationMetadata metadata) => new( @@ -25,5 +26,6 @@ public record OrganizationMetadataResponse( metadata.IsSubscriptionCanceled, metadata.InvoiceDueDate, metadata.InvoiceCreatedDate, - metadata.SubPeriodEndDate); + metadata.SubPeriodEndDate, + metadata.OrganizationOccupiedSeats); } diff --git a/src/Core/Billing/Models/OrganizationMetadata.cs b/src/Core/Billing/Models/OrganizationMetadata.cs index 41666949bf..0f2bf9a454 100644 --- a/src/Core/Billing/Models/OrganizationMetadata.cs +++ b/src/Core/Billing/Models/OrganizationMetadata.cs @@ -10,7 +10,8 @@ public record OrganizationMetadata( bool IsSubscriptionCanceled, DateTime? InvoiceDueDate, DateTime? InvoiceCreatedDate, - DateTime? SubPeriodEndDate) + DateTime? SubPeriodEndDate, + int OrganizationOccupiedSeats) { public static OrganizationMetadata Default => new OrganizationMetadata( false, @@ -22,5 +23,6 @@ public record OrganizationMetadata( false, null, null, - null); + null, + 0); } diff --git a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs index 95df34dfd4..c647e825b6 100644 --- a/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs +++ b/src/Core/Billing/Services/Implementations/OrganizationBillingService.cs @@ -31,6 +31,7 @@ public class OrganizationBillingService( IGlobalSettings globalSettings, ILogger logger, IOrganizationRepository organizationRepository, + IOrganizationUserRepository organizationUserRepository, IPricingClient pricingClient, ISetupIntentCache setupIntentCache, IStripeAdapter stripeAdapter, @@ -107,6 +108,8 @@ public class OrganizationBillingService( ? await stripeAdapter.InvoiceGetAsync(subscription.LatestInvoiceId, new InvoiceGetOptions()) : null; + var orgOccupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id); + return new OrganizationMetadata( isEligibleForSelfHost, isManaged, @@ -117,7 +120,8 @@ public class OrganizationBillingService( subscription.Status == StripeConstants.SubscriptionStatus.Canceled, invoice?.DueDate, invoice?.Created, - subscription.CurrentPeriodEnd); + subscription.CurrentPeriodEnd, + orgOccupiedSeats); } public async Task diff --git a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs index a8c3cf15a9..aff51b0d1d 100644 --- a/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs +++ b/test/Api.Test/Billing/Controllers/OrganizationBillingControllerTests.cs @@ -52,7 +52,7 @@ public class OrganizationBillingControllerTests { sutProvider.GetDependency().OrganizationUser(organizationId).Returns(true); sutProvider.GetDependency().GetMetadata(organizationId) - .Returns(new OrganizationMetadata(true, true, true, true, true, true, true, null, null, null)); + .Returns(new OrganizationMetadata(true, true, true, true, true, true, true, null, null, null, 0)); var result = await sutProvider.Sut.GetMetadataAsync(organizationId); From 3d02d6c4eb8dadc60a88d519fbe2afb0e34a3383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Ch=C4=99ci=C5=84ski?= Date: Thu, 29 May 2025 16:07:46 +0200 Subject: [PATCH 113/114] [BRE-826] Update remaining DockerHub references to GitHub (#5877) * Update README.md to replace Docker Hub links with GitHub Packages links * Update README.md Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com> * Remove docker badge --------- Co-authored-by: Vince Grassia <593223+vgrassia@users.noreply.github.com> --- README.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 73992785d7..c817931c67 100644 --- a/README.md +++ b/README.md @@ -5,9 +5,6 @@ Github Workflow build on main - - DockerHub - gitter chat @@ -26,12 +23,12 @@ Please refer to the [Server Setup Guide](https://contributing.bitwarden.com/gett ## Deploy

    - + docker

    -You can deploy Bitwarden using Docker containers on Windows, macOS, and Linux distributions. Use the provided PowerShell and Bash scripts to get started quickly. Find all of the Bitwarden images on [Docker Hub](https://hub.docker.com/u/bitwarden/). +You can deploy Bitwarden using Docker containers on Windows, macOS, and Linux distributions. Use the provided PowerShell and Bash scripts to get started quickly. Find all of the Bitwarden images on [GitHub Container Registry](https://github.com/orgs/bitwarden/packages). Full documentation for deploying Bitwarden with Docker can be found in our help center at: https://help.bitwarden.com/article/install-on-premise/ From d50ad97e6eeb733af9c069a949939b0567ba936d Mon Sep 17 00:00:00 2001 From: Jared McCannon Date: Thu, 29 May 2025 10:57:50 -0400 Subject: [PATCH 114/114] Adding OptimizeNestedTraverse feature flag. moved custome role permissions to ac section. (#5853) --- src/Core/Constants.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs index 7a2b3c9ac7..78a8364695 100644 --- a/src/Core/Constants.cs +++ b/src/Core/Constants.cs @@ -109,6 +109,8 @@ public static class FeatureFlagKeys public const string SsoExternalIdVisibility = "pm-18630-sso-external-id-visibility"; public const string ScimInviteUserOptimization = "pm-16811-optimize-invite-user-flow-to-fail-fast"; public const string EventBasedOrganizationIntegrations = "event-based-organization-integrations"; + public const string OptimizeNestedTraverseTypescript = "pm-21695-optimize-nested-traverse-typescript"; + public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions"; /* Auth Team */ public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence"; @@ -200,7 +202,6 @@ public static class FeatureFlagKeys public const string DesktopCipherForms = "pm-18520-desktop-cipher-forms"; public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk"; public const string EndUserNotifications = "pm-10609-end-user-notifications"; - public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions"; public const string PhishingDetection = "phishing-detection"; public const string RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy";