From a447c740d590120b366665df83420f5d8a44c675 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Tue, 22 Sep 2026 14:58:44 -0500 Subject: [PATCH 01/27] [PM-40673 Part 1] add TwoFactorRememberToken.cs and ITwoFactorRememberTokenRepository.cs --- .../Auth/Entities/TwoFactorRememberToken.cs | 62 +++++++++++++++++++ .../ITwoFactorRememberTokenRepository.cs | 42 +++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 src/Core/Auth/Entities/TwoFactorRememberToken.cs create mode 100644 src/Core/Auth/Repositories/ITwoFactorRememberTokenRepository.cs diff --git a/src/Core/Auth/Entities/TwoFactorRememberToken.cs b/src/Core/Auth/Entities/TwoFactorRememberToken.cs new file mode 100644 index 000000000000..c611cbbbe662 --- /dev/null +++ b/src/Core/Auth/Entities/TwoFactorRememberToken.cs @@ -0,0 +1,62 @@ +using System.ComponentModel.DataAnnotations; +using Bit.Core.Entities; +using Bit.Core.Utilities; + +namespace Bit.Core.Auth.Entities; + +/// +/// Server-side state backing a single remembered device's two-factor "remember me" token. +/// One row per (, ). +/// +/// +/// A token issued to a device carries a copy of that device's , and is honored only +/// while the two match. Writing a new therefore invalidates every token previously +/// issued for the device, leaving the user's other devices and their access and refresh tokens untouched. +/// +public class TwoFactorRememberToken : ITableObject +{ + public Guid Id { get; set; } + + /// + /// The owning user. Derivable through , but stored directly so that + /// revoking every remembered device for one user is an index seek. + /// + public Guid UserId { get; set; } + + /// + /// The remembered device. Note this is Device.Id, not the client-supplied device identifier. + /// + public Guid DeviceId { get; set; } + + /// + /// The value a presented token's stamp is compared against. Replaced to revoke. + /// + [MaxLength(50)] + public string Stamp { get; set; } = null!; + + /// + /// When this device was first remembered. Preserved when the row is re-issued. + /// + public DateTime CreationDate { get; set; } = DateTime.UtcNow; + + /// + /// When was last written, whether by re-issuance or by a revocation. + /// + public DateTime RevisionDate { get; set; } = DateTime.UtcNow; + + /// + /// When this row stops being honored, regardless of . + /// + public DateTime ExpirationDate { get; set; } + + /// + /// Initializes to a new COMB GUID. + /// + public void SetNewId() + { + if (Id == default) + { + Id = CoreHelpers.GenerateComb(); + } + } +} diff --git a/src/Core/Auth/Repositories/ITwoFactorRememberTokenRepository.cs b/src/Core/Auth/Repositories/ITwoFactorRememberTokenRepository.cs new file mode 100644 index 000000000000..ebf93c7a6df0 --- /dev/null +++ b/src/Core/Auth/Repositories/ITwoFactorRememberTokenRepository.cs @@ -0,0 +1,42 @@ +using Bit.Core.Auth.Entities; + +namespace Bit.Core.Auth.Repositories; + +/// +/// Point lookups and scoped writes only, deliberately: no ad-hoc queries, so that every operation here +/// is expressible against a partitioned document store if this ever needs a second implementation. +/// +public interface ITwoFactorRememberTokenRepository +{ + /// + /// Reads the row for one remembered device, or null if the device has never been remembered. + /// + Task GetByUserIdDeviceIdAsync(Guid userId, Guid deviceId); + + /// + /// Creates the row for this user and device, or updates the existing one in place, + /// preserving its CreationDate. The caller supplies a fresh Stamp, so any + /// token previously issued for this device stops validating. + /// + Task UpsertAsync(TwoFactorRememberToken token); + + /// + /// Rotates the Stamp on every row for this user, invalidating all of their remember + /// tokens at once. Rows are kept. + /// + /// + /// The new stamp is generated here rather than by the caller: nothing above this layer needs to + /// know the value, and generating it in one place keeps every provider writing the same thing. + /// + Task RotateStampsByUserIdAsync(Guid userId); + + /// + /// Deletes every row that expired before . Hygiene only — an expired row is + /// already refused at validation time. + /// + /// + /// The instant to compare against, supplied by the caller so that every provider and the stored + /// procedure agree on one value. + /// + Task DeleteExpiredAsync(DateTime now); +} From 272ccb5ce7a56e5d063245250e9dbd1efa2d1b8c Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:55:33 -0500 Subject: [PATCH 02/27] [PM-40673 Part 1] add TwoFactorRememberToken.sql, migrator script, stored procedures, Dapper infrastructure --- .../TwoFactorRememberTokenRepository.cs | 89 +++++ .../DapperServiceCollectionExtensions.cs | 1 + .../TwoFactorRememberToken_DeleteExpired.sql | 22 ++ ...ctorRememberToken_ReadByUserIdDeviceId.sql | 15 + .../TwoFactorRememberToken_Save.sql | 57 ++++ ...RememberToken_UpdateManyStampsByUserId.sql | 24 ++ .../Auth/Tables/TwoFactorRememberToken.sql | 26 ++ .../Auth/Views/TwoFactorRememberTokenView.sql | 6 + .../TwoFactorRememberTokenRepositoryTests.cs | 316 ++++++++++++++++++ ...-09-22_00_CreateTwoFactorRememberToken.sql | 163 +++++++++ 10 files changed, 719 insertions(+) create mode 100644 src/Infrastructure.Dapper/Auth/Repositories/TwoFactorRememberTokenRepository.cs create mode 100644 src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_DeleteExpired.sql create mode 100644 src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_ReadByUserIdDeviceId.sql create mode 100644 src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_Save.sql create mode 100644 src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_UpdateManyStampsByUserId.sql create mode 100644 src/Sql/dbo/Auth/Tables/TwoFactorRememberToken.sql create mode 100644 src/Sql/dbo/Auth/Views/TwoFactorRememberTokenView.sql create mode 100644 test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs create mode 100644 util/Migrator/DbScripts/2026-09-22_00_CreateTwoFactorRememberToken.sql diff --git a/src/Infrastructure.Dapper/Auth/Repositories/TwoFactorRememberTokenRepository.cs b/src/Infrastructure.Dapper/Auth/Repositories/TwoFactorRememberTokenRepository.cs new file mode 100644 index 000000000000..0f9031bbdbac --- /dev/null +++ b/src/Infrastructure.Dapper/Auth/Repositories/TwoFactorRememberTokenRepository.cs @@ -0,0 +1,89 @@ +using System.Data; +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Repositories; +using Bit.Core.Settings; +using Bit.Infrastructure.Dapper.Repositories; +using Dapper; +using Microsoft.Data.SqlClient; + +namespace Bit.Infrastructure.Dapper.Auth.Repositories; + +/// +/// Derives from rather than the generic repository because every +/// operation is a named procedure; none of them map onto generic CRUD. +/// +public class TwoFactorRememberTokenRepository : BaseRepository, ITwoFactorRememberTokenRepository +{ + public TwoFactorRememberTokenRepository(GlobalSettings globalSettings) + : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + { } + + public TwoFactorRememberTokenRepository(string connectionString, string readOnlyConnectionString) + : base(connectionString, readOnlyConnectionString) + { } + + public async Task GetByUserIdDeviceIdAsync(Guid userId, Guid deviceId) + { + await using var connection = new SqlConnection(ConnectionString); + + var results = await connection.QueryAsync( + "[dbo].[TwoFactorRememberToken_ReadByUserIdDeviceId]", + new { UserId = userId, DeviceId = deviceId }, + commandType: CommandType.StoredProcedure); + + return results.SingleOrDefault(); + } + + public async Task UpsertAsync(TwoFactorRememberToken token) + { + // BaseRepository does not assign ids the way the generic repository does, and the procedure + // needs one for the insert branch. SetNewId only fills an unset id, so an update is unaffected. + token.SetNewId(); + + await using var connection = new SqlConnection(ConnectionString); + + await connection.ExecuteAsync( + "[dbo].[TwoFactorRememberToken_Save]", + new + { + token.Id, + token.UserId, + token.DeviceId, + token.Stamp, + token.CreationDate, + token.RevisionDate, + token.ExpirationDate, + }, + commandType: CommandType.StoredProcedure); + + return token; + } + + public async Task RotateStampsByUserIdAsync(Guid userId) + { + await using var connection = new SqlConnection(ConnectionString); + + await connection.ExecuteAsync( + "[dbo].[TwoFactorRememberToken_UpdateManyStampsByUserId]", + new + { + UserId = userId, + // Generated here rather than with NEWID() so that this and the Entity Framework + // implementations write the same shape of value. One stamp covers every row for the + // user; see the procedure for why that is safe. + Stamp = Guid.NewGuid().ToString(), + RevisionDate = DateTime.UtcNow, + }, + commandType: CommandType.StoredProcedure); + } + + public async Task DeleteExpiredAsync(DateTime now) + { + await using var connection = new SqlConnection(ConnectionString); + + await connection.ExecuteAsync( + "[dbo].[TwoFactorRememberToken_DeleteExpired]", + new { Now = now }, + commandType: CommandType.StoredProcedure); + } +} diff --git a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs index 8b4061620bd2..1670e394b67a 100644 --- a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs +++ b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs @@ -72,6 +72,7 @@ public static void AddDapperRepositories(this IServiceCollection services, bool services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_DeleteExpired.sql b/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_DeleteExpired.sql new file mode 100644 index 000000000000..6437ec29291f --- /dev/null +++ b/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_DeleteExpired.sql @@ -0,0 +1,22 @@ +CREATE PROCEDURE [dbo].[TwoFactorRememberToken_DeleteExpired] + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- Deleted in batches so each statement holds few enough locks to avoid escalating to a table + -- lock, which would block logins while the sweep runs. The loop ends when a batch deletes + -- nothing. + DECLARE @BatchSize INT = 100 + + WHILE @BatchSize > 0 + BEGIN + DELETE TOP(@BatchSize) + FROM + [dbo].[TwoFactorRememberToken] + WHERE + [ExpirationDate] < @Now + + SET @BatchSize = @@ROWCOUNT + END +END diff --git a/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_ReadByUserIdDeviceId.sql b/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_ReadByUserIdDeviceId.sql new file mode 100644 index 000000000000..f3b714f38cfa --- /dev/null +++ b/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_ReadByUserIdDeviceId.sql @@ -0,0 +1,15 @@ +CREATE PROCEDURE [dbo].[TwoFactorRememberToken_ReadByUserIdDeviceId] + @UserId UNIQUEIDENTIFIER, + @DeviceId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[TwoFactorRememberTokenView] + WHERE + [UserId] = @UserId + AND [DeviceId] = @DeviceId +END diff --git a/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_Save.sql b/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_Save.sql new file mode 100644 index 000000000000..ae28d19ad847 --- /dev/null +++ b/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_Save.sql @@ -0,0 +1,57 @@ +CREATE PROCEDURE [dbo].[TwoFactorRememberToken_Save] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @DeviceId UNIQUEIDENTIFIER, + @Stamp NVARCHAR(50), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @ExpirationDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + -- An explicit transaction is required so the range lock below is still held when the INSERT + -- runs; XACT_ABORT leaves the pooled connection clean if the unique-index backstop trips. + SET XACT_ABORT ON + + BEGIN TRANSACTION + + -- One row per (UserId, DeviceId). The UPDLOCK, HOLDLOCK range lock serializes a concurrent + -- remember-login on the same device: it blocks here until this transaction commits, then + -- finds the row and takes the UPDATE branch instead of inserting a duplicate. The lock is + -- cheap because [IX_TwoFactorRememberToken_UserId_DeviceId] makes it a key-range lock + -- rather than a scan. CreationDate is deliberately not updated. + UPDATE [dbo].[TwoFactorRememberToken] WITH (UPDLOCK, HOLDLOCK) + SET + [Stamp] = @Stamp, + [RevisionDate] = @RevisionDate, + [ExpirationDate] = @ExpirationDate + WHERE + [UserId] = @UserId + AND [DeviceId] = @DeviceId + + IF @@ROWCOUNT = 0 + BEGIN + INSERT INTO [dbo].[TwoFactorRememberToken] + ( + [Id], + [UserId], + [DeviceId], + [Stamp], + [CreationDate], + [RevisionDate], + [ExpirationDate] + ) + VALUES + ( + @Id, + @UserId, + @DeviceId, + @Stamp, + @CreationDate, + @RevisionDate, + @ExpirationDate + ) + END + + COMMIT TRANSACTION +END diff --git a/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_UpdateManyStampsByUserId.sql b/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_UpdateManyStampsByUserId.sql new file mode 100644 index 000000000000..2708880469b3 --- /dev/null +++ b/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_UpdateManyStampsByUserId.sql @@ -0,0 +1,24 @@ +CREATE PROCEDURE [dbo].[TwoFactorRememberToken_UpdateManyStampsByUserId] + @UserId UNIQUEIDENTIFIER, + @Stamp NVARCHAR(50), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- Revokes every remember token this user holds by replacing the value each of their tokens is + -- compared against. The rows are kept, so CreationDate still records when each device was first + -- remembered and RevisionDate now records when it was cut off. + -- + -- One stamp is shared across the user's rows, and the caller supplies it. Rows are located by + -- (UserId, DeviceId), so the stamp never selects a row and a token naming one device can never + -- match another's. Generating it in application code also keeps this procedure and the Entity + -- Framework implementations writing identical values. + UPDATE + [dbo].[TwoFactorRememberToken] + SET + [Stamp] = @Stamp, + [RevisionDate] = @RevisionDate + WHERE + [UserId] = @UserId +END diff --git a/src/Sql/dbo/Auth/Tables/TwoFactorRememberToken.sql b/src/Sql/dbo/Auth/Tables/TwoFactorRememberToken.sql new file mode 100644 index 000000000000..74044176041c --- /dev/null +++ b/src/Sql/dbo/Auth/Tables/TwoFactorRememberToken.sql @@ -0,0 +1,26 @@ +CREATE TABLE [dbo].[TwoFactorRememberToken] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NOT NULL, + [DeviceId] UNIQUEIDENTIFIER NOT NULL, + [Stamp] NVARCHAR(50) NOT NULL, + [CreationDate] DATETIME2(7) NOT NULL, + [RevisionDate] DATETIME2(7) NOT NULL, + [ExpirationDate] DATETIME2(7) NOT NULL, + CONSTRAINT [PK_TwoFactorRememberToken] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_TwoFactorRememberToken_User] FOREIGN KEY ([UserId]) + REFERENCES [dbo].[User] ([Id]), + CONSTRAINT [FK_TwoFactorRememberToken_Device] FOREIGN KEY ([DeviceId]) + REFERENCES [dbo].[Device] ([Id]) ON DELETE CASCADE +); + +GO +CREATE UNIQUE NONCLUSTERED INDEX [IX_TwoFactorRememberToken_UserId_DeviceId] + ON [dbo].[TwoFactorRememberToken]([UserId] ASC, [DeviceId] ASC); + +GO +CREATE NONCLUSTERED INDEX [IX_TwoFactorRememberToken_DeviceId] + ON [dbo].[TwoFactorRememberToken]([DeviceId] ASC); + +GO +CREATE NONCLUSTERED INDEX [IX_TwoFactorRememberToken_ExpirationDate] + ON [dbo].[TwoFactorRememberToken]([ExpirationDate] ASC); diff --git a/src/Sql/dbo/Auth/Views/TwoFactorRememberTokenView.sql b/src/Sql/dbo/Auth/Views/TwoFactorRememberTokenView.sql new file mode 100644 index 000000000000..d0f04a1e905a --- /dev/null +++ b/src/Sql/dbo/Auth/Views/TwoFactorRememberTokenView.sql @@ -0,0 +1,6 @@ +CREATE VIEW [dbo].[TwoFactorRememberTokenView] +AS +SELECT + * +FROM + [dbo].[TwoFactorRememberToken] diff --git a/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs new file mode 100644 index 000000000000..d4a3f838ea9b --- /dev/null +++ b/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs @@ -0,0 +1,316 @@ +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Repositories; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories; +using Bit.Infrastructure.IntegrationTest.AdminConsole; +using Microsoft.Data.SqlClient; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Bit.Infrastructure.IntegrationTest.Auth.Repositories; + +public class TwoFactorRememberTokenRepositoryTests +{ + // ------------------------------------------------------------------------------------------- + // Test helpers + // ------------------------------------------------------------------------------------------- + + private static Task CreateTestDeviceAsync(IDeviceRepository deviceRepository, Guid userId) => + deviceRepository.CreateAsync(new Device + { + UserId = userId, + Name = "chrome-test", + Type = DeviceType.ChromeBrowser, + Identifier = Guid.NewGuid().ToString(), + Active = true, + LastActivityDate = DateTime.UtcNow, + }); + + private static TwoFactorRememberToken NewToken( + Guid userId, + Guid deviceId, + string stamp, + DateTime? expirationDate = null) => + new() + { + UserId = userId, + DeviceId = deviceId, + Stamp = stamp, + ExpirationDate = expirationDate ?? DateTime.UtcNow.AddDays(30), + }; + + /// + /// Deletes a device row at the database level, bypassing repository code entirely. + /// + /// + /// There is no Device_DeleteById stored procedure, so the Dapper DeviceRepository + /// cannot delete. More importantly, the claim under test is about a database-level cascade, so + /// the delete has to reach the database without EF resolving the cascade in memory first — + /// hence ExecuteDeleteAsync rather than Remove plus SaveChanges. + /// + private static async Task DeleteDeviceAtDatabaseLevelAsync( + IServiceProvider services, Database database, Guid deviceId) + { + if (database.Type == SupportedDatabaseProviders.SqlServer && !database.UseEf) + { + await using var connection = new SqlConnection(database.ConnectionString); + await connection.OpenAsync(); + await using var command = connection.CreateCommand(); + command.CommandText = "DELETE FROM [dbo].[Device] WHERE [Id] = @Id"; + command.Parameters.Add(new SqlParameter("@Id", deviceId)); + await command.ExecuteNonQueryAsync(); + return; + } + + using var scope = services.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + await dbContext.Devices.Where(d => d.Id == deviceId).ExecuteDeleteAsync(); + } + + // ------------------------------------------------------------------------------------------- + // R1 — upsert semantics + // ------------------------------------------------------------------------------------------- + + [Theory, DatabaseData] + public async Task UpsertAsync_SameUserAndDevice_UpdatesInPlaceAndPreservesCreationDate( + ITwoFactorRememberTokenRepository sut, + IUserRepository userRepository, + IDeviceRepository deviceRepository) + { + var user = await userRepository.CreateTestUserAsync(); + var device = await CreateTestDeviceAsync(deviceRepository, user.Id); + + await sut.UpsertAsync(NewToken(user.Id, device.Id, "stamp-one")); + var inserted = await sut.GetByUserIdDeviceIdAsync(user.Id, device.Id); + + Assert.NotNull(inserted); + Assert.NotEqual(Guid.Empty, inserted.Id); + Assert.Equal("stamp-one", inserted.Stamp); + + await sut.UpsertAsync(NewToken(user.Id, device.Id, "stamp-two")); + var updated = await sut.GetByUserIdDeviceIdAsync(user.Id, device.Id); + + Assert.NotNull(updated); + Assert.Equal("stamp-two", updated.Stamp); + // Same row rather than a second one, and the row remembers when the device was first trusted. + Assert.Equal(inserted.Id, updated.Id); + Assert.Equal(inserted.CreationDate, updated.CreationDate); + Assert.True(updated.RevisionDate >= inserted.RevisionDate); + } + + // ------------------------------------------------------------------------------------------- + // R2 — concurrent upsert + // ------------------------------------------------------------------------------------------- + + /// + /// Two logins requesting "remember me" for the same device at the same instant must not produce a + /// unique-key violation, which would surface to the user as a failed login. + /// + /// + /// On SQL Server this exercises the UPDLOCK, HOLDLOCK range lock inside an explicit + /// transaction. That hint is MSSQL-only, so on the EF providers this is a regression guard on the + /// common path rather than proof the lock works. + /// + [Theory, DatabaseData] + public async Task UpsertAsync_Concurrent_BothCompleteAndOneRowSurvives( + ITwoFactorRememberTokenRepository sut, + IUserRepository userRepository, + IDeviceRepository deviceRepository) + { + var user = await userRepository.CreateTestUserAsync(); + var device = await CreateTestDeviceAsync(deviceRepository, user.Id); + + await Task.WhenAll( + sut.UpsertAsync(NewToken(user.Id, device.Id, "stamp-a")), + sut.UpsertAsync(NewToken(user.Id, device.Id, "stamp-b"))); + + var row = await sut.GetByUserIdDeviceIdAsync(user.Id, device.Id); + + Assert.NotNull(row); + Assert.Contains(row.Stamp, new[] { "stamp-a", "stamp-b" }); + } + + // ------------------------------------------------------------------------------------------- + // R3, R4 — rotation + // ------------------------------------------------------------------------------------------- + + [Theory, DatabaseData] + public async Task RotateStampsByUserIdAsync_RotatesEveryRowForThatUser( + ITwoFactorRememberTokenRepository sut, + IUserRepository userRepository, + IDeviceRepository deviceRepository) + { + var user = await userRepository.CreateTestUserAsync(); + var deviceOne = await CreateTestDeviceAsync(deviceRepository, user.Id); + var deviceTwo = await CreateTestDeviceAsync(deviceRepository, user.Id); + + await sut.UpsertAsync(NewToken(user.Id, deviceOne.Id, "stamp-one")); + await sut.UpsertAsync(NewToken(user.Id, deviceTwo.Id, "stamp-two")); + + var beforeOne = await sut.GetByUserIdDeviceIdAsync(user.Id, deviceOne.Id); + var beforeTwo = await sut.GetByUserIdDeviceIdAsync(user.Id, deviceTwo.Id); + Assert.NotNull(beforeOne); + Assert.NotNull(beforeTwo); + + await sut.RotateStampsByUserIdAsync(user.Id); + + var afterOne = await sut.GetByUserIdDeviceIdAsync(user.Id, deviceOne.Id); + var afterTwo = await sut.GetByUserIdDeviceIdAsync(user.Id, deviceTwo.Id); + + Assert.NotNull(afterOne); + Assert.NotNull(afterTwo); + + // Every device the user remembered is cut off, and the rows are kept rather than deleted. + Assert.NotEqual(beforeOne.Stamp, afterOne.Stamp); + Assert.NotEqual(beforeTwo.Stamp, afterTwo.Stamp); + Assert.Equal(beforeOne.Id, afterOne.Id); + Assert.Equal(beforeOne.CreationDate, afterOne.CreationDate); + Assert.True(afterOne.RevisionDate >= beforeOne.RevisionDate); + + // Stamps are generated in C#, never by NEWID(), which would emit upper-case hex. + Assert.Equal(afterOne.Stamp.ToLowerInvariant(), afterOne.Stamp); + Assert.Equal(afterTwo.Stamp.ToLowerInvariant(), afterTwo.Stamp); + } + + [Theory, DatabaseData] + public async Task RotateStampsByUserIdAsync_LeavesOtherUsersAlone( + ITwoFactorRememberTokenRepository sut, + IUserRepository userRepository, + IDeviceRepository deviceRepository) + { + var revokedUser = await userRepository.CreateTestUserAsync("revoked"); + var untouchedUser = await userRepository.CreateTestUserAsync("untouched"); + var revokedDevice = await CreateTestDeviceAsync(deviceRepository, revokedUser.Id); + var untouchedDevice = await CreateTestDeviceAsync(deviceRepository, untouchedUser.Id); + + await sut.UpsertAsync(NewToken(revokedUser.Id, revokedDevice.Id, "stamp-revoked")); + await sut.UpsertAsync(NewToken(untouchedUser.Id, untouchedDevice.Id, "stamp-untouched")); + + await sut.RotateStampsByUserIdAsync(revokedUser.Id); + + var untouched = await sut.GetByUserIdDeviceIdAsync(untouchedUser.Id, untouchedDevice.Id); + + Assert.NotNull(untouched); + Assert.Equal("stamp-untouched", untouched.Stamp); + } + + // ------------------------------------------------------------------------------------------- + // R5, R6 — cascade behavior + // ------------------------------------------------------------------------------------------- + + [Theory, DatabaseData] + public async Task DeletingDevice_CascadesToTokenRow( + ITwoFactorRememberTokenRepository sut, + IUserRepository userRepository, + IDeviceRepository deviceRepository, + Database database, + IServiceProvider services) + { + var user = await userRepository.CreateTestUserAsync(); + var device = await CreateTestDeviceAsync(deviceRepository, user.Id); + await sut.UpsertAsync(NewToken(user.Id, device.Id, "stamp")); + + Assert.NotNull(await sut.GetByUserIdDeviceIdAsync(user.Id, device.Id)); + + await DeleteDeviceAtDatabaseLevelAsync(services, database, device.Id); + + Assert.Null(await sut.GetByUserIdDeviceIdAsync(user.Id, device.Id)); + } + + /// + /// Nothing deletes these rows by UserId. They survive a user deletion only because both + /// user-delete paths delete the user's Device rows first and the cascade follows. + /// + [Theory, DatabaseData] + public async Task DeleteAsync_User_RemovesTokenRows( + ITwoFactorRememberTokenRepository sut, + IUserRepository userRepository, + IDeviceRepository deviceRepository) + { + var user = await userRepository.CreateTestUserAsync(); + var device = await CreateTestDeviceAsync(deviceRepository, user.Id); + await sut.UpsertAsync(NewToken(user.Id, device.Id, "stamp")); + + await userRepository.DeleteAsync(user); + + Assert.Null(await sut.GetByUserIdDeviceIdAsync(user.Id, device.Id)); + } + + /// + /// The bulk path takes different code than on EF — + /// ExecuteDeleteAsync bypasses change tracking, so it depends on the database-level cascade + /// actually existing rather than on EF resolving it. + /// + [Theory, DatabaseData] + public async Task DeleteManyAsync_Users_RemovesTokenRows( + ITwoFactorRememberTokenRepository sut, + IUserRepository userRepository, + IDeviceRepository deviceRepository) + { + var user = await userRepository.CreateTestUserAsync(); + var device = await CreateTestDeviceAsync(deviceRepository, user.Id); + await sut.UpsertAsync(NewToken(user.Id, device.Id, "stamp")); + + await userRepository.DeleteManyAsync(new[] { user }); + + Assert.Null(await sut.GetByUserIdDeviceIdAsync(user.Id, device.Id)); + } + + // ------------------------------------------------------------------------------------------- + // R7 — expiry sweep + // ------------------------------------------------------------------------------------------- + + [Theory, DatabaseData] + public async Task DeleteExpiredAsync_RemovesOnlyExpiredRows( + ITwoFactorRememberTokenRepository sut, + IUserRepository userRepository, + IDeviceRepository deviceRepository) + { + var user = await userRepository.CreateTestUserAsync(); + var expiredDevice = await CreateTestDeviceAsync(deviceRepository, user.Id); + var liveDevice = await CreateTestDeviceAsync(deviceRepository, user.Id); + + // A fixed instant the two rows straddle, so the sweep is deterministic rather than a race + // against the wall clock. + var now = DateTime.UtcNow; + + await sut.UpsertAsync(NewToken(user.Id, expiredDevice.Id, "expired", now.AddMinutes(-1))); + await sut.UpsertAsync(NewToken(user.Id, liveDevice.Id, "live", now.AddMinutes(1))); + + await sut.DeleteExpiredAsync(now); + + Assert.Null(await sut.GetByUserIdDeviceIdAsync(user.Id, expiredDevice.Id)); + Assert.NotNull(await sut.GetByUserIdDeviceIdAsync(user.Id, liveDevice.Id)); + } + + // ------------------------------------------------------------------------------------------- + // R8 — storage fidelity + // ------------------------------------------------------------------------------------------- + + /// + /// Cheap guard against a column width or collation mistake on any one provider: the stamp is + /// compared with ordinal case sensitivity at validation time, so it has to come back byte-identical. + /// + [Theory, DatabaseData] + public async Task UpsertAsync_StampRoundTripsUnchanged( + ITwoFactorRememberTokenRepository sut, + IUserRepository userRepository, + IDeviceRepository deviceRepository) + { + var user = await userRepository.CreateTestUserAsync(); + var device = await CreateTestDeviceAsync(deviceRepository, user.Id); + var stamp = Guid.NewGuid().ToString(); + + await sut.UpsertAsync(NewToken(user.Id, device.Id, stamp)); + + var row = await sut.GetByUserIdDeviceIdAsync(user.Id, device.Id); + + Assert.NotNull(row); + Assert.Equal(stamp, row.Stamp, StringComparer.Ordinal); + Assert.Equal(user.Id, row.UserId); + Assert.Equal(device.Id, row.DeviceId); + } +} diff --git a/util/Migrator/DbScripts/2026-09-22_00_CreateTwoFactorRememberToken.sql b/util/Migrator/DbScripts/2026-09-22_00_CreateTwoFactorRememberToken.sql new file mode 100644 index 000000000000..47c10e9e928b --- /dev/null +++ b/util/Migrator/DbScripts/2026-09-22_00_CreateTwoFactorRememberToken.sql @@ -0,0 +1,163 @@ +-- Table +IF OBJECT_ID('[dbo].[TwoFactorRememberToken]') IS NULL +BEGIN + CREATE TABLE [dbo].[TwoFactorRememberToken] + ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [UserId] UNIQUEIDENTIFIER NOT NULL, + [DeviceId] UNIQUEIDENTIFIER NOT NULL, + [Stamp] NVARCHAR(50) NOT NULL, + [CreationDate] DATETIME2(7) NOT NULL, + [RevisionDate] DATETIME2(7) NOT NULL, + [ExpirationDate] DATETIME2(7) NOT NULL, + CONSTRAINT [PK_TwoFactorRememberToken] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_TwoFactorRememberToken_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]), + CONSTRAINT [FK_TwoFactorRememberToken_Device] FOREIGN KEY ([DeviceId]) REFERENCES [dbo].[Device] ([Id]) ON DELETE CASCADE + ); + + CREATE UNIQUE NONCLUSTERED INDEX [IX_TwoFactorRememberToken_UserId_DeviceId] + ON [dbo].[TwoFactorRememberToken]([UserId] ASC, [DeviceId] ASC); + + CREATE NONCLUSTERED INDEX [IX_TwoFactorRememberToken_DeviceId] + ON [dbo].[TwoFactorRememberToken]([DeviceId] ASC); + + CREATE NONCLUSTERED INDEX [IX_TwoFactorRememberToken_ExpirationDate] + ON [dbo].[TwoFactorRememberToken]([ExpirationDate] ASC); +END +GO + +-- View +CREATE OR ALTER VIEW [dbo].[TwoFactorRememberTokenView] +AS +SELECT + * +FROM + [dbo].[TwoFactorRememberToken] +GO + +-- Stored Procedures +CREATE OR ALTER PROCEDURE [dbo].[TwoFactorRememberToken_Save] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @DeviceId UNIQUEIDENTIFIER, + @Stamp NVARCHAR(50), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @ExpirationDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + -- An explicit transaction is required so the range lock below is still held when the INSERT + -- runs; XACT_ABORT leaves the pooled connection clean if the unique-index backstop trips. + SET XACT_ABORT ON + + BEGIN TRANSACTION + + -- One row per (UserId, DeviceId). The UPDLOCK, HOLDLOCK range lock serializes a concurrent + -- remember-login on the same device: it blocks here until this transaction commits, then + -- finds the row and takes the UPDATE branch instead of inserting a duplicate. The lock is + -- cheap because [IX_TwoFactorRememberToken_UserId_DeviceId] makes it a key-range lock + -- rather than a scan. CreationDate is deliberately not updated. + UPDATE [dbo].[TwoFactorRememberToken] WITH (UPDLOCK, HOLDLOCK) + SET + [Stamp] = @Stamp, + [RevisionDate] = @RevisionDate, + [ExpirationDate] = @ExpirationDate + WHERE + [UserId] = @UserId + AND [DeviceId] = @DeviceId + + IF @@ROWCOUNT = 0 + BEGIN + INSERT INTO [dbo].[TwoFactorRememberToken] + ( + [Id], + [UserId], + [DeviceId], + [Stamp], + [CreationDate], + [RevisionDate], + [ExpirationDate] + ) + VALUES + ( + @Id, + @UserId, + @DeviceId, + @Stamp, + @CreationDate, + @RevisionDate, + @ExpirationDate + ) + END + + COMMIT TRANSACTION +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[TwoFactorRememberToken_ReadByUserIdDeviceId] + @UserId UNIQUEIDENTIFIER, + @DeviceId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[TwoFactorRememberTokenView] + WHERE + [UserId] = @UserId + AND [DeviceId] = @DeviceId +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[TwoFactorRememberToken_UpdateManyStampsByUserId] + @UserId UNIQUEIDENTIFIER, + @Stamp NVARCHAR(50), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- Revokes every remember token this user holds by replacing the value each of their tokens is + -- compared against. The rows are kept, so CreationDate still records when each device was first + -- remembered and RevisionDate now records when it was cut off. + -- + -- One stamp is shared across the user's rows, and the caller supplies it. Rows are located by + -- (UserId, DeviceId), so the stamp never selects a row and a token naming one device can never + -- match another's. Generating it in application code also keeps this procedure and the Entity + -- Framework implementations writing identical values. + UPDATE + [dbo].[TwoFactorRememberToken] + SET + [Stamp] = @Stamp, + [RevisionDate] = @RevisionDate + WHERE + [UserId] = @UserId +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[TwoFactorRememberToken_DeleteExpired] + @Now DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- Deleted in batches so each statement holds few enough locks to avoid escalating to a table + -- lock, which would block logins while the sweep runs. The loop ends when a batch deletes + -- nothing. + DECLARE @BatchSize INT = 100 + + WHILE @BatchSize > 0 + BEGIN + DELETE TOP(@BatchSize) + FROM + [dbo].[TwoFactorRememberToken] + WHERE + [ExpirationDate] < @Now + + SET @BatchSize = @@ROWCOUNT + END +END +GO From 18a82a306a3d98c94d3a277a09c170df81e7087e Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:09:51 -0500 Subject: [PATCH 03/27] [PM-40673 Part 1] add Entity Framework infrastructure --- ...torRememberTokenEntityTypeConfiguration.cs | 48 + .../Auth/Models/TwoFactorRememberToken.cs | 18 + .../TwoFactorRememberTokenRepository.cs | 83 + ...ityFrameworkServiceCollectionExtensions.cs | 1 + .../Repositories/DatabaseContext.cs | 1 + .../TwoFactorRememberTokenRepositoryTests.cs | 65 + ...1_CreateTwoFactorRememberToken.Designer.cs | 4179 ++++++++++++++++ ...0923010021_CreateTwoFactorRememberToken.cs | 68 + .../DatabaseContextModelSnapshot.cs | 59 + ...1_CreateTwoFactorRememberToken.Designer.cs | 4185 +++++++++++++++++ ...0923010041_CreateTwoFactorRememberToken.cs | 66 + .../DatabaseContextModelSnapshot.cs | 59 + ...5_CreateTwoFactorRememberToken.Designer.cs | 4168 ++++++++++++++++ ...0923010055_CreateTwoFactorRememberToken.cs | 66 + .../DatabaseContextModelSnapshot.cs | 59 + 15 files changed, 13125 insertions(+) create mode 100644 src/Infrastructure.EntityFramework/Auth/Configurations/TwoFactorRememberTokenEntityTypeConfiguration.cs create mode 100644 src/Infrastructure.EntityFramework/Auth/Models/TwoFactorRememberToken.cs create mode 100644 src/Infrastructure.EntityFramework/Auth/Repositories/TwoFactorRememberTokenRepository.cs create mode 100644 util/MySqlMigrations/Migrations/20260923010021_CreateTwoFactorRememberToken.Designer.cs create mode 100644 util/MySqlMigrations/Migrations/20260923010021_CreateTwoFactorRememberToken.cs create mode 100644 util/PostgresMigrations/Migrations/20260923010041_CreateTwoFactorRememberToken.Designer.cs create mode 100644 util/PostgresMigrations/Migrations/20260923010041_CreateTwoFactorRememberToken.cs create mode 100644 util/SqliteMigrations/Migrations/20260923010055_CreateTwoFactorRememberToken.Designer.cs create mode 100644 util/SqliteMigrations/Migrations/20260923010055_CreateTwoFactorRememberToken.cs diff --git a/src/Infrastructure.EntityFramework/Auth/Configurations/TwoFactorRememberTokenEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/Auth/Configurations/TwoFactorRememberTokenEntityTypeConfiguration.cs new file mode 100644 index 000000000000..7a42eff15df7 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Auth/Configurations/TwoFactorRememberTokenEntityTypeConfiguration.cs @@ -0,0 +1,48 @@ +using Bit.Infrastructure.EntityFramework.Auth.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace Bit.Infrastructure.EntityFramework.Auth.Configurations; + +public class TwoFactorRememberTokenEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder + .Property(e => e.Id) + .ValueGeneratedNever(); + + // Enforces one row per remembered device, serves the point lookup on validation, and — because + // UserId leads — also serves the revoke-everything-for-this-user write. + builder + .HasIndex(e => new { e.UserId, e.DeviceId }) + .IsUnique() + .IsClustered(false); + + // Foreign keys are not indexed automatically, and the cascade below needs a seek on DeviceId. + // The composite index above cannot serve it because DeviceId is not its leading column. + builder + .HasIndex(e => e.DeviceId) + .IsClustered(false); + + builder + .HasIndex(e => e.ExpirationDate) + .IsClustered(false); + + // Must match the MSSQL schema. The bulk user-delete path uses ExecuteDeleteAsync on Device, + // which bypasses change tracking, so it relies on this cascade existing in the database. + builder + .HasOne(e => e.Device) + .WithMany() + .HasForeignKey(e => e.DeviceId) + .OnDelete(DeleteBehavior.Cascade); + + builder + .HasOne(e => e.User) + .WithMany() + .HasForeignKey(e => e.UserId) + .OnDelete(DeleteBehavior.NoAction); + + builder.ToTable(nameof(TwoFactorRememberToken)); + } +} diff --git a/src/Infrastructure.EntityFramework/Auth/Models/TwoFactorRememberToken.cs b/src/Infrastructure.EntityFramework/Auth/Models/TwoFactorRememberToken.cs new file mode 100644 index 000000000000..635c20dcbe44 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Auth/Models/TwoFactorRememberToken.cs @@ -0,0 +1,18 @@ +using AutoMapper; + +namespace Bit.Infrastructure.EntityFramework.Auth.Models; + +public class TwoFactorRememberToken : Core.Auth.Entities.TwoFactorRememberToken +{ + public virtual Bit.Infrastructure.EntityFramework.Models.User User { get; set; } = null!; + public virtual Bit.Infrastructure.EntityFramework.Models.Device Device { get; set; } = null!; +} + +public class TwoFactorRememberTokenMapperProfile : Profile +{ + public TwoFactorRememberTokenMapperProfile() + { + CreateMap() + .ReverseMap(); + } +} diff --git a/src/Infrastructure.EntityFramework/Auth/Repositories/TwoFactorRememberTokenRepository.cs b/src/Infrastructure.EntityFramework/Auth/Repositories/TwoFactorRememberTokenRepository.cs new file mode 100644 index 000000000000..5ddf8bca145f --- /dev/null +++ b/src/Infrastructure.EntityFramework/Auth/Repositories/TwoFactorRememberTokenRepository.cs @@ -0,0 +1,83 @@ +using AutoMapper; +using Bit.Core.Auth.Repositories; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Infrastructure.EntityFramework.Auth.Repositories; + +public class TwoFactorRememberTokenRepository : BaseEntityFrameworkRepository, ITwoFactorRememberTokenRepository +{ + public TwoFactorRememberTokenRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) + : base(serviceScopeFactory, mapper) + { } + + public async Task GetByUserIdDeviceIdAsync(Guid userId, Guid deviceId) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + + return await dbContext.TwoFactorRememberTokens + .AsNoTracking() + .FirstOrDefaultAsync(t => t.UserId == userId && t.DeviceId == deviceId); + } + + public async Task UpsertAsync( + Core.Auth.Entities.TwoFactorRememberToken token) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + + var existing = await dbContext.TwoFactorRememberTokens + .FirstOrDefaultAsync(t => t.UserId == token.UserId && t.DeviceId == token.DeviceId); + + if (existing != null) + { + // CreationDate is deliberately left alone so the row still records when the device was + // first remembered, matching the MSSQL procedure. + existing.Stamp = token.Stamp; + existing.RevisionDate = token.RevisionDate; + existing.ExpirationDate = token.ExpirationDate; + await dbContext.SaveChangesAsync(); + + token.Id = existing.Id; + token.CreationDate = existing.CreationDate; + return token; + } + + token.SetNewId(); + var entity = Mapper.Map(token); + await dbContext.AddAsync(entity); + await dbContext.SaveChangesAsync(); + + return token; + } + + public async Task RotateStampsByUserIdAsync(Guid userId) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + + // One stamp shared across the user's rows, generated here rather than per row, so this and + // the MSSQL procedure write identical values. Rows are located by (UserId, DeviceId), so the + // stamp never selects a row and a token naming one device cannot match another's. + var stamp = Guid.NewGuid().ToString(); + var revisionDate = DateTime.UtcNow; + + await dbContext.TwoFactorRememberTokens + .Where(t => t.UserId == userId) + .ExecuteUpdateAsync(s => s + .SetProperty(t => t.Stamp, stamp) + .SetProperty(t => t.RevisionDate, revisionDate)); + } + + public async Task DeleteExpiredAsync(DateTime now) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + + await dbContext.TwoFactorRememberTokens + .Where(t => t.ExpirationDate < now) + .ExecuteDeleteAsync(); + } +} diff --git a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs index ce81db621a3a..2dbb19c50bbc 100644 --- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs @@ -101,6 +101,7 @@ public static void AddPasswordManagerEFRepositories(this IServiceCollection serv services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs index ca0229e02f03..9112a32b4f3a 100644 --- a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs +++ b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs @@ -63,6 +63,7 @@ public DatabaseContext(DbContextOptions options) public DbSet Events { get; set; } public DbSet Folders { get; set; } public DbSet Grants { get; set; } + public DbSet TwoFactorRememberTokens { get; set; } public DbSet Groups { get; set; } public DbSet GroupUsers { get; set; } public DbSet Installations { get; set; } diff --git a/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs index d4a3f838ea9b..c196cd6f3239 100644 --- a/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs @@ -3,8 +3,10 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; +using Bit.Core.Utilities; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.IntegrationTest.AdminConsole; +using EfTwoFactorRememberToken = Bit.Infrastructure.EntityFramework.Auth.Models.TwoFactorRememberToken; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -286,6 +288,69 @@ public async Task DeleteExpiredAsync_RemovesOnlyExpiredRows( Assert.NotNull(await sut.GetByUserIdDeviceIdAsync(user.Id, liveDevice.Id)); } + // ------------------------------------------------------------------------------------------- + // R9 — the unique index exists on every provider + // ------------------------------------------------------------------------------------------- + + /// + /// Proves the one-row-per-device constraint is enforced by the database rather than only by the + /// upsert's own logic. Inserts directly, since going through UpsertAsync would update. + /// + [Theory, DatabaseData] + public async Task DuplicateUserIdDeviceId_IsRejectedByUniqueIndex( + ITwoFactorRememberTokenRepository sut, + IUserRepository userRepository, + IDeviceRepository deviceRepository, + Database database, + IServiceProvider services) + { + var user = await userRepository.CreateTestUserAsync(); + var device = await CreateTestDeviceAsync(deviceRepository, user.Id); + await sut.UpsertAsync(NewToken(user.Id, device.Id, "stamp-one")); + + await Assert.ThrowsAnyAsync( + () => InsertRowDirectlyAsync(services, database, user.Id, device.Id)); + } + + private static async Task InsertRowDirectlyAsync( + IServiceProvider services, Database database, Guid userId, Guid deviceId) + { + if (database.Type == SupportedDatabaseProviders.SqlServer && !database.UseEf) + { + await using var connection = new SqlConnection(database.ConnectionString); + await connection.OpenAsync(); + await using var command = connection.CreateCommand(); + command.CommandText = """ + INSERT INTO [dbo].[TwoFactorRememberToken] + ([Id], [UserId], [DeviceId], [Stamp], [CreationDate], [RevisionDate], [ExpirationDate]) + VALUES + (@Id, @UserId, @DeviceId, @Stamp, @Now, @Now, @Expiration) + """; + command.Parameters.Add(new SqlParameter("@Id", CoreHelpers.GenerateComb())); + command.Parameters.Add(new SqlParameter("@UserId", userId)); + command.Parameters.Add(new SqlParameter("@DeviceId", deviceId)); + command.Parameters.Add(new SqlParameter("@Stamp", "stamp-two")); + command.Parameters.Add(new SqlParameter("@Now", DateTime.UtcNow)); + command.Parameters.Add(new SqlParameter("@Expiration", DateTime.UtcNow.AddDays(30))); + await command.ExecuteNonQueryAsync(); + return; + } + + using var scope = services.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + dbContext.TwoFactorRememberTokens.Add(new EfTwoFactorRememberToken + { + Id = CoreHelpers.GenerateComb(), + UserId = userId, + DeviceId = deviceId, + Stamp = "stamp-two", + CreationDate = DateTime.UtcNow, + RevisionDate = DateTime.UtcNow, + ExpirationDate = DateTime.UtcNow.AddDays(30), + }); + await dbContext.SaveChangesAsync(); + } + // ------------------------------------------------------------------------------------------- // R8 — storage fidelity // ------------------------------------------------------------------------------------------- diff --git a/util/MySqlMigrations/Migrations/20260923010021_CreateTwoFactorRememberToken.Designer.cs b/util/MySqlMigrations/Migrations/20260923010021_CreateTwoFactorRememberToken.Designer.cs new file mode 100644 index 000000000000..f95c632d7809 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20260923010021_CreateTwoFactorRememberToken.Designer.cs @@ -0,0 +1,4179 @@ +// +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("20260923010021_CreateTwoFactorRememberToken")] + partial class CreateTwoFactorRememberToken + { + /// + 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.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CollectionName") + .HasColumnType("longtext"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("GroupName") + .HasColumnType("longtext"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("Manage") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("UserGuid") + .HasColumnType("char(36)"); + + b.Property("UserName") + .HasColumnType("longtext"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessRuleId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("longtext"); + + 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.Property("Type") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("AccessRuleId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.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.AdminConsole.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.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("ExemptFromBillingAutomation") + .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("SyncSeats") + .HasColumnType("tinyint(1)"); + + 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("UseAutomaticUserConfirmation") + .HasColumnType("tinyint(1)"); + + b.Property("UseCustomPermissions") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseDisableSmAdsForUsers") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseInviteLinks") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UseMyItems") + .HasColumnType("tinyint(1)"); + + b.Property("UseOrganizationDomains") + .HasColumnType("tinyint(1)"); + + b.Property("UsePam") + .HasColumnType("tinyint(1)"); + + b.Property("UsePasswordManager") + .HasColumnType("tinyint(1)"); + + b.Property("UsePhishingBlocker") + .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("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp", "UsersGetPremium" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationInviteLink", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowedDomains") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("Code") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Invite") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SupportsConfirmation") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInviteLink", (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") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + 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.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + 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("smallint"); + + 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.TwoFactorRememberToken", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeviceId") + .HasColumnType("char(36)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Stamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "DeviceId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("TwoFactorRememberToken", (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.OrganizationPlanMigrationCohort", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ChurnDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("IsActive") + .HasColumnType("tinyint(1)"); + + b.Property("MigrationPathId") + .HasColumnType("tinyint unsigned"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("ProactiveDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("varchar(64)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Name") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohort", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohortAssignment", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ChurnDiscountAppliedDate") + .HasColumnType("datetime(6)"); + + b.Property("CohortId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("MigratedDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RenewalNotificationSentDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ScheduledDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("CohortId", "CreationDate", "Id") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("CohortId", "ScheduledDate", "MigratedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohortAssignment", (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.Billing.Models.SubscriptionDiscount", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AmountOff") + .HasColumnType("bigint"); + + b.Property("AudienceType") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Currency") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Duration") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)"); + + b.Property("DurationInMonths") + .HasColumnType("int"); + + b.Property("EndDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("PercentOff") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("StartDate") + .HasColumnType("datetime(6)"); + + b.Property("StripeCouponId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("StripeProductIds") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("StripeCouponId") + .IsUnique(); + + b.HasIndex("StartDate", "EndDate") + .HasDatabaseName("IX_SubscriptionDiscount_DateRange") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SubscriptionDiscount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationDeleteTask", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CompletedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("FailureCount") + .HasColumnType("int"); + + b.Property("ItemsDeletedCount") + .HasColumnType("bigint"); + + b.Property("LastError") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("StartDate") + .HasColumnType("datetime(6)"); + + b.Property("TaskType") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CompletedDate", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationDeleteTask", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.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.Dirt.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("Filters") + .HasColumnType("longtext"); + + 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.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApplicationAtRiskCount") + .HasColumnType("int"); + + b.Property("ApplicationCount") + .HasColumnType("int"); + + b.Property("ApplicationData") + .HasColumnType("longtext"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("CriticalApplicationAtRiskCount") + .HasColumnType("int"); + + b.Property("CriticalApplicationCount") + .HasColumnType("int"); + + b.Property("CriticalMemberAtRiskCount") + .HasColumnType("int"); + + b.Property("CriticalMemberCount") + .HasColumnType("int"); + + b.Property("CriticalPasswordAtRiskCount") + .HasColumnType("int"); + + b.Property("CriticalPasswordCount") + .HasColumnType("int"); + + b.Property("MemberAtRiskCount") + .HasColumnType("int"); + + b.Property("MemberCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PasswordAtRiskCount") + .HasColumnType("int"); + + b.Property("PasswordCount") + .HasColumnType("int"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("ReportFile") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SummaryData") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.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.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.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.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("Active") + .HasColumnType("tinyint(1)") + .HasDefaultValue(true); + + b.Property("ClientVersion") + .HasMaxLength(43) + .HasColumnType("varchar(43)"); + + 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("LastActivityDate") + .HasColumnType("datetime(6)"); + + 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("GrantedServiceAccountId") + .HasColumnType("char(36)"); + + 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("ProjectId") + .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("SendId") + .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") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasDatabaseName("IX_Event_OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "SendId", "Date") + .HasDatabaseName("IX_Event_OrganizationIdSendIdDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + 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("AccessPam") + .HasColumnType("tinyint(1)"); + + 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("RevocationReason") + .HasColumnType("tinyint unsigned"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("StatusNew") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("V2UpgradeToken") + .HasColumnType("longtext"); + + 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.PlayItem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PlayId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("PlayId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ProviderId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PlayItem", null, t => + { + t.HasCheckConstraint("CK_PlayItem_UserOrOrganizationOrProvider", "((CASE WHEN \"UserId\" IS NOT NULL THEN 1 ELSE 0 END) + (CASE WHEN \"OrganizationId\" IS NOT NULL THEN 1 ELSE 0 END) + (CASE WHEN \"ProviderId\" IS NOT NULL THEN 1 ELSE 0 END)) = 1"); + }); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("AuthType") + .HasColumnType("tinyint unsigned"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("DeletionDate") + .HasColumnType("datetime(6)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("Emails") + .HasMaxLength(4000) + .HasColumnType("varchar(4000)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("HideEmail") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .IsRequired() + .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("LastApiKeyRotationDate") + .HasColumnType("datetime(6)"); + + 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("MasterPasswordSalt") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + 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("SecurityState") + .HasColumnType("longtext"); + + b.Property("SecurityVersion") + .HasColumnType("int"); + + b.Property("SignedPublicKey") + .HasColumnType("longtext"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UserKeyId") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("V2UpgradeToken") + .HasColumnType("longtext"); + + b.Property("VerifyDevices") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SignatureAlgorithm") + .HasColumnType("tinyint unsigned"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (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.Pam.Models.AccessDecision", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessRequestId") + .HasColumnType("char(36)"); + + b.Property("ApproverId") + .HasColumnType("char(36)"); + + b.Property("Comment") + .HasColumnType("longtext"); + + b.Property("ConditionKind") + .HasColumnType("tinyint unsigned"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeciderKind") + .HasColumnType("tinyint unsigned"); + + b.Property("EvaluationContext") + .HasColumnType("longtext"); + + b.Property("Verdict") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccessRequestId"); + + b.ToTable("AccessDecision", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessLease", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessRequestId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("NotAfter") + .HasColumnType("datetime(6)"); + + b.Property("NotBefore") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RequesterId") + .HasColumnType("char(36)"); + + b.Property("RevokedBy") + .HasColumnType("char(36)"); + + b.Property("RevokedDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("AccessRequestId") + .IsUnique(); + + b.HasIndex("OrganizationId"); + + b.HasIndex("CipherId", "Status"); + + b.HasIndex("CollectionId", "Status"); + + b.HasIndex("NotAfter", "Status"); + + b.HasIndex("RequesterId", "CipherId", "Status"); + + b.ToTable("AccessLease", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRequest", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExtensionOfLeaseId") + .HasColumnType("char(36)"); + + b.Property("NotAfter") + .HasColumnType("datetime(6)"); + + b.Property("NotBefore") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Reason") + .HasColumnType("longtext"); + + b.Property("RequesterId") + .HasColumnType("char(36)"); + + b.Property("ResolvedDate") + .HasColumnType("datetime(6)"); + + b.Property("RuleId") + .HasColumnType("char(36)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("ExtensionOfLeaseId"); + + b.HasIndex("RuleId"); + + b.HasIndex("CollectionId", "Status"); + + b.HasIndex("OrganizationId", "Status"); + + b.HasIndex("RequesterId", "CipherId", "Status"); + + b.ToTable("AccessRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AllowsExtensions") + .HasColumnType("tinyint(1)"); + + b.Property("Conditions") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DefaultLeaseDurationSeconds") + .HasColumnType("int"); + + b.Property("Description") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("LastEditedBy") + .HasColumnType("char(36)"); + + b.Property("MaxExtensionDurationSeconds") + .HasColumnType("int"); + + b.Property("MaxLeaseDurationSeconds") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SingleActiveLease") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("AccessRule", (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.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("EditorServiceAccountId") + .HasColumnType("char(36)"); + + b.Property("SecretId") + .HasColumnType("char(36)"); + + b.Property("Value") + .IsRequired() + .HasColumnType("longtext"); + + b.Property("VersionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + + 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.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Archives") + .HasColumnType("longtext"); + + 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.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", null) + .WithMany() + .HasForeignKey("AccessRuleId") + .OnDelete(DeleteBehavior.Restrict); + + 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.AdminConsole.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.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.AdminConsole.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.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.AdminConsole.Models.OrganizationInviteLink", 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.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.TwoFactorRememberToken", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Device"); + + 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.OrganizationPlanMigrationCohortAssignment", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohort", "Cohort") + .WithMany() + .HasForeignKey("CohortId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cohort"); + + 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.Dirt.Models.OrganizationApplication", 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.Dirt.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.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", 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.Dirt.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.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.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + 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.PlayItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + 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.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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.Pam.Models.AccessDecision", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRequest", null) + .WithMany() + .HasForeignKey("AccessRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessLease", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRequest", null) + .WithMany() + .HasForeignKey("AccessRequestId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessLease", null) + .WithMany() + .HasForeignKey("ExtensionOfLeaseId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", null) + .WithMany() + .HasForeignKey("RuleId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", 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.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.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + + 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.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.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + 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.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("SecretVersions"); + + 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/20260923010021_CreateTwoFactorRememberToken.cs b/util/MySqlMigrations/Migrations/20260923010021_CreateTwoFactorRememberToken.cs new file mode 100644 index 000000000000..fd8e02961a36 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20260923010021_CreateTwoFactorRememberToken.cs @@ -0,0 +1,68 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.MySqlMigrations.Migrations +{ + /// + public partial class CreateTwoFactorRememberToken : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TwoFactorRememberToken", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + UserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + DeviceId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Stamp = table.Column(type: "varchar(50)", maxLength: 50, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CreationDate = table.Column(type: "datetime(6)", nullable: false), + RevisionDate = table.Column(type: "datetime(6)", nullable: false), + ExpirationDate = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TwoFactorRememberToken", x => x.Id); + table.ForeignKey( + name: "FK_TwoFactorRememberToken_Device_DeviceId", + column: x => x.DeviceId, + principalTable: "Device", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TwoFactorRememberToken_User_UserId", + column: x => x.UserId, + principalTable: "User", + principalColumn: "Id"); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_TwoFactorRememberToken_DeviceId", + table: "TwoFactorRememberToken", + column: "DeviceId"); + + migrationBuilder.CreateIndex( + name: "IX_TwoFactorRememberToken_ExpirationDate", + table: "TwoFactorRememberToken", + column: "ExpirationDate"); + + migrationBuilder.CreateIndex( + name: "IX_TwoFactorRememberToken_UserId_DeviceId", + table: "TwoFactorRememberToken", + columns: new[] { "UserId", "DeviceId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TwoFactorRememberToken"); + } + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index 1bf375260bad..a05e910d06ee 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -865,6 +865,46 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("SsoUser", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.TwoFactorRememberToken", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("DeviceId") + .HasColumnType("char(36)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Stamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "DeviceId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("TwoFactorRememberToken", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => { b.Property("Id") @@ -3732,6 +3772,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.TwoFactorRememberToken", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Device"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => { b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") diff --git a/util/PostgresMigrations/Migrations/20260923010041_CreateTwoFactorRememberToken.Designer.cs b/util/PostgresMigrations/Migrations/20260923010041_CreateTwoFactorRememberToken.Designer.cs new file mode 100644 index 000000000000..4d8e979eef64 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20260923010041_CreateTwoFactorRememberToken.Designer.cs @@ -0,0 +1,4185 @@ +// +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("20260923010041_CreateTwoFactorRememberToken")] + partial class CreateTwoFactorRememberToken + { + /// + 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.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CollectionName") + .HasColumnType("text"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("GroupName") + .HasColumnType("text"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("Manage") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("UserGuid") + .HasColumnType("uuid"); + + b.Property("UserName") + .HasColumnType("text"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessRuleId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultUserCollectionEmail") + .HasColumnType("text"); + + 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.Property("Type") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AccessRuleId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.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.AdminConsole.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.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("ExemptFromBillingAutomation") + .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("SyncSeats") + .HasColumnType("boolean"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseAutomaticUserConfirmation") + .HasColumnType("boolean"); + + b.Property("UseCustomPermissions") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseDisableSmAdsForUsers") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseInviteLinks") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UseMyItems") + .HasColumnType("boolean"); + + b.Property("UseOrganizationDomains") + .HasColumnType("boolean"); + + b.Property("UsePam") + .HasColumnType("boolean"); + + b.Property("UsePasswordManager") + .HasColumnType("boolean"); + + b.Property("UsePhishingBlocker") + .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("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Id", "Enabled"); + + NpgsqlIndexBuilderExtensions.IncludeProperties(b.HasIndex("Id", "Enabled"), new[] { "UseTotp", "UsersGetPremium" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationInviteLink", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowedDomains") + .IsRequired() + .HasColumnType("text"); + + b.Property("Code") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Invite") + .IsRequired() + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SupportsConfirmation") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInviteLink", (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") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + 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.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + 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("smallint"); + + 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.TwoFactorRememberToken", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceId") + .HasColumnType("uuid"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Stamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "DeviceId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("TwoFactorRememberToken", (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.OrganizationPlanMigrationCohort", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ChurnDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MigrationPathId") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("ProactiveDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Name") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohort", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohortAssignment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ChurnDiscountAppliedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CohortId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("MigratedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RenewalNotificationSentDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ScheduledDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("CohortId", "CreationDate", "Id") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("CohortId", "ScheduledDate", "MigratedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohortAssignment", (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.Billing.Models.SubscriptionDiscount", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AmountOff") + .HasColumnType("bigint"); + + b.Property("AudienceType") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Currency") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Duration") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("DurationInMonths") + .HasColumnType("integer"); + + b.Property("EndDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("PercentOff") + .HasPrecision(5, 2) + .HasColumnType("numeric(5,2)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StripeCouponId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("StripeProductIds") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("StripeCouponId") + .IsUnique(); + + b.HasIndex("StartDate", "EndDate") + .HasDatabaseName("IX_SubscriptionDiscount_DateRange") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SubscriptionDiscount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationDeleteTask", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CompletedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("FailureCount") + .HasColumnType("integer"); + + b.Property("ItemsDeletedCount") + .HasColumnType("bigint"); + + b.Property("LastError") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("StartDate") + .HasColumnType("timestamp with time zone"); + + b.Property("TaskType") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CompletedDate", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationDeleteTask", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.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.Dirt.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("Filters") + .HasColumnType("text"); + + 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.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApplicationAtRiskCount") + .HasColumnType("integer"); + + b.Property("ApplicationCount") + .HasColumnType("integer"); + + b.Property("ApplicationData") + .HasColumnType("text"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CriticalApplicationAtRiskCount") + .HasColumnType("integer"); + + b.Property("CriticalApplicationCount") + .HasColumnType("integer"); + + b.Property("CriticalMemberAtRiskCount") + .HasColumnType("integer"); + + b.Property("CriticalMemberCount") + .HasColumnType("integer"); + + b.Property("CriticalPasswordAtRiskCount") + .HasColumnType("integer"); + + b.Property("CriticalPasswordCount") + .HasColumnType("integer"); + + b.Property("MemberAtRiskCount") + .HasColumnType("integer"); + + b.Property("MemberCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PasswordAtRiskCount") + .HasColumnType("integer"); + + b.Property("PasswordCount") + .HasColumnType("integer"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("text"); + + b.Property("ReportFile") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SummaryData") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.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.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.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.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Active") + .HasColumnType("boolean") + .HasDefaultValue(true); + + b.Property("ClientVersion") + .HasMaxLength(43) + .HasColumnType("character varying(43)"); + + 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("LastActivityDate") + .HasColumnType("timestamp with time zone"); + + 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("GrantedServiceAccountId") + .HasColumnType("uuid"); + + 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("ProjectId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("ProviderOrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderUserId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("SendId") + .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") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasDatabaseName("IX_Event_OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "SendId", "Date") + .HasDatabaseName("IX_Event_OrganizationIdSendIdDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + 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("AccessPam") + .HasColumnType("boolean"); + + 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("RevocationReason") + .HasColumnType("smallint"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("StatusNew") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("V2UpgradeToken") + .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.PlayItem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PlayId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("PlayId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ProviderId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PlayItem", null, t => + { + t.HasCheckConstraint("CK_PlayItem_UserOrOrganizationOrProvider", "((CASE WHEN \"UserId\" IS NOT NULL THEN 1 ELSE 0 END) + (CASE WHEN \"OrganizationId\" IS NOT NULL THEN 1 ELSE 0 END) + (CASE WHEN \"ProviderId\" IS NOT NULL THEN 1 ELSE 0 END)) = 1"); + }); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("AuthType") + .HasColumnType("smallint"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Data") + .IsRequired() + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("Emails") + .HasMaxLength(4000) + .HasColumnType("character varying(4000)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("HideEmail") + .HasColumnType("boolean"); + + b.Property("Key") + .IsRequired() + .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("LastApiKeyRotationDate") + .HasColumnType("timestamp with time zone"); + + 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("MasterPasswordSalt") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + 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("SecurityState") + .HasColumnType("text"); + + b.Property("SecurityVersion") + .HasColumnType("integer"); + + b.Property("SignedPublicKey") + .HasColumnType("text"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UserKeyId") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.Property("V2UpgradeToken") + .HasColumnType("text"); + + b.Property("VerifyDevices") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SignatureAlgorithm") + .HasColumnType("smallint"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("text"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (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.Pam.Models.AccessDecision", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessRequestId") + .HasColumnType("uuid"); + + b.Property("ApproverId") + .HasColumnType("uuid"); + + b.Property("Comment") + .HasColumnType("text"); + + b.Property("ConditionKind") + .HasColumnType("smallint"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeciderKind") + .HasColumnType("smallint"); + + b.Property("EvaluationContext") + .HasColumnType("text"); + + b.Property("Verdict") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("AccessRequestId"); + + b.ToTable("AccessDecision", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessLease", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessRequestId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NotAfter") + .HasColumnType("timestamp with time zone"); + + b.Property("NotBefore") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RequesterId") + .HasColumnType("uuid"); + + b.Property("RevokedBy") + .HasColumnType("uuid"); + + b.Property("RevokedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("AccessRequestId") + .IsUnique(); + + b.HasIndex("OrganizationId"); + + b.HasIndex("CipherId", "Status"); + + b.HasIndex("CollectionId", "Status"); + + b.HasIndex("NotAfter", "Status"); + + b.HasIndex("RequesterId", "CipherId", "Status"); + + b.ToTable("AccessLease", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRequest", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExtensionOfLeaseId") + .HasColumnType("uuid"); + + b.Property("NotAfter") + .HasColumnType("timestamp with time zone"); + + b.Property("NotBefore") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reason") + .HasColumnType("text"); + + b.Property("RequesterId") + .HasColumnType("uuid"); + + b.Property("ResolvedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RuleId") + .HasColumnType("uuid"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ExtensionOfLeaseId"); + + b.HasIndex("RuleId"); + + b.HasIndex("CollectionId", "Status"); + + b.HasIndex("OrganizationId", "Status"); + + b.HasIndex("RequesterId", "CipherId", "Status"); + + b.ToTable("AccessRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AllowsExtensions") + .HasColumnType("boolean"); + + b.Property("Conditions") + .IsRequired() + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultLeaseDurationSeconds") + .HasColumnType("integer"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("LastEditedBy") + .HasColumnType("uuid"); + + b.Property("MaxExtensionDurationSeconds") + .HasColumnType("integer"); + + b.Property("MaxLeaseDurationSeconds") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SingleActiveLease") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("AccessRule", (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.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("EditorServiceAccountId") + .HasColumnType("uuid"); + + b.Property("SecretId") + .HasColumnType("uuid"); + + b.Property("Value") + .IsRequired() + .HasColumnType("text"); + + b.Property("VersionDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + + 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.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Archives") + .HasColumnType("text"); + + 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.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", null) + .WithMany() + .HasForeignKey("AccessRuleId") + .OnDelete(DeleteBehavior.Restrict); + + 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.AdminConsole.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.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.AdminConsole.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.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.AdminConsole.Models.OrganizationInviteLink", 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.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.TwoFactorRememberToken", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Device"); + + 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.OrganizationPlanMigrationCohortAssignment", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohort", "Cohort") + .WithMany() + .HasForeignKey("CohortId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cohort"); + + 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.Dirt.Models.OrganizationApplication", 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.Dirt.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.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", 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.Dirt.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.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.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + 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.PlayItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + 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.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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.Pam.Models.AccessDecision", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRequest", null) + .WithMany() + .HasForeignKey("AccessRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessLease", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRequest", null) + .WithMany() + .HasForeignKey("AccessRequestId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessLease", null) + .WithMany() + .HasForeignKey("ExtensionOfLeaseId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", null) + .WithMany() + .HasForeignKey("RuleId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", 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.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.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + + 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.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.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + 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.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("SecretVersions"); + + 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/20260923010041_CreateTwoFactorRememberToken.cs b/util/PostgresMigrations/Migrations/20260923010041_CreateTwoFactorRememberToken.cs new file mode 100644 index 000000000000..2c6a95e3712b --- /dev/null +++ b/util/PostgresMigrations/Migrations/20260923010041_CreateTwoFactorRememberToken.cs @@ -0,0 +1,66 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.PostgresMigrations.Migrations +{ + /// + public partial class CreateTwoFactorRememberToken : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TwoFactorRememberToken", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + DeviceId = table.Column(type: "uuid", nullable: false), + Stamp = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + CreationDate = table.Column(type: "timestamp with time zone", nullable: false), + RevisionDate = table.Column(type: "timestamp with time zone", nullable: false), + ExpirationDate = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TwoFactorRememberToken", x => x.Id); + table.ForeignKey( + name: "FK_TwoFactorRememberToken_Device_DeviceId", + column: x => x.DeviceId, + principalTable: "Device", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TwoFactorRememberToken_User_UserId", + column: x => x.UserId, + principalTable: "User", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_TwoFactorRememberToken_DeviceId", + table: "TwoFactorRememberToken", + column: "DeviceId"); + + migrationBuilder.CreateIndex( + name: "IX_TwoFactorRememberToken_ExpirationDate", + table: "TwoFactorRememberToken", + column: "ExpirationDate"); + + migrationBuilder.CreateIndex( + name: "IX_TwoFactorRememberToken_UserId_DeviceId", + table: "TwoFactorRememberToken", + columns: new[] { "UserId", "DeviceId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TwoFactorRememberToken"); + } + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index 16e52e26431f..832a69adc235 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -870,6 +870,46 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("SsoUser", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.TwoFactorRememberToken", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("DeviceId") + .HasColumnType("uuid"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Stamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "DeviceId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("TwoFactorRememberToken", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => { b.Property("Id") @@ -3738,6 +3778,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.TwoFactorRememberToken", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Device"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => { b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") diff --git a/util/SqliteMigrations/Migrations/20260923010055_CreateTwoFactorRememberToken.Designer.cs b/util/SqliteMigrations/Migrations/20260923010055_CreateTwoFactorRememberToken.Designer.cs new file mode 100644 index 000000000000..02b9f3f65f09 --- /dev/null +++ b/util/SqliteMigrations/Migrations/20260923010055_CreateTwoFactorRememberToken.Designer.cs @@ -0,0 +1,4168 @@ +// +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("20260923010055_CreateTwoFactorRememberToken")] + partial class CreateTwoFactorRememberToken + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + + modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => + { + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CollectionName") + .HasColumnType("TEXT"); + + b.Property("Email") + .HasColumnType("TEXT"); + + b.Property("GroupId") + .HasColumnType("TEXT"); + + b.Property("GroupName") + .HasColumnType("TEXT"); + + b.Property("HidePasswords") + .HasColumnType("INTEGER"); + + b.Property("Manage") + .HasColumnType("INTEGER"); + + b.Property("ReadOnly") + .HasColumnType("INTEGER"); + + b.Property("ResetPasswordKey") + .HasColumnType("TEXT"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("UserGuid") + .HasColumnType("TEXT"); + + b.Property("UserName") + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.ToTable("OrganizationMemberBaseDetails"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessRuleId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DefaultUserCollectionEmail") + .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.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccessRuleId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.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.AdminConsole.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.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("ExemptFromBillingAutomation") + .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("SyncSeats") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("Use2fa") + .HasColumnType("INTEGER"); + + b.Property("UseAdminSponsoredFamilies") + .HasColumnType("INTEGER"); + + b.Property("UseApi") + .HasColumnType("INTEGER"); + + b.Property("UseAutomaticUserConfirmation") + .HasColumnType("INTEGER"); + + b.Property("UseCustomPermissions") + .HasColumnType("INTEGER"); + + b.Property("UseDirectory") + .HasColumnType("INTEGER"); + + b.Property("UseDisableSmAdsForUsers") + .HasColumnType("INTEGER"); + + b.Property("UseEvents") + .HasColumnType("INTEGER"); + + b.Property("UseGroups") + .HasColumnType("INTEGER"); + + b.Property("UseInviteLinks") + .HasColumnType("INTEGER"); + + b.Property("UseKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("UseMyItems") + .HasColumnType("INTEGER"); + + b.Property("UseOrganizationDomains") + .HasColumnType("INTEGER"); + + b.Property("UsePam") + .HasColumnType("INTEGER"); + + b.Property("UsePasswordManager") + .HasColumnType("INTEGER"); + + b.Property("UsePhishingBlocker") + .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("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Id", "Enabled") + .HasAnnotation("Npgsql:IndexInclude", new[] { "UseTotp", "UsersGetPremium" }); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationInviteLink", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowedDomains") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Code") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Invite") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SupportsConfirmation") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationInviteLink", (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") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .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.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + 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.TwoFactorRememberToken", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Stamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "DeviceId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("TwoFactorRememberToken", (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.OrganizationPlanMigrationCohort", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ChurnDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("IsActive") + .HasColumnType("INTEGER"); + + b.Property("MigrationPathId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("ProactiveDiscountCouponCode") + .HasMaxLength(64) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("Name") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohort", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohortAssignment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ChurnDiscountAppliedDate") + .HasColumnType("TEXT"); + + b.Property("CohortId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("MigratedDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RenewalNotificationSentDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("ScheduledDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("CohortId", "CreationDate", "Id") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("CohortId", "ScheduledDate", "MigratedDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationPlanMigrationCohortAssignment", (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.Billing.Models.SubscriptionDiscount", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AmountOff") + .HasColumnType("INTEGER"); + + b.Property("AudienceType") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Currency") + .HasMaxLength(10) + .HasColumnType("TEXT"); + + b.Property("Duration") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("DurationInMonths") + .HasColumnType("INTEGER"); + + b.Property("EndDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("PercentOff") + .HasPrecision(5, 2) + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("StripeCouponId") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("StripeProductIds") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("StripeCouponId") + .IsUnique(); + + b.HasIndex("StartDate", "EndDate") + .HasDatabaseName("IX_SubscriptionDiscount_DateRange") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("SubscriptionDiscount", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationApplication", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Applications") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationApplication", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationDeleteTask", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CompletedDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("FailureCount") + .HasColumnType("INTEGER"); + + b.Property("ItemsDeletedCount") + .HasColumnType("INTEGER"); + + b.Property("LastError") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("StartDate") + .HasColumnType("TEXT"); + + b.Property("TaskType") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("CompletedDate", "CreationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationDeleteTask", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.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.Dirt.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("Filters") + .HasColumnType("TEXT"); + + 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.Dirt.Models.OrganizationReport", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApplicationAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("ApplicationCount") + .HasColumnType("INTEGER"); + + b.Property("ApplicationData") + .HasColumnType("TEXT"); + + b.Property("ContentEncryptionKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("CriticalApplicationAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalApplicationCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalMemberAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalMemberCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalPasswordAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("CriticalPasswordCount") + .HasColumnType("INTEGER"); + + b.Property("MemberAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("MemberCount") + .HasColumnType("INTEGER"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PasswordAtRiskCount") + .HasColumnType("INTEGER"); + + b.Property("PasswordCount") + .HasColumnType("INTEGER"); + + b.Property("ReportData") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("ReportFile") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SummaryData") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("OrganizationReport", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.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.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.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.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Active") + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("ClientVersion") + .HasMaxLength(43) + .HasColumnType("TEXT"); + + 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("LastActivityDate") + .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("GrantedServiceAccountId") + .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("ProjectId") + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("ProviderOrganizationId") + .HasColumnType("TEXT"); + + b.Property("ProviderUserId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("SendId") + .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") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("OrganizationId") + .HasDatabaseName("IX_Event_OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("OrganizationId", "SendId", "Date") + .HasDatabaseName("IX_Event_OrganizationIdSendIdDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("Date", "OrganizationId", "ActingUserId", "CipherId") + .HasDatabaseName("IX_Event_DateOrganizationIdUserId") + .HasAnnotation("SqlServer:Clustered", false) + .HasAnnotation("SqlServer:Include", new[] { "ServiceAccountId", "GrantedServiceAccountId" }); + + 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("AccessPam") + .HasColumnType("INTEGER"); + + 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("RevocationReason") + .HasColumnType("INTEGER"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.Property("StatusNew") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("V2UpgradeToken") + .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.PlayItem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("PlayId") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ProviderId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("PlayId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ProviderId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("PlayItem", null, t => + { + t.HasCheckConstraint("CK_PlayItem_UserOrOrganizationOrProvider", "((CASE WHEN \"UserId\" IS NOT NULL THEN 1 ELSE 0 END) + (CASE WHEN \"OrganizationId\" IS NOT NULL THEN 1 ELSE 0 END) + (CASE WHEN \"ProviderId\" IS NOT NULL THEN 1 ELSE 0 END)) = 1"); + }); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessCount") + .HasColumnType("INTEGER"); + + b.Property("AuthType") + .HasColumnType("INTEGER"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Data") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeletionDate") + .HasColumnType("TEXT"); + + b.Property("Disabled") + .HasColumnType("INTEGER"); + + b.Property("Emails") + .HasMaxLength(4000) + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("HideEmail") + .HasColumnType("INTEGER"); + + b.Property("Key") + .IsRequired() + .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("LastApiKeyRotationDate") + .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("MasterPasswordSalt") + .HasMaxLength(256) + .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("SecurityState") + .HasColumnType("TEXT"); + + b.Property("SecurityVersion") + .HasColumnType("INTEGER"); + + b.Property("SignedPublicKey") + .HasColumnType("TEXT"); + + b.Property("Storage") + .HasColumnType("INTEGER"); + + b.Property("TwoFactorProviders") + .HasColumnType("TEXT"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UserKeyId") + .HasMaxLength(32) + .HasColumnType("TEXT"); + + b.Property("UsesKeyConnector") + .HasColumnType("INTEGER"); + + b.Property("V2UpgradeToken") + .HasColumnType("TEXT"); + + b.Property("VerifyDevices") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Email") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("GatewayCustomerId"); + + b.HasIndex("GatewaySubscriptionId"); + + b.HasIndex("Premium", "PremiumExpirationDate", "RenewalReminderDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("User", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.UserSignatureKeyPair", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SignatureAlgorithm") + .HasColumnType("INTEGER"); + + b.Property("SigningKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("VerifyingKey") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("UserSignatureKeyPair", (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.Pam.Models.AccessDecision", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessRequestId") + .HasColumnType("TEXT"); + + b.Property("ApproverId") + .HasColumnType("TEXT"); + + b.Property("Comment") + .HasColumnType("TEXT"); + + b.Property("ConditionKind") + .HasColumnType("INTEGER"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeciderKind") + .HasColumnType("INTEGER"); + + b.Property("EvaluationContext") + .HasColumnType("TEXT"); + + b.Property("Verdict") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccessRequestId"); + + b.ToTable("AccessDecision", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessLease", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessRequestId") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("NotAfter") + .HasColumnType("TEXT"); + + b.Property("NotBefore") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RequesterId") + .HasColumnType("TEXT"); + + b.Property("RevokedBy") + .HasColumnType("TEXT"); + + b.Property("RevokedDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AccessRequestId") + .IsUnique(); + + b.HasIndex("OrganizationId"); + + b.HasIndex("CipherId", "Status"); + + b.HasIndex("CollectionId", "Status"); + + b.HasIndex("NotAfter", "Status"); + + b.HasIndex("RequesterId", "CipherId", "Status"); + + b.ToTable("AccessLease", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRequest", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExtensionOfLeaseId") + .HasColumnType("TEXT"); + + b.Property("NotAfter") + .HasColumnType("TEXT"); + + b.Property("NotBefore") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Reason") + .HasColumnType("TEXT"); + + b.Property("RequesterId") + .HasColumnType("TEXT"); + + b.Property("ResolvedDate") + .HasColumnType("TEXT"); + + b.Property("RuleId") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ExtensionOfLeaseId"); + + b.HasIndex("RuleId"); + + b.HasIndex("CollectionId", "Status"); + + b.HasIndex("OrganizationId", "Status"); + + b.HasIndex("RequesterId", "CipherId", "Status"); + + b.ToTable("AccessRequest", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AllowsExtensions") + .HasColumnType("INTEGER"); + + b.Property("Conditions") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DefaultLeaseDurationSeconds") + .HasColumnType("INTEGER"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("LastEditedBy") + .HasColumnType("TEXT"); + + b.Property("MaxExtensionDurationSeconds") + .HasColumnType("INTEGER"); + + b.Property("MaxLeaseDurationSeconds") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SingleActiveLease") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("AccessRule", (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.SecretVersion", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("EditorOrganizationUserId") + .HasColumnType("TEXT"); + + b.Property("EditorServiceAccountId") + .HasColumnType("TEXT"); + + b.Property("SecretId") + .HasColumnType("TEXT"); + + b.Property("Value") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("VersionDate") + .HasColumnType("TEXT"); + + b.HasKey("Id") + .HasAnnotation("SqlServer:Clustered", true); + + b.HasIndex("EditorOrganizationUserId") + .HasDatabaseName("IX_SecretVersion_EditorOrganizationUserId"); + + b.HasIndex("EditorServiceAccountId") + .HasDatabaseName("IX_SecretVersion_EditorServiceAccountId"); + + b.HasIndex("SecretId") + .HasDatabaseName("IX_SecretVersion_SecretId"); + + b.ToTable("SecretVersion"); + }); + + 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.Vault.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Archives") + .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.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", null) + .WithMany() + .HasForeignKey("AccessRuleId") + .OnDelete(DeleteBehavior.Restrict); + + 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.AdminConsole.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.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.AdminConsole.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.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.AdminConsole.Models.OrganizationInviteLink", 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.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.TwoFactorRememberToken", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Device"); + + 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.OrganizationPlanMigrationCohortAssignment", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationPlanMigrationCohort", "Cohort") + .WithMany() + .HasForeignKey("CohortId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cohort"); + + 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.Dirt.Models.OrganizationApplication", 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.Dirt.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.Dirt.Models.OrganizationIntegrationConfiguration", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationIntegration", "OrganizationIntegration") + .WithMany() + .HasForeignKey("OrganizationIntegrationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationIntegration"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Dirt.Models.OrganizationReport", 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.Dirt.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.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.AdminConsole.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + 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.PlayItem", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Provider.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + + 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.Models.UserSignatureKeyPair", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + 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.Pam.Models.AccessDecision", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRequest", null) + .WithMany() + .HasForeignKey("AccessRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessLease", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRequest", null) + .WithMany() + .HasForeignKey("AccessRequestId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRequest", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessLease", null) + .WithMany() + .HasForeignKey("ExtensionOfLeaseId") + .OnDelete(DeleteBehavior.Restrict); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", null) + .WithMany() + .HasForeignKey("RuleId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRule", 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.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.SecretVersion", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "EditorOrganizationUser") + .WithMany() + .HasForeignKey("EditorOrganizationUserId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "EditorServiceAccount") + .WithMany() + .HasForeignKey("EditorServiceAccountId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.Secret", "Secret") + .WithMany("SecretVersions") + .HasForeignKey("SecretId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EditorOrganizationUser"); + + b.Navigation("EditorServiceAccount"); + + b.Navigation("Secret"); + }); + + 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.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.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + 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.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("SecretVersions"); + + 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/20260923010055_CreateTwoFactorRememberToken.cs b/util/SqliteMigrations/Migrations/20260923010055_CreateTwoFactorRememberToken.cs new file mode 100644 index 000000000000..6dff2032cadf --- /dev/null +++ b/util/SqliteMigrations/Migrations/20260923010055_CreateTwoFactorRememberToken.cs @@ -0,0 +1,66 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace Bit.SqliteMigrations.Migrations +{ + /// + public partial class CreateTwoFactorRememberToken : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "TwoFactorRememberToken", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + UserId = table.Column(type: "TEXT", nullable: false), + DeviceId = table.Column(type: "TEXT", nullable: false), + Stamp = table.Column(type: "TEXT", maxLength: 50, nullable: false), + CreationDate = table.Column(type: "TEXT", nullable: false), + RevisionDate = table.Column(type: "TEXT", nullable: false), + ExpirationDate = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TwoFactorRememberToken", x => x.Id); + table.ForeignKey( + name: "FK_TwoFactorRememberToken_Device_DeviceId", + column: x => x.DeviceId, + principalTable: "Device", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TwoFactorRememberToken_User_UserId", + column: x => x.UserId, + principalTable: "User", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_TwoFactorRememberToken_DeviceId", + table: "TwoFactorRememberToken", + column: "DeviceId"); + + migrationBuilder.CreateIndex( + name: "IX_TwoFactorRememberToken_ExpirationDate", + table: "TwoFactorRememberToken", + column: "ExpirationDate"); + + migrationBuilder.CreateIndex( + name: "IX_TwoFactorRememberToken_UserId_DeviceId", + table: "TwoFactorRememberToken", + columns: new[] { "UserId", "DeviceId" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TwoFactorRememberToken"); + } + } +} diff --git a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs index ab8c546e9e5b..90a871907d42 100644 --- a/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/SqliteMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -854,6 +854,46 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("SsoUser", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.TwoFactorRememberToken", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("DeviceId") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Stamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("DeviceId") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("ExpirationDate") + .HasAnnotation("SqlServer:Clustered", false); + + b.HasIndex("UserId", "DeviceId") + .IsUnique() + .HasAnnotation("SqlServer:Clustered", false); + + b.ToTable("TwoFactorRememberToken", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => { b.Property("Id") @@ -3721,6 +3761,25 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.TwoFactorRememberToken", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Device", "Device") + .WithMany() + .HasForeignKey("DeviceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Device"); + + b.Navigation("User"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.WebAuthnCredential", b => { b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") From 324cbfed3b6a583194cd2d61d09beec6d62fbf55 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:13:10 -0500 Subject: [PATCH 04/27] [PM-40673 Part 1] add TwoFactorRememberTokenable.cs --- .../Tokenables/TwoFactorRememberTokenable.cs | 71 ++++++++ .../Utilities/ServiceCollectionExtensions.cs | 6 + .../TwoFactorRememberTokenableTests.cs | 167 ++++++++++++++++++ 3 files changed, 244 insertions(+) create mode 100644 src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenable.cs create mode 100644 test/Core.Test/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableTests.cs diff --git a/src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenable.cs b/src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenable.cs new file mode 100644 index 000000000000..b803afdaf6f4 --- /dev/null +++ b/src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenable.cs @@ -0,0 +1,71 @@ +using System.Text.Json.Serialization; +using Bit.Core.Tokens; + +namespace Bit.Core.Auth.Models.Business.Tokenables; + +/// +/// The "remember this device" token a client presents to satisfy two-factor authentication without +/// being challenged again. +/// +/// +/// The token is encrypted and signed, so a client can neither read nor alter its contents. It names +/// the device it was issued to and carries two stamps that are compared against server-side state on +/// every use: against the user, and against that +/// device's TwoFactorRememberToken row. +/// +public class TwoFactorRememberTokenable : ExpiringTokenable +{ + public static TimeSpan GetTokenLifetime() => TimeSpan.FromDays(30); + + public const string ClearTextPrefix = "BwTwoFactorRemember_"; + public const string DataProtectorPurpose = "TwoFactorRememberTokenDataProtector"; + public const string TokenIdentifier = "TwoFactorRememberToken"; + + public string Identifier { get; set; } = TokenIdentifier; + + public Guid UserId { get; set; } + + /// + /// The primary key of the device this token was issued to, used to locate the row to compare + /// against. + /// + public Guid DeviceId { get; set; } + + /// + /// The client-generated device identifier from the request that minted this token. Compared + /// against the presenting request's identifier, which binds the token to one device without + /// needing a database read. + /// + public string DeviceIdentifier { get; set; } = null!; + + /// + /// A copy of the issuing device's row stamp. Device-scoped. + /// + public string Stamp { get; set; } = null!; + + // TODO: PM-XXXXX - Consider removing SecurityStamp from this token. The row's Stamp already + // covers the same ground more precisely, and carrying the user stamp ties remember-me lifetime + // to every user stamp rotation. Any removal has to stay backwards compatible: tokens already + // issued carry this field, so treat it as optional on read before dropping it. + /// + /// A copy of User.SecurityStamp as it stood at issuance. Account-scoped. + /// + public string SecurityStamp { get; set; } = null!; + + [JsonConstructor] + public TwoFactorRememberTokenable() + { + ExpirationDate = DateTime.UtcNow.Add(GetTokenLifetime()); + } + + /// + /// Checks only that the deserialized fields are present and well-formed. Comparing them against + /// the user and the device's row requires state this type does not have, and happens in + /// ValidateTwoFactorRememberTokenQuery. + /// + protected override bool TokenIsValid() => + Identifier == TokenIdentifier && UserId != default && DeviceId != default + && !string.IsNullOrWhiteSpace(DeviceIdentifier) + && !string.IsNullOrWhiteSpace(Stamp) + && !string.IsNullOrWhiteSpace(SecurityStamp); +} diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 04d61d530ec5..c98b23e3ad9c 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -240,6 +240,12 @@ public static void AddTokenizers(this IServiceCollection services) DuoUserStateTokenable.DataProtectorPurpose, serviceProvider.GetDataProtectionProvider(), serviceProvider.GetRequiredService>>())); + services.AddSingleton>(serviceProvider => + new DataProtectorTokenFactory( + TwoFactorRememberTokenable.ClearTextPrefix, + TwoFactorRememberTokenable.DataProtectorPurpose, + serviceProvider.GetDataProtectionProvider(), + serviceProvider.GetRequiredService>>())); services.AddSingleton>(serviceProvider => new DataProtectorTokenFactory( diff --git a/test/Core.Test/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableTests.cs b/test/Core.Test/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableTests.cs new file mode 100644 index 000000000000..63712510a434 --- /dev/null +++ b/test/Core.Test/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableTests.cs @@ -0,0 +1,167 @@ +using AutoFixture; +using AutoFixture.Xunit2; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Tokens; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Logging; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Auth.Models.Business.Tokenables; + +// Note: test names follow MethodName_StateUnderTest_ExpectedBehavior pattern. +public class TwoFactorRememberTokenableTests +{ + private static DataProtectorTokenFactory GetSigningFactory() + { + var fixture = new Fixture(); + return new DataProtectorTokenFactory( + TwoFactorRememberTokenable.ClearTextPrefix, + TwoFactorRememberTokenable.DataProtectorPurpose, + fixture.Create(), + Substitute.For>>()); + } + + private static TwoFactorRememberTokenable NewTokenable( + Guid? userId = null, + Guid? deviceId = null, + string deviceIdentifier = "device-identifier", + string stamp = "row-stamp", + string securityStamp = "user-security-stamp") => + new() + { + UserId = userId ?? Guid.NewGuid(), + DeviceId = deviceId ?? Guid.NewGuid(), + DeviceIdentifier = deviceIdentifier, + Stamp = stamp, + SecurityStamp = securityStamp, + }; + + /// + /// A freshly constructed token must be valid. If ExpirationDate were left at its default, + /// every token would be unusable the instant it was minted, which presents as a validation + /// problem everywhere downstream rather than as a problem with this class. + /// + [Fact] + public void Constructor_AfterInitialization_TokenIsValid() + { + var token = NewTokenable(); + + Assert.False(token.IsExpired); + Assert.True(token.Valid); + } + + [Fact] + public void Constructor_AfterInitialization_ExpirationSetToExpectedDuration() + { + var before = DateTime.UtcNow; + var token = new TwoFactorRememberTokenable(); + var after = DateTime.UtcNow; + + Assert.InRange( + token.ExpirationDate, + before + TwoFactorRememberTokenable.GetTokenLifetime(), + after + TwoFactorRememberTokenable.GetTokenLifetime()); + } + + /// + /// Deserialization runs the constructor first and then overwrites ExpirationDate from the + /// payload, which is what stops a round trip from granting a fresh lifetime. + /// + [Fact] + public void ProtectUnprotect_ValidToken_PreservesDataAndExpiration() + { + var factory = GetSigningFactory(); + var token = NewTokenable(); + var originalExpiration = token.ExpirationDate; + + var recovered = factory.Unprotect(factory.Protect(token)); + + Assert.Equal(token.UserId, recovered.UserId); + Assert.Equal(token.DeviceId, recovered.DeviceId); + Assert.Equal(token.DeviceIdentifier, recovered.DeviceIdentifier); + Assert.Equal(token.Stamp, recovered.Stamp); + Assert.Equal(token.SecurityStamp, recovered.SecurityStamp); + Assert.Equal(TwoFactorRememberTokenable.TokenIdentifier, recovered.Identifier); + Assert.Equal(originalExpiration, recovered.ExpirationDate, TimeSpan.FromSeconds(1)); + Assert.True(recovered.Valid); + } + + [Fact] + public void Protect_Always_AppliesClearTextPrefix() + { + var factory = GetSigningFactory(); + + var protectedToken = factory.Protect(NewTokenable()); + + Assert.StartsWith(TwoFactorRememberTokenable.ClearTextPrefix, protectedToken); + } + + [Fact] + public void Valid_Expired_ReturnsFalse() + { + var token = NewTokenable(); + token.ExpirationDate = DateTime.UtcNow.AddMinutes(-1); + + Assert.True(token.IsExpired); + Assert.False(token.Valid); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void Valid_MissingDeviceIdentifier_ReturnsFalse(string? deviceIdentifier) + { + var token = NewTokenable(deviceIdentifier: deviceIdentifier!); + + Assert.False(token.Valid); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void Valid_MissingStamp_ReturnsFalse(string? stamp) + { + var token = NewTokenable(stamp: stamp!); + + Assert.False(token.Valid); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData(null)] + public void Valid_MissingSecurityStamp_ReturnsFalse(string? securityStamp) + { + var token = NewTokenable(securityStamp: securityStamp!); + + Assert.False(token.Valid); + } + + [Fact] + public void Valid_DefaultUserId_ReturnsFalse() + { + var token = NewTokenable(userId: default(Guid)); + + Assert.False(token.Valid); + } + + [Fact] + public void Valid_DefaultDeviceId_ReturnsFalse() + { + var token = NewTokenable(deviceId: default(Guid)); + + Assert.False(token.Valid); + } + + [Theory, AutoData] + public void Valid_WrongIdentifier_ReturnsFalse(string identifier) + { + var token = NewTokenable(); + token.Identifier = identifier; + + Assert.False(token.Valid); + } +} From 231a89c7d0624b71b7422c9e94f2c53679331f4a Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:18:26 -0500 Subject: [PATCH 05/27] [PM-40673 Part 1] add commands for issue/revoke, and query for validating tokens --- .../IIssueTwoFactorRememberTokenCommand.cs | 17 ++ .../IRevokeTwoFactorRememberTokensCommand.cs | 14 ++ .../IValidateTwoFactorRememberTokenQuery.cs | 22 ++ .../IssueTwoFactorRememberTokenCommand.cs | 44 ++++ .../RevokeTwoFactorRememberTokensCommand.cs | 11 + .../ValidateTwoFactorRememberTokenQuery.cs | 68 +++++ .../UserServiceCollectionExtensions.cs | 3 + ...IssueTwoFactorRememberTokenCommandTests.cs | 130 ++++++++++ ...vokeTwoFactorRememberTokensCommandTests.cs | 57 +++++ ...alidateTwoFactorRememberTokenQueryTests.cs | 232 ++++++++++++++++++ 10 files changed, 598 insertions(+) create mode 100644 src/Core/Auth/UserFeatures/TwoFactorAuth/IIssueTwoFactorRememberTokenCommand.cs create mode 100644 src/Core/Auth/UserFeatures/TwoFactorAuth/IRevokeTwoFactorRememberTokensCommand.cs create mode 100644 src/Core/Auth/UserFeatures/TwoFactorAuth/IValidateTwoFactorRememberTokenQuery.cs create mode 100644 src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/IssueTwoFactorRememberTokenCommand.cs create mode 100644 src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/RevokeTwoFactorRememberTokensCommand.cs create mode 100644 src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs create mode 100644 test/Core.Test/Auth/UserFeatures/TwoFactorAuth/IssueTwoFactorRememberTokenCommandTests.cs create mode 100644 test/Core.Test/Auth/UserFeatures/TwoFactorAuth/RevokeTwoFactorRememberTokensCommandTests.cs create mode 100644 test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/IIssueTwoFactorRememberTokenCommand.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/IIssueTwoFactorRememberTokenCommand.cs new file mode 100644 index 000000000000..a5085de5a48c --- /dev/null +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/IIssueTwoFactorRememberTokenCommand.cs @@ -0,0 +1,17 @@ +using Bit.Core.Entities; + +namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth; + +public interface IIssueTwoFactorRememberTokenCommand +{ + /// + /// Records that this device is remembered and mints the token the client presents to skip the + /// two-factor challenge on its next login. + /// + /// + /// Writing the row assigns it a fresh stamp, so any token previously issued for this device + /// stops being honored — one live remember token per device, newest wins. + /// + /// The protected token string, to be returned to the client verbatim. + Task IssueAsync(User user, Device device); +} diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/IRevokeTwoFactorRememberTokensCommand.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/IRevokeTwoFactorRememberTokensCommand.cs new file mode 100644 index 000000000000..70b1fe653ba1 --- /dev/null +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/IRevokeTwoFactorRememberTokensCommand.cs @@ -0,0 +1,14 @@ +namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth; + +public interface IRevokeTwoFactorRememberTokensCommand +{ + /// + /// Stops every remember token this user holds from being honored, on all of their devices. + /// + /// + /// Scoped to remember-me only: access tokens, refresh tokens, and live sessions are unaffected, + /// so this does not log the user out anywhere. Devices stay remembered as rows — they simply + /// require a two-factor challenge again on next login. + /// + Task RevokeAllForUserAsync(Guid userId); +} diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/IValidateTwoFactorRememberTokenQuery.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/IValidateTwoFactorRememberTokenQuery.cs new file mode 100644 index 000000000000..2a190d9346fd --- /dev/null +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/IValidateTwoFactorRememberTokenQuery.cs @@ -0,0 +1,22 @@ +using Bit.Core.Entities; + +namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth; + +public interface IValidateTwoFactorRememberTokenQuery +{ + /// + /// Decides whether a presented remember token entitles this request to skip the two-factor + /// challenge. + /// + /// + /// A pure read: nothing here writes. If a last-used timestamp is ever wanted, it belongs in its + /// own command rather than folded in here. + /// + /// The authenticating user. + /// + /// The client-generated device identifier from the current request. The device row itself is not + /// available at this point in the login pipeline, which is why the identifier is passed in. + /// + /// The protected token string as presented by the client. + Task ValidateAsync(User user, string deviceIdentifier, string token); +} diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/IssueTwoFactorRememberTokenCommand.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/IssueTwoFactorRememberTokenCommand.cs new file mode 100644 index 000000000000..634702a62423 --- /dev/null +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/IssueTwoFactorRememberTokenCommand.cs @@ -0,0 +1,44 @@ +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.Repositories; +using Bit.Core.Entities; +using Bit.Core.Tokens; + +namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations; + +public class IssueTwoFactorRememberTokenCommand( + ITwoFactorRememberTokenRepository twoFactorRememberTokenRepository, + IDataProtectorTokenFactory tokenFactory, + TimeProvider timeProvider) : IIssueTwoFactorRememberTokenCommand +{ + public async Task IssueAsync(User user, Device device) + { + var now = timeProvider.GetUtcNow().UtcDateTime; + var expirationDate = now.Add(TwoFactorRememberTokenable.GetTokenLifetime()); + + // Generated here rather than by the repository because the value has to go into the token + // as well as the row. Guid.NewGuid rather than a comb: this is an opaque comparand, not a + // table identifier, and it should carry no embedded timestamp. + var stamp = Guid.NewGuid().ToString(); + + var row = await twoFactorRememberTokenRepository.UpsertAsync(new TwoFactorRememberToken + { + UserId = user.Id, + DeviceId = device.Id, + Stamp = stamp, + CreationDate = now, + RevisionDate = now, + ExpirationDate = expirationDate, + }); + + return tokenFactory.Protect(new TwoFactorRememberTokenable + { + UserId = user.Id, + DeviceId = device.Id, + DeviceIdentifier = device.Identifier, + Stamp = row.Stamp, + SecurityStamp = user.SecurityStamp, + ExpirationDate = row.ExpirationDate, + }); + } +} diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/RevokeTwoFactorRememberTokensCommand.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/RevokeTwoFactorRememberTokensCommand.cs new file mode 100644 index 000000000000..2efcc929be12 --- /dev/null +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/RevokeTwoFactorRememberTokensCommand.cs @@ -0,0 +1,11 @@ +using Bit.Core.Auth.Repositories; + +namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations; + +public class RevokeTwoFactorRememberTokensCommand( + ITwoFactorRememberTokenRepository twoFactorRememberTokenRepository) + : IRevokeTwoFactorRememberTokensCommand +{ + public Task RevokeAllForUserAsync(Guid userId) => + twoFactorRememberTokenRepository.RotateStampsByUserIdAsync(userId); +} diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs new file mode 100644 index 000000000000..ac9b69073655 --- /dev/null +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs @@ -0,0 +1,68 @@ +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.Repositories; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; +using Bit.Core.Entities; +using Bit.Core.Tokens; + +namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations; + +public class ValidateTwoFactorRememberTokenQuery( + ITwoFactorRememberTokenRepository twoFactorRememberTokenRepository, + IDataProtectorTokenFactory tokenFactory, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, + TimeProvider timeProvider) : IValidateTwoFactorRememberTokenQuery +{ + public async Task ValidateAsync(User user, string deviceIdentifier, string token) + { + // Covers tampering, a wrong data-protection purpose, the token's own expiry, and the + // presence checks on its fields. + if (!tokenFactory.TryUnprotect(token, out var tokenable) || tokenable is null || !tokenable.Valid) + { + return false; + } + + if (tokenable.UserId != user.Id) + { + return false; + } + + // Account-scoped: a security stamp rotation invalidates every token carrying the old value. + if (!string.Equals(tokenable.SecurityStamp, user.SecurityStamp, StringComparison.Ordinal)) + { + return false; + } + + // Device binding. Case-insensitive because Device_ReadByIdentifierUserId matches the same + // value through a case-insensitive collation, and a stricter comparison here would reject a + // token whose identifier still resolves to the same device row. + if (!string.Equals(tokenable.DeviceIdentifier, deviceIdentifier, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // A remember token stands in for a second factor, so it cannot be honored by an account that + // has no second factor configured. Ordered ahead of the row read because a user with no + // enabled providers resolves without a database call. + if (!await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user)) + { + return false; + } + + // Safe to locate the row by the token's own DeviceId only because the identifier comparison + // above already proved the presenter is on the device the token names. Do not reorder. + var row = await twoFactorRememberTokenRepository.GetByUserIdDeviceIdAsync(user.Id, tokenable.DeviceId); + if (row is null) + { + return false; + } + + // Device-scoped: rotating the row's stamp cuts off this device without affecting the user's + // other devices or any of their sessions. + if (!string.Equals(tokenable.Stamp, row.Stamp, StringComparison.Ordinal)) + { + return false; + } + + return row.ExpirationDate > timeProvider.GetUtcNow().UtcDateTime; + } +} diff --git a/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs b/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs index dbe56bc0b46c..5e04efeb3fcd 100644 --- a/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs +++ b/src/Core/Auth/UserFeatures/UserServiceCollectionExtensions.cs @@ -105,6 +105,9 @@ private static void AddTwoFactorCommandsQueries(this IServiceCollection services services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); } } diff --git a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/IssueTwoFactorRememberTokenCommandTests.cs b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/IssueTwoFactorRememberTokenCommandTests.cs new file mode 100644 index 000000000000..18eabccfcc0c --- /dev/null +++ b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/IssueTwoFactorRememberTokenCommandTests.cs @@ -0,0 +1,130 @@ +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.Repositories; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations; +using Bit.Core.Entities; +using Bit.Core.Tokens; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Auth.UserFeatures.TwoFactorAuth; + +[SutProviderCustomize] +public class IssueTwoFactorRememberTokenCommandTests +{ + /// + /// U1 — the stamp written to the row is the stamp embedded in the token, or validation could + /// never succeed. + /// + [Theory, BitAutoData] + public async Task IssueAsync_UpsertsRowWithStampMatchingToken( + SutProvider sutProvider, + User user, + Device device) + { + device.UserId = user.Id; + TwoFactorRememberToken? saved = null; + sutProvider.GetDependency() + .UpsertAsync(Arg.Do(t => saved = t)) + .Returns(c => c.Arg()); + + TwoFactorRememberTokenable? minted = null; + sutProvider.GetDependency>() + .Protect(Arg.Do(t => minted = t)) + .Returns("protected-token"); + + var result = await sutProvider.Sut.IssueAsync(user, device); + + Assert.Equal("protected-token", result); + Assert.NotNull(saved); + Assert.NotNull(minted); + Assert.Equal(user.Id, saved.UserId); + Assert.Equal(device.Id, saved.DeviceId); + Assert.Equal(saved.Stamp, minted.Stamp); + Assert.Equal(user.Id, minted.UserId); + Assert.Equal(device.Id, minted.DeviceId); + Assert.Equal(device.Identifier, minted.DeviceIdentifier); + Assert.Equal(user.SecurityStamp, minted.SecurityStamp); + } + + /// + /// U2 — re-remembering a device writes a different stamp, which is what orphans the token that + /// device was holding. + /// + [Theory, BitAutoData] + public async Task IssueAsync_CalledTwice_WritesDifferentStampEachTime( + SutProvider sutProvider, + User user, + Device device) + { + device.UserId = user.Id; + var stamps = new List(); + sutProvider.GetDependency() + .UpsertAsync(Arg.Do(t => stamps.Add(t.Stamp))) + .Returns(c => c.Arg()); + + await sutProvider.Sut.IssueAsync(user, device); + await sutProvider.Sut.IssueAsync(user, device); + + Assert.Equal(2, stamps.Count); + Assert.NotEqual(stamps[0], stamps[1]); + } + + /// + /// U3 — the row and the token expire together, both derived from the one lifetime constant. + /// + [Theory, BitAutoData] + public async Task IssueAsync_RowAndTokenShareExpiration( + SutProvider sutProvider, + User user, + Device device) + { + device.UserId = user.Id; + TwoFactorRememberToken? saved = null; + sutProvider.GetDependency() + .UpsertAsync(Arg.Do(t => saved = t)) + .Returns(c => c.Arg()); + + TwoFactorRememberTokenable? minted = null; + sutProvider.GetDependency>() + .Protect(Arg.Do(t => minted = t)) + .Returns("protected-token"); + + var before = DateTime.UtcNow; + await sutProvider.Sut.IssueAsync(user, device); + var after = DateTime.UtcNow; + + Assert.NotNull(saved); + Assert.NotNull(minted); + Assert.InRange( + saved.ExpirationDate, + before + TwoFactorRememberTokenable.GetTokenLifetime(), + after + TwoFactorRememberTokenable.GetTokenLifetime()); + Assert.Equal(saved.ExpirationDate, minted.ExpirationDate, TimeSpan.FromSeconds(1)); + } + + /// + /// Stamps are lower-case GUID text on every write path, which is what makes the ordinal, + /// case-sensitive comparison at validation time safe. + /// + [Theory, BitAutoData] + public async Task IssueAsync_StampIsLowerCaseGuidText( + SutProvider sutProvider, + User user, + Device device) + { + device.UserId = user.Id; + TwoFactorRememberToken? saved = null; + sutProvider.GetDependency() + .UpsertAsync(Arg.Do(t => saved = t)) + .Returns(c => c.Arg()); + + await sutProvider.Sut.IssueAsync(user, device); + + Assert.NotNull(saved); + Assert.True(Guid.TryParse(saved.Stamp, out _)); + Assert.Equal(saved.Stamp.ToLowerInvariant(), saved.Stamp); + } +} diff --git a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/RevokeTwoFactorRememberTokensCommandTests.cs b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/RevokeTwoFactorRememberTokensCommandTests.cs new file mode 100644 index 000000000000..2c2b2101f2d9 --- /dev/null +++ b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/RevokeTwoFactorRememberTokensCommandTests.cs @@ -0,0 +1,57 @@ +using Bit.Core.Auth.Repositories; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Auth.UserFeatures.TwoFactorAuth; + +[SutProviderCustomize] +public class RevokeTwoFactorRememberTokensCommandTests +{ + [Theory, BitAutoData] + public async Task RevokeAllForUserAsync_RotatesStampsForThatUser( + SutProvider sutProvider, + Guid userId) + { + await sutProvider.Sut.RevokeAllForUserAsync(userId); + + await sutProvider.GetDependency() + .Received(1) + .RotateStampsByUserIdAsync(userId); + } + + /// + /// Revocation is scoped to remember-me. It must not delete rows, which would destroy the record + /// of when each device was first remembered. + /// + [Theory, BitAutoData] + public async Task RevokeAllForUserAsync_DoesNotDeleteRows( + SutProvider sutProvider, + Guid userId) + { + await sutProvider.Sut.RevokeAllForUserAsync(userId); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteExpiredAsync(default); + } + + /// + /// Failures propagate. Call sites rely on this: they revoke before the write that commits a + /// teardown so that a failure leaves a state the caller can retry. + /// + [Theory, BitAutoData] + public async Task RevokeAllForUserAsync_RepositoryThrows_Propagates( + SutProvider sutProvider, + Guid userId) + { + sutProvider.GetDependency() + .RotateStampsByUserIdAsync(userId) + .Returns(Task.FromException(new InvalidOperationException("database unavailable"))); + + await Assert.ThrowsAsync( + () => sutProvider.Sut.RevokeAllForUserAsync(userId)); + } +} diff --git a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs new file mode 100644 index 000000000000..548b85323c9d --- /dev/null +++ b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs @@ -0,0 +1,232 @@ +using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.Repositories; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations; +using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; +using Bit.Core.Entities; +using Bit.Core.Tokens; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Auth.UserFeatures.TwoFactorAuth; + +/// +/// One test per rejection path. Each is asserted in isolation — a test that broke two checks at once +/// would still pass if one of them were dropped from the implementation. +/// +[SutProviderCustomize] +public class ValidateTwoFactorRememberTokenQueryTests +{ + private const string _deviceIdentifier = "device-identifier"; + private const string _token = "protected-token"; + + private static TwoFactorRememberTokenable Tokenable(User user, Guid deviceId, string stamp) => + new() + { + UserId = user.Id, + DeviceId = deviceId, + DeviceIdentifier = _deviceIdentifier, + Stamp = stamp, + SecurityStamp = user.SecurityStamp, + }; + + private static TwoFactorRememberToken Row(User user, Guid deviceId, string stamp) => + new() + { + Id = Guid.NewGuid(), + UserId = user.Id, + DeviceId = deviceId, + Stamp = stamp, + ExpirationDate = DateTime.UtcNow.AddDays(1), + }; + + /// + /// Wires the happy path: token unprotects, 2FA is on, and the row exists with a matching stamp. + /// Individual tests then break exactly one of those. + /// + private static (TwoFactorRememberTokenable tokenable, TwoFactorRememberToken row) ArrangeValid( + SutProvider sutProvider, User user, Guid deviceId) + { + const string stamp = "row-stamp"; + var tokenable = Tokenable(user, deviceId, stamp); + var row = Row(user, deviceId, stamp); + + sutProvider.GetDependency>() + .TryUnprotect(_token, out Arg.Any()) + .Returns(c => { c[1] = tokenable; return true; }); + + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(user) + .Returns(true); + + sutProvider.GetDependency() + .GetByUserIdDeviceIdAsync(user.Id, deviceId) + .Returns(row); + + return (tokenable, row); + } + + /// U4 — the happy path. + [Theory, BitAutoData] + public async Task ValidateAsync_EverythingMatches_ReturnsTrue( + SutProvider sutProvider, User user, Guid deviceId) + { + ArrangeValid(sutProvider, user, deviceId); + + Assert.True(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); + } + + /// U5 — the targeted revocation: the device's row stamp has been rotated. + [Theory, BitAutoData] + public async Task ValidateAsync_RowStampRotated_ReturnsFalse( + SutProvider sutProvider, User user, Guid deviceId) + { + var (_, row) = ArrangeValid(sutProvider, user, deviceId); + row.Stamp = "rotated-stamp"; + + Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); + } + + /// U6 — the account-wide check inherited from the previous design. + [Theory, BitAutoData] + public async Task ValidateAsync_UserSecurityStampRotated_ReturnsFalse( + SutProvider sutProvider, User user, Guid deviceId) + { + var (tokenable, _) = ArrangeValid(sutProvider, user, deviceId); + tokenable.SecurityStamp = "a-different-security-stamp"; + + Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); + } + + /// + /// U7 — a remember token cannot stand in as a second factor for an account that currently has no + /// second factor configured. + /// + [Theory, BitAutoData] + public async Task ValidateAsync_UserHasNoTwoFactorEnabled_ReturnsFalse( + SutProvider sutProvider, User user, Guid deviceId) + { + ArrangeValid(sutProvider, user, deviceId); + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(user) + .Returns(false); + + Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); + } + + /// + /// The 2FA-enabled check is ordered ahead of the row read, so the common teardown case rejects + /// without touching the table. + /// + [Theory, BitAutoData] + public async Task ValidateAsync_UserHasNoTwoFactorEnabled_DoesNotReadRow( + SutProvider sutProvider, User user, Guid deviceId) + { + ArrangeValid(sutProvider, user, deviceId); + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(user) + .Returns(false); + + await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .GetByUserIdDeviceIdAsync(default, default); + } + + /// U8 — a token issued to a different user. + [Theory, BitAutoData] + public async Task ValidateAsync_UserIdMismatch_ReturnsFalse( + SutProvider sutProvider, User user, Guid deviceId) + { + var (tokenable, _) = ArrangeValid(sutProvider, user, deviceId); + tokenable.UserId = Guid.NewGuid(); + + Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); + } + + /// U9 — the device binding. + [Theory, BitAutoData] + public async Task ValidateAsync_DeviceIdentifierMismatch_ReturnsFalse( + SutProvider sutProvider, User user, Guid deviceId) + { + ArrangeValid(sutProvider, user, deviceId); + + Assert.False(await sutProvider.Sut.ValidateAsync(user, "a-different-device", _token)); + } + + /// + /// U9 — identifiers are matched the way SQL collation matches them, so a case difference must + /// not cause a spurious challenge. + /// + [Theory, BitAutoData] + public async Task ValidateAsync_DeviceIdentifierDiffersOnlyByCase_ReturnsTrue( + SutProvider sutProvider, User user, Guid deviceId) + { + ArrangeValid(sutProvider, user, deviceId); + + Assert.True(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier.ToUpperInvariant(), _token)); + } + + /// U10 — no row for this device. + [Theory, BitAutoData] + public async Task ValidateAsync_RowMissing_ReturnsFalse( + SutProvider sutProvider, User user, Guid deviceId) + { + ArrangeValid(sutProvider, user, deviceId); + sutProvider.GetDependency() + .GetByUserIdDeviceIdAsync(user.Id, deviceId) + .Returns((TwoFactorRememberToken?)null); + + Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); + } + + /// U11 — the row outlived its expiry but the sweep has not reached it. + [Theory, BitAutoData] + public async Task ValidateAsync_RowExpired_ReturnsFalse( + SutProvider sutProvider, User user, Guid deviceId) + { + var (_, row) = ArrangeValid(sutProvider, user, deviceId); + row.ExpirationDate = DateTime.UtcNow.AddMinutes(-1); + + Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_TokenCannotBeUnprotected_ReturnsFalse( + SutProvider sutProvider, User user) + { + sutProvider.GetDependency>() + .TryUnprotect(_token, out Arg.Any()) + .Returns(c => { c[1] = null!; return false; }); + + Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_TokenItselfExpired_ReturnsFalse( + SutProvider sutProvider, User user, Guid deviceId) + { + var (tokenable, _) = ArrangeValid(sutProvider, user, deviceId); + tokenable.ExpirationDate = DateTime.UtcNow.AddMinutes(-1); + + Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); + } + + /// Validation reads; it must never write. + [Theory, BitAutoData] + public async Task ValidateAsync_Always_DoesNotWrite( + SutProvider sutProvider, User user, Guid deviceId) + { + ArrangeValid(sutProvider, user, deviceId); + + await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token); + + var repository = sutProvider.GetDependency(); + await repository.DidNotReceiveWithAnyArgs().UpsertAsync(default!); + await repository.DidNotReceiveWithAnyArgs().RotateStampsByUserIdAsync(default); + await repository.DidNotReceiveWithAnyArgs().DeleteExpiredAsync(default); + } +} From 7dda6bf94b2eb584a0a69dffe06f522cd8621cf5 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Tue, 22 Sep 2026 20:22:22 -0500 Subject: [PATCH 06/27] [PM-40673 Part 1] add DeleteExpiredTwoFactorRememberTokensJob.cs --- ...DeleteExpiredTwoFactorRememberTokensJob.cs | 36 +++++++++++++ src/Admin/Jobs/JobsHostedService.cs | 2 + ...eExpiredTwoFactorRememberTokensJobTests.cs | 52 +++++++++++++++++++ 3 files changed, 90 insertions(+) create mode 100644 src/Admin/Auth/Jobs/DeleteExpiredTwoFactorRememberTokensJob.cs create mode 100644 test/Admin.Test/Jobs/DeleteExpiredTwoFactorRememberTokensJobTests.cs diff --git a/src/Admin/Auth/Jobs/DeleteExpiredTwoFactorRememberTokensJob.cs b/src/Admin/Auth/Jobs/DeleteExpiredTwoFactorRememberTokensJob.cs new file mode 100644 index 000000000000..246e75a3c2c2 --- /dev/null +++ b/src/Admin/Auth/Jobs/DeleteExpiredTwoFactorRememberTokensJob.cs @@ -0,0 +1,36 @@ +using Bit.Core; +using Bit.Core.Auth.Repositories; +using Bit.Core.Jobs; +using Quartz; + +namespace Bit.Admin.Auth.Jobs; + +/// +/// Clears out remember-token rows whose expiry has passed. Hygiene rather than correctness — an +/// expired row is refused at validation time regardless, the row count is bounded by the number of +/// devices, and the expiry index makes the sweep cheap. Weekly is sufficient. +/// +public class DeleteExpiredTwoFactorRememberTokensJob : BaseJob +{ + private readonly ITwoFactorRememberTokenRepository _twoFactorRememberTokenRepository; + private readonly TimeProvider _timeProvider; + + public DeleteExpiredTwoFactorRememberTokensJob( + ITwoFactorRememberTokenRepository twoFactorRememberTokenRepository, + TimeProvider timeProvider, + ILogger logger) + : base(logger) + { + _twoFactorRememberTokenRepository = twoFactorRememberTokenRepository; + _timeProvider = timeProvider; + } + + protected async override Task ExecuteJobAsync(IJobExecutionContext context) + { + _logger.LogInformation( + Constants.BypassFiltersEventId, "Execute job task: DeleteExpiredTwoFactorRememberTokensAsync"); + await _twoFactorRememberTokenRepository.DeleteExpiredAsync(_timeProvider.GetUtcNow().UtcDateTime); + _logger.LogInformation( + Constants.BypassFiltersEventId, "Finished job task: DeleteExpiredTwoFactorRememberTokensAsync"); + } +} diff --git a/src/Admin/Jobs/JobsHostedService.cs b/src/Admin/Jobs/JobsHostedService.cs index 23c8e6d4ab77..864e2cb19319 100644 --- a/src/Admin/Jobs/JobsHostedService.cs +++ b/src/Admin/Jobs/JobsHostedService.cs @@ -88,6 +88,7 @@ public override async Task StartAsync(CancellationToken cancellationToken) { new Tuple(typeof(DeleteSendsJob), everyFiveMinutesTrigger), new Tuple(typeof(DatabaseExpiredGrantsJob), everyFridayAt10pmTrigger), + new Tuple(typeof(DeleteExpiredTwoFactorRememberTokensJob), everyFridayAt10pmTrigger), new Tuple(typeof(DeleteCiphersJob), everyDayAtMidnightUtc), new Tuple(typeof(DatabaseExpiredSponsorshipsJob), everyMondayAtMidnightTrigger), new Tuple(typeof(DeleteAuthRequestsJob), everyFifteenMinutesTrigger), @@ -122,6 +123,7 @@ public static void AddJobsServices(IServiceCollection services, bool selfHosted) services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/test/Admin.Test/Jobs/DeleteExpiredTwoFactorRememberTokensJobTests.cs b/test/Admin.Test/Jobs/DeleteExpiredTwoFactorRememberTokensJobTests.cs new file mode 100644 index 000000000000..b3f761d0ea6e --- /dev/null +++ b/test/Admin.Test/Jobs/DeleteExpiredTwoFactorRememberTokensJobTests.cs @@ -0,0 +1,52 @@ +using Bit.Admin.Auth.Jobs; +using Bit.Core.Auth.Repositories; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using Quartz; + +namespace Admin.Test.Jobs; + +public class DeleteExpiredTwoFactorRememberTokensJobTests +{ + private readonly ITwoFactorRememberTokenRepository _repository; + private readonly FakeTimeProvider _timeProvider; + private readonly ILogger _logger; + private readonly DeleteExpiredTwoFactorRememberTokensJob _sut; + + public DeleteExpiredTwoFactorRememberTokensJobTests() + { + _repository = Substitute.For(); + _timeProvider = new FakeTimeProvider(new DateTimeOffset(2026, 9, 22, 22, 0, 0, TimeSpan.Zero)); + _logger = Substitute.For>(); + _sut = new DeleteExpiredTwoFactorRememberTokensJob(_repository, _timeProvider, _logger); + } + + [Fact] + public async Task Execute_Always_SweepsExpiredRows() + { + await _sut.Execute(CreateContext()); + + await _repository.Received(1).DeleteExpiredAsync(Arg.Any()); + } + + /// + /// The job owns the clock reading and passes it down, so the procedure and every Entity + /// Framework implementation compare against the same instant. + /// + [Fact] + public async Task Execute_Always_PassesItsOwnClockReading() + { + await _sut.Execute(CreateContext()); + + await _repository.Received(1) + .DeleteExpiredAsync(_timeProvider.GetUtcNow().UtcDateTime); + } + + private static IJobExecutionContext CreateContext(CancellationToken cancellationToken = default) + { + var context = Substitute.For(); + context.CancellationToken.Returns(cancellationToken); + return context; + } +} From d56f90b5eafc7e2488a0692707156aeda851c9e7 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Mon, 28 Sep 2026 11:10:30 -0500 Subject: [PATCH 07/27] [PM-40673 Part 1] link TODO comment to specific Jira ticket --- .../Models/Business/Tokenables/TwoFactorRememberTokenable.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenable.cs b/src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenable.cs index b803afdaf6f4..9110a5281a2d 100644 --- a/src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenable.cs +++ b/src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenable.cs @@ -43,7 +43,7 @@ public class TwoFactorRememberTokenable : ExpiringTokenable /// public string Stamp { get; set; } = null!; - // TODO: PM-XXXXX - Consider removing SecurityStamp from this token. The row's Stamp already + // TODO: PM-44171 - Consider removing SecurityStamp from this token. The row's Stamp already // covers the same ground more precisely, and carrying the user stamp ties remember-me lifetime // to every user stamp rotation. Any removal has to stay backwards compatible: tokens already // issued carry this field, so treat it as optional on read before dropping it. From 99e9ddc62f057ad0e61e89361c174b4d93fa0cb3 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Fri, 2 Oct 2026 09:51:22 -0500 Subject: [PATCH 08/27] [PM-40673 Part 1] remove prefixes from test comments --- .../IssueTwoFactorRememberTokenCommandTests.cs | 6 +++--- ...ValidateTwoFactorRememberTokenQueryTests.cs | 18 +++++++++--------- .../TwoFactorRememberTokenRepositoryTests.cs | 14 +++++++------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/IssueTwoFactorRememberTokenCommandTests.cs b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/IssueTwoFactorRememberTokenCommandTests.cs index 18eabccfcc0c..14dba3b8c7da 100644 --- a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/IssueTwoFactorRememberTokenCommandTests.cs +++ b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/IssueTwoFactorRememberTokenCommandTests.cs @@ -15,7 +15,7 @@ namespace Bit.Core.Test.Auth.UserFeatures.TwoFactorAuth; public class IssueTwoFactorRememberTokenCommandTests { /// - /// U1 — the stamp written to the row is the stamp embedded in the token, or validation could + /// The stamp written to the row is the stamp embedded in the token, or validation could /// never succeed. /// [Theory, BitAutoData] @@ -50,7 +50,7 @@ public async Task IssueAsync_UpsertsRowWithStampMatchingToken( } /// - /// U2 — re-remembering a device writes a different stamp, which is what orphans the token that + /// Re-remembering a device writes a different stamp, which is what orphans the token that /// device was holding. /// [Theory, BitAutoData] @@ -73,7 +73,7 @@ public async Task IssueAsync_CalledTwice_WritesDifferentStampEachTime( } /// - /// U3 — the row and the token expire together, both derived from the one lifetime constant. + /// The row and the token expire together, both derived from the one lifetime constant. /// [Theory, BitAutoData] public async Task IssueAsync_RowAndTokenShareExpiration( diff --git a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs index 548b85323c9d..7276bfb64519 100644 --- a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs +++ b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs @@ -68,7 +68,7 @@ private static (TwoFactorRememberTokenable tokenable, TwoFactorRememberToken row return (tokenable, row); } - /// U4 — the happy path. + /// The happy path. [Theory, BitAutoData] public async Task ValidateAsync_EverythingMatches_ReturnsTrue( SutProvider sutProvider, User user, Guid deviceId) @@ -78,7 +78,7 @@ public async Task ValidateAsync_EverythingMatches_ReturnsTrue( Assert.True(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); } - /// U5 — the targeted revocation: the device's row stamp has been rotated. + /// The targeted revocation: the device's row stamp has been rotated. [Theory, BitAutoData] public async Task ValidateAsync_RowStampRotated_ReturnsFalse( SutProvider sutProvider, User user, Guid deviceId) @@ -89,7 +89,7 @@ public async Task ValidateAsync_RowStampRotated_ReturnsFalse( Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); } - /// U6 — the account-wide check inherited from the previous design. + /// The account-wide check inherited from the previous design. [Theory, BitAutoData] public async Task ValidateAsync_UserSecurityStampRotated_ReturnsFalse( SutProvider sutProvider, User user, Guid deviceId) @@ -101,7 +101,7 @@ public async Task ValidateAsync_UserSecurityStampRotated_ReturnsFalse( } /// - /// U7 — a remember token cannot stand in as a second factor for an account that currently has no + /// A remember token cannot stand in as a second factor for an account that currently has no /// second factor configured. /// [Theory, BitAutoData] @@ -136,7 +136,7 @@ await sutProvider.GetDependency() .GetByUserIdDeviceIdAsync(default, default); } - /// U8 — a token issued to a different user. + /// A token issued to a different user. [Theory, BitAutoData] public async Task ValidateAsync_UserIdMismatch_ReturnsFalse( SutProvider sutProvider, User user, Guid deviceId) @@ -147,7 +147,7 @@ public async Task ValidateAsync_UserIdMismatch_ReturnsFalse( Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); } - /// U9 — the device binding. + /// The device binding. [Theory, BitAutoData] public async Task ValidateAsync_DeviceIdentifierMismatch_ReturnsFalse( SutProvider sutProvider, User user, Guid deviceId) @@ -158,7 +158,7 @@ public async Task ValidateAsync_DeviceIdentifierMismatch_ReturnsFalse( } /// - /// U9 — identifiers are matched the way SQL collation matches them, so a case difference must + /// Identifiers are matched the way SQL collation matches them, so a case difference must /// not cause a spurious challenge. /// [Theory, BitAutoData] @@ -170,7 +170,7 @@ public async Task ValidateAsync_DeviceIdentifierDiffersOnlyByCase_ReturnsTrue( Assert.True(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier.ToUpperInvariant(), _token)); } - /// U10 — no row for this device. + /// No row for this device. [Theory, BitAutoData] public async Task ValidateAsync_RowMissing_ReturnsFalse( SutProvider sutProvider, User user, Guid deviceId) @@ -183,7 +183,7 @@ public async Task ValidateAsync_RowMissing_ReturnsFalse( Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); } - /// U11 — the row outlived its expiry but the sweep has not reached it. + /// The row outlived its expiry but the sweep has not reached it. [Theory, BitAutoData] public async Task ValidateAsync_RowExpired_ReturnsFalse( SutProvider sutProvider, User user, Guid deviceId) diff --git a/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs index c196cd6f3239..8251521d9c01 100644 --- a/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs @@ -73,7 +73,7 @@ private static async Task DeleteDeviceAtDatabaseLevelAsync( } // ------------------------------------------------------------------------------------------- - // R1 — upsert semantics + // Upsert semantics // ------------------------------------------------------------------------------------------- [Theory, DatabaseData] @@ -104,7 +104,7 @@ public async Task UpsertAsync_SameUserAndDevice_UpdatesInPlaceAndPreservesCreati } // ------------------------------------------------------------------------------------------- - // R2 — concurrent upsert + // Concurrent upsert // ------------------------------------------------------------------------------------------- /// @@ -136,7 +136,7 @@ await Task.WhenAll( } // ------------------------------------------------------------------------------------------- - // R3, R4 — rotation + // Rotation // ------------------------------------------------------------------------------------------- [Theory, DatabaseData] @@ -200,7 +200,7 @@ public async Task RotateStampsByUserIdAsync_LeavesOtherUsersAlone( } // ------------------------------------------------------------------------------------------- - // R5, R6 — cascade behavior + // Cascade behavior // ------------------------------------------------------------------------------------------- [Theory, DatabaseData] @@ -262,7 +262,7 @@ public async Task DeleteManyAsync_Users_RemovesTokenRows( } // ------------------------------------------------------------------------------------------- - // R7 — expiry sweep + // Expiry sweep // ------------------------------------------------------------------------------------------- [Theory, DatabaseData] @@ -289,7 +289,7 @@ public async Task DeleteExpiredAsync_RemovesOnlyExpiredRows( } // ------------------------------------------------------------------------------------------- - // R9 — the unique index exists on every provider + // The unique index exists on every provider // ------------------------------------------------------------------------------------------- /// @@ -352,7 +352,7 @@ INSERT INTO [dbo].[TwoFactorRememberToken] } // ------------------------------------------------------------------------------------------- - // R8 — storage fidelity + // Storage fidelity // ------------------------------------------------------------------------------------------- /// From 5614074da22253f0b767bcf05a5944efc5a76e04 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:07:12 -0500 Subject: [PATCH 09/27] [PM-40673 Part 1] rename stored procedure to _DeleteManyExpired --- .../Auth/Repositories/TwoFactorRememberTokenRepository.cs | 2 +- ...Expired.sql => TwoFactorRememberToken_DeleteManyExpired.sql} | 2 +- .../DbScripts/2026-09-22_00_CreateTwoFactorRememberToken.sql | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename src/Sql/dbo/Auth/Stored Procedures/{TwoFactorRememberToken_DeleteExpired.sql => TwoFactorRememberToken_DeleteManyExpired.sql} (88%) diff --git a/src/Infrastructure.Dapper/Auth/Repositories/TwoFactorRememberTokenRepository.cs b/src/Infrastructure.Dapper/Auth/Repositories/TwoFactorRememberTokenRepository.cs index 0f9031bbdbac..5d01a242a438 100644 --- a/src/Infrastructure.Dapper/Auth/Repositories/TwoFactorRememberTokenRepository.cs +++ b/src/Infrastructure.Dapper/Auth/Repositories/TwoFactorRememberTokenRepository.cs @@ -82,7 +82,7 @@ public async Task DeleteExpiredAsync(DateTime now) await using var connection = new SqlConnection(ConnectionString); await connection.ExecuteAsync( - "[dbo].[TwoFactorRememberToken_DeleteExpired]", + "[dbo].[TwoFactorRememberToken_DeleteManyExpired]", new { Now = now }, commandType: CommandType.StoredProcedure); } diff --git a/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_DeleteExpired.sql b/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_DeleteManyExpired.sql similarity index 88% rename from src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_DeleteExpired.sql rename to src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_DeleteManyExpired.sql index 6437ec29291f..df061e154249 100644 --- a/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_DeleteExpired.sql +++ b/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_DeleteManyExpired.sql @@ -1,4 +1,4 @@ -CREATE PROCEDURE [dbo].[TwoFactorRememberToken_DeleteExpired] +CREATE PROCEDURE [dbo].[TwoFactorRememberToken_DeleteManyExpired] @Now DATETIME2(7) AS BEGIN diff --git a/util/Migrator/DbScripts/2026-09-22_00_CreateTwoFactorRememberToken.sql b/util/Migrator/DbScripts/2026-09-22_00_CreateTwoFactorRememberToken.sql index 47c10e9e928b..76cbeb285d89 100644 --- a/util/Migrator/DbScripts/2026-09-22_00_CreateTwoFactorRememberToken.sql +++ b/util/Migrator/DbScripts/2026-09-22_00_CreateTwoFactorRememberToken.sql @@ -138,7 +138,7 @@ BEGIN END GO -CREATE OR ALTER PROCEDURE [dbo].[TwoFactorRememberToken_DeleteExpired] +CREATE OR ALTER PROCEDURE [dbo].[TwoFactorRememberToken_DeleteManyExpired] @Now DATETIME2(7) AS BEGIN From 39edfbcc54068ed65d76268d2edbc34c3355343b Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:21:28 -0500 Subject: [PATCH 10/27] [PM-40673 Part 1] split upsert (_Save) into _Create and _Update --- .../TwoFactorRememberTokenRepository.cs | 66 ++++++++++-- .../TwoFactorRememberToken_Create.sql | 33 ++++++ .../TwoFactorRememberToken_Save.sql | 57 ---------- .../TwoFactorRememberToken_Update.sql | 22 ++++ .../TwoFactorRememberTokenRepositoryTests.cs | 100 +++++++++++++++++- ...-09-22_00_CreateTwoFactorRememberToken.sql | 80 +++++++------- 6 files changed, 252 insertions(+), 106 deletions(-) create mode 100644 src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_Create.sql delete mode 100644 src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_Save.sql create mode 100644 src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_Update.sql diff --git a/src/Infrastructure.Dapper/Auth/Repositories/TwoFactorRememberTokenRepository.cs b/src/Infrastructure.Dapper/Auth/Repositories/TwoFactorRememberTokenRepository.cs index 5d01a242a438..d203915be7dd 100644 --- a/src/Infrastructure.Dapper/Auth/Repositories/TwoFactorRememberTokenRepository.cs +++ b/src/Infrastructure.Dapper/Auth/Repositories/TwoFactorRememberTokenRepository.cs @@ -22,7 +22,8 @@ public TwoFactorRememberTokenRepository(string connectionString, string readOnly : base(connectionString, readOnlyConnectionString) { } - public async Task GetByUserIdDeviceIdAsync(Guid userId, Guid deviceId) + // Virtual so tests can simulate a stale read, which is what the fallbacks in UpsertAsync handle. + public virtual async Task GetByUserIdDeviceIdAsync(Guid userId, Guid deviceId) { await using var connection = new SqlConnection(ConnectionString); @@ -36,14 +37,69 @@ public TwoFactorRememberTokenRepository(string connectionString, string readOnly public async Task UpsertAsync(TwoFactorRememberToken token) { - // BaseRepository does not assign ids the way the generic repository does, and the procedure - // needs one for the insert branch. SetNewId only fills an unset id, so an update is unaffected. + var existing = await GetByUserIdDeviceIdAsync(token.UserId, token.DeviceId); + if (existing != null && await TryUpdateAsync(token, existing)) + { + return token; + } + + // Either there is no row, or it was removed between the read and the update (the expiry + // sweep can do that). BaseRepository does not assign ids the way the generic repository does, + // and the procedure needs one for the insert. token.SetNewId(); + try + { + await CreateAsync(token); + } + catch (SqlException e) when (e.Number is 2601 or 2627) + { + // Unique-index backstop ([IX_TwoFactorRememberToken_UserId_DeviceId]): a concurrent + // remember-login on the same device inserted the row between our read and our insert. + // Update the winner's row instead of failing the login. + existing = await GetByUserIdDeviceIdAsync(token.UserId, token.DeviceId); + if (existing == null || !await TryUpdateAsync(token, existing)) + { + throw; + } + } + + return token; + } + + private async Task TryUpdateAsync(TwoFactorRememberToken token, TwoFactorRememberToken existing) + { + await using var connection = new SqlConnection(ConnectionString); + + var rowsUpdated = await connection.ExecuteScalarAsync( + "[dbo].[TwoFactorRememberToken_Update]", + new + { + existing.Id, + token.Stamp, + token.RevisionDate, + token.ExpirationDate, + }, + commandType: CommandType.StoredProcedure); + + if (rowsUpdated == 0) + { + return false; + } + + // CreationDate stays as first written, so the row still records when the device was first + // remembered; the update procedure does not write it. + token.Id = existing.Id; + token.CreationDate = existing.CreationDate; + return true; + } + + private async Task CreateAsync(TwoFactorRememberToken token) + { await using var connection = new SqlConnection(ConnectionString); await connection.ExecuteAsync( - "[dbo].[TwoFactorRememberToken_Save]", + "[dbo].[TwoFactorRememberToken_Create]", new { token.Id, @@ -55,8 +111,6 @@ await connection.ExecuteAsync( token.ExpirationDate, }, commandType: CommandType.StoredProcedure); - - return token; } public async Task RotateStampsByUserIdAsync(Guid userId) diff --git a/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_Create.sql b/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_Create.sql new file mode 100644 index 000000000000..b947b06453d4 --- /dev/null +++ b/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_Create.sql @@ -0,0 +1,33 @@ +CREATE PROCEDURE [dbo].[TwoFactorRememberToken_Create] + @Id UNIQUEIDENTIFIER, + @UserId UNIQUEIDENTIFIER, + @DeviceId UNIQUEIDENTIFIER, + @Stamp NVARCHAR(50), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @ExpirationDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[TwoFactorRememberToken] + ( + [Id], + [UserId], + [DeviceId], + [Stamp], + [CreationDate], + [RevisionDate], + [ExpirationDate] + ) + VALUES + ( + @Id, + @UserId, + @DeviceId, + @Stamp, + @CreationDate, + @RevisionDate, + @ExpirationDate + ) +END diff --git a/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_Save.sql b/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_Save.sql deleted file mode 100644 index ae28d19ad847..000000000000 --- a/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_Save.sql +++ /dev/null @@ -1,57 +0,0 @@ -CREATE PROCEDURE [dbo].[TwoFactorRememberToken_Save] - @Id UNIQUEIDENTIFIER, - @UserId UNIQUEIDENTIFIER, - @DeviceId UNIQUEIDENTIFIER, - @Stamp NVARCHAR(50), - @CreationDate DATETIME2(7), - @RevisionDate DATETIME2(7), - @ExpirationDate DATETIME2(7) -AS -BEGIN - SET NOCOUNT ON - -- An explicit transaction is required so the range lock below is still held when the INSERT - -- runs; XACT_ABORT leaves the pooled connection clean if the unique-index backstop trips. - SET XACT_ABORT ON - - BEGIN TRANSACTION - - -- One row per (UserId, DeviceId). The UPDLOCK, HOLDLOCK range lock serializes a concurrent - -- remember-login on the same device: it blocks here until this transaction commits, then - -- finds the row and takes the UPDATE branch instead of inserting a duplicate. The lock is - -- cheap because [IX_TwoFactorRememberToken_UserId_DeviceId] makes it a key-range lock - -- rather than a scan. CreationDate is deliberately not updated. - UPDATE [dbo].[TwoFactorRememberToken] WITH (UPDLOCK, HOLDLOCK) - SET - [Stamp] = @Stamp, - [RevisionDate] = @RevisionDate, - [ExpirationDate] = @ExpirationDate - WHERE - [UserId] = @UserId - AND [DeviceId] = @DeviceId - - IF @@ROWCOUNT = 0 - BEGIN - INSERT INTO [dbo].[TwoFactorRememberToken] - ( - [Id], - [UserId], - [DeviceId], - [Stamp], - [CreationDate], - [RevisionDate], - [ExpirationDate] - ) - VALUES - ( - @Id, - @UserId, - @DeviceId, - @Stamp, - @CreationDate, - @RevisionDate, - @ExpirationDate - ) - END - - COMMIT TRANSACTION -END diff --git a/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_Update.sql b/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_Update.sql new file mode 100644 index 000000000000..03a6d1e7f01d --- /dev/null +++ b/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_Update.sql @@ -0,0 +1,22 @@ +CREATE PROCEDURE [dbo].[TwoFactorRememberToken_Update] + @Id UNIQUEIDENTIFIER, + @Stamp NVARCHAR(50), + @RevisionDate DATETIME2(7), + @ExpirationDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- CreationDate is deliberately not updated: it records when the device was first remembered. + UPDATE + [dbo].[TwoFactorRememberToken] + SET + [Stamp] = @Stamp, + [RevisionDate] = @RevisionDate, + [ExpirationDate] = @ExpirationDate + WHERE + [Id] = @Id + + -- Zero when the row no longer exists, e.g. the expiry sweep removed it after the caller read it. + SELECT @@ROWCOUNT +END diff --git a/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs index 8251521d9c01..ac062ca16cca 100644 --- a/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs @@ -4,6 +4,7 @@ using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Utilities; +using Bit.Infrastructure.Dapper.Auth.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.IntegrationTest.AdminConsole; using EfTwoFactorRememberToken = Bit.Infrastructure.EntityFramework.Auth.Models.TwoFactorRememberToken; @@ -112,9 +113,8 @@ public async Task UpsertAsync_SameUserAndDevice_UpdatesInPlaceAndPreservesCreati /// unique-key violation, which would surface to the user as a failed login. /// /// - /// On SQL Server this exercises the UPDLOCK, HOLDLOCK range lock inside an explicit - /// transaction. That hint is MSSQL-only, so on the EF providers this is a regression guard on the - /// common path rather than proof the lock works. + /// Whether the two calls actually collide is timing-dependent, so this is a regression guard on the + /// common path. The two tests that follow force the Dapper repository's fallbacks deterministically. /// [Theory, DatabaseData] public async Task UpsertAsync_Concurrent_BothCompleteAndOneRowSurvives( @@ -135,6 +135,100 @@ await Task.WhenAll( Assert.Contains(row.Stamp, new[] { "stamp-a", "stamp-b" }); } + /// + /// A repository whose first read returns a canned value, standing in for a read that was stale by + /// the time the write happened. + /// + private sealed class StaleFirstReadRepository(string connectionString, TwoFactorRememberToken? staleRow) + : TwoFactorRememberTokenRepository(connectionString, connectionString) + { + private bool _served; + + public override Task GetByUserIdDeviceIdAsync(Guid userId, Guid deviceId) + { + if (_served) + { + return base.GetByUserIdDeviceIdAsync(userId, deviceId); + } + + _served = true; + return Task.FromResult(staleRow); + } + } + + /// + /// The read finds no row, but a concurrent login inserts one before this call's insert runs. The + /// unique index rejects the insert, and the repository must update the winner's row rather than + /// fail the login. + /// + [Theory, DatabaseData] + public async Task UpsertAsync_RowAppearsAfterRead_UpdatesTheWinnersRow( + Database database, + ITwoFactorRememberTokenRepository sut, + IUserRepository userRepository, + IDeviceRepository deviceRepository) + { + if (database.Type != SupportedDatabaseProviders.SqlServer || database.UseEf) + { + Assert.Skip("Exercises the Dapper repository's duplicate-key fallback."); + } + + var user = await userRepository.CreateTestUserAsync(); + var device = await CreateTestDeviceAsync(deviceRepository, user.Id); + await sut.UpsertAsync(NewToken(user.Id, device.Id, "winner")); + var winner = await sut.GetByUserIdDeviceIdAsync(user.Id, device.Id); + Assert.NotNull(winner); + + var loser = new StaleFirstReadRepository(database.ConnectionString, staleRow: null); + await loser.UpsertAsync(NewToken(user.Id, device.Id, "loser")); + + var row = await sut.GetByUserIdDeviceIdAsync(user.Id, device.Id); + Assert.NotNull(row); + Assert.Equal("loser", row.Stamp); + Assert.Equal(winner.Id, row.Id); + Assert.Equal(winner.CreationDate, row.CreationDate); + } + + /// + /// The read finds a row, which the expiry sweep then deletes before the update runs. The update + /// matches nothing, so the repository must create the row instead of returning a token with no + /// row behind it. + /// + [Theory, DatabaseData] + public async Task UpsertAsync_RowDeletedAfterRead_CreatesTheRow( + Database database, + ITwoFactorRememberTokenRepository sut, + IUserRepository userRepository, + IDeviceRepository deviceRepository) + { + if (database.Type != SupportedDatabaseProviders.SqlServer || database.UseEf) + { + Assert.Skip("Exercises the Dapper repository's update-matched-nothing fallback."); + } + + var user = await userRepository.CreateTestUserAsync(); + var device = await CreateTestDeviceAsync(deviceRepository, user.Id); + await sut.UpsertAsync(NewToken(user.Id, device.Id, "original")); + var staleRow = await sut.GetByUserIdDeviceIdAsync(user.Id, device.Id); + Assert.NotNull(staleRow); + + await using (var connection = new SqlConnection(database.ConnectionString)) + { + await connection.OpenAsync(TestContext.Current.CancellationToken); + await using var command = connection.CreateCommand(); + command.CommandText = "DELETE FROM [dbo].[TwoFactorRememberToken] WHERE [Id] = @Id"; + command.Parameters.Add(new SqlParameter("@Id", staleRow.Id)); + await command.ExecuteNonQueryAsync(TestContext.Current.CancellationToken); + } + + var repository = new StaleFirstReadRepository(database.ConnectionString, staleRow); + await repository.UpsertAsync(NewToken(user.Id, device.Id, "replacement")); + + var row = await sut.GetByUserIdDeviceIdAsync(user.Id, device.Id); + Assert.NotNull(row); + Assert.Equal("replacement", row.Stamp); + } + // ------------------------------------------------------------------------------------------- // Rotation // ------------------------------------------------------------------------------------------- diff --git a/util/Migrator/DbScripts/2026-09-22_00_CreateTwoFactorRememberToken.sql b/util/Migrator/DbScripts/2026-09-22_00_CreateTwoFactorRememberToken.sql index 76cbeb285d89..75589ddb08b6 100644 --- a/util/Migrator/DbScripts/2026-09-22_00_CreateTwoFactorRememberToken.sql +++ b/util/Migrator/DbScripts/2026-09-22_00_CreateTwoFactorRememberToken.sql @@ -36,7 +36,7 @@ FROM GO -- Stored Procedures -CREATE OR ALTER PROCEDURE [dbo].[TwoFactorRememberToken_Save] +CREATE OR ALTER PROCEDURE [dbo].[TwoFactorRememberToken_Create] @Id UNIQUEIDENTIFIER, @UserId UNIQUEIDENTIFIER, @DeviceId UNIQUEIDENTIFIER, @@ -47,51 +47,51 @@ CREATE OR ALTER PROCEDURE [dbo].[TwoFactorRememberToken_Save] AS BEGIN SET NOCOUNT ON - -- An explicit transaction is required so the range lock below is still held when the INSERT - -- runs; XACT_ABORT leaves the pooled connection clean if the unique-index backstop trips. - SET XACT_ABORT ON - - BEGIN TRANSACTION - - -- One row per (UserId, DeviceId). The UPDLOCK, HOLDLOCK range lock serializes a concurrent - -- remember-login on the same device: it blocks here until this transaction commits, then - -- finds the row and takes the UPDATE branch instead of inserting a duplicate. The lock is - -- cheap because [IX_TwoFactorRememberToken_UserId_DeviceId] makes it a key-range lock - -- rather than a scan. CreationDate is deliberately not updated. - UPDATE [dbo].[TwoFactorRememberToken] WITH (UPDLOCK, HOLDLOCK) + + INSERT INTO [dbo].[TwoFactorRememberToken] + ( + [Id], + [UserId], + [DeviceId], + [Stamp], + [CreationDate], + [RevisionDate], + [ExpirationDate] + ) + VALUES + ( + @Id, + @UserId, + @DeviceId, + @Stamp, + @CreationDate, + @RevisionDate, + @ExpirationDate + ) +END +GO + +CREATE OR ALTER PROCEDURE [dbo].[TwoFactorRememberToken_Update] + @Id UNIQUEIDENTIFIER, + @Stamp NVARCHAR(50), + @RevisionDate DATETIME2(7), + @ExpirationDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + -- CreationDate is deliberately not updated: it records when the device was first remembered. + UPDATE + [dbo].[TwoFactorRememberToken] SET [Stamp] = @Stamp, [RevisionDate] = @RevisionDate, [ExpirationDate] = @ExpirationDate WHERE - [UserId] = @UserId - AND [DeviceId] = @DeviceId - - IF @@ROWCOUNT = 0 - BEGIN - INSERT INTO [dbo].[TwoFactorRememberToken] - ( - [Id], - [UserId], - [DeviceId], - [Stamp], - [CreationDate], - [RevisionDate], - [ExpirationDate] - ) - VALUES - ( - @Id, - @UserId, - @DeviceId, - @Stamp, - @CreationDate, - @RevisionDate, - @ExpirationDate - ) - END + [Id] = @Id - COMMIT TRANSACTION + -- Zero when the row no longer exists, e.g. the expiry sweep removed it after the caller read it. + SELECT @@ROWCOUNT END GO From 220186ea62281f4bdcf794faa059e75785b82ae3 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:27:40 -0500 Subject: [PATCH 11/27] [PM-40673 Part 1] update expiration time comparison check --- .../ValidateTwoFactorRememberTokenQuery.cs | 2 +- ...alidateTwoFactorRememberTokenQueryTests.cs | 29 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs index ac9b69073655..db282544c826 100644 --- a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs @@ -63,6 +63,6 @@ public async Task ValidateAsync(User user, string deviceIdentifier, string return false; } - return row.ExpirationDate > timeProvider.GetUtcNow().UtcDateTime; + return row.ExpirationDate >= timeProvider.GetUtcNow().UtcDateTime; } } diff --git a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs index 7276bfb64519..de2a47a4dda1 100644 --- a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs +++ b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs @@ -7,6 +7,7 @@ using Bit.Core.Tokens; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Time.Testing; using NSubstitute; using Xunit; @@ -194,6 +195,34 @@ public async Task ValidateAsync_RowExpired_ReturnsFalse( Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); } + /// + /// Expiry is exclusive of the expiry instant, matching ExpiringTokenable.IsExpired: a row is + /// still honored at exactly its ExpirationDate and refused one tick later. + /// + [Theory, BitAutoData] + public async Task ValidateAsync_RowExpiresExactlyNow_ReturnsTrue(User user, Guid deviceId) + { + var sutProvider = new SutProvider().WithFakeTimeProvider().Create(); + var now = new DateTime(2026, 10, 2, 12, 0, 0, DateTimeKind.Utc); + sutProvider.GetDependency().SetUtcNow(now); + var (_, row) = ArrangeValid(sutProvider, user, deviceId); + row.ExpirationDate = now; + + Assert.True(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); + } + + [Theory, BitAutoData] + public async Task ValidateAsync_RowExpiredOneTickAgo_ReturnsFalse(User user, Guid deviceId) + { + var sutProvider = new SutProvider().WithFakeTimeProvider().Create(); + var now = new DateTime(2026, 10, 2, 12, 0, 0, DateTimeKind.Utc); + sutProvider.GetDependency().SetUtcNow(now); + var (_, row) = ArrangeValid(sutProvider, user, deviceId); + row.ExpirationDate = now.AddTicks(-1); + + Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); + } + [Theory, BitAutoData] public async Task ValidateAsync_TokenCannotBeUnprotected_ReturnsFalse( SutProvider sutProvider, User user) From c601599cdb168397536d6278b4398ba887f0ad04 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:31:31 -0500 Subject: [PATCH 12/27] [PM-40673 Part 1] use FakeTimeProvider in ValidateTwoFactorRememberTokenQueryTests --- ...alidateTwoFactorRememberTokenQueryTests.cs | 68 ++++++++++++------- 1 file changed, 45 insertions(+), 23 deletions(-) diff --git a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs index de2a47a4dda1..f365d8dd67a9 100644 --- a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs +++ b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs @@ -23,6 +23,17 @@ public class ValidateTwoFactorRememberTokenQueryTests private const string _deviceIdentifier = "device-identifier"; private const string _token = "protected-token"; + private static readonly DateTime _now = new(2026, 10, 2, 12, 0, 0, DateTimeKind.Utc); + + private static SutProvider GetSutProvider() + { + var sutProvider = new SutProvider() + .WithFakeTimeProvider() + .Create(); + sutProvider.GetDependency().SetUtcNow(_now); + return sutProvider; + } + private static TwoFactorRememberTokenable Tokenable(User user, Guid deviceId, string stamp) => new() { @@ -40,7 +51,7 @@ private static TwoFactorRememberToken Row(User user, Guid deviceId, string stamp UserId = user.Id, DeviceId = deviceId, Stamp = stamp, - ExpirationDate = DateTime.UtcNow.AddDays(1), + ExpirationDate = _now.AddDays(1), }; /// @@ -72,8 +83,9 @@ private static (TwoFactorRememberTokenable tokenable, TwoFactorRememberToken row /// The happy path. [Theory, BitAutoData] public async Task ValidateAsync_EverythingMatches_ReturnsTrue( - SutProvider sutProvider, User user, Guid deviceId) + User user, Guid deviceId) { + var sutProvider = GetSutProvider(); ArrangeValid(sutProvider, user, deviceId); Assert.True(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); @@ -82,8 +94,9 @@ public async Task ValidateAsync_EverythingMatches_ReturnsTrue( /// The targeted revocation: the device's row stamp has been rotated. [Theory, BitAutoData] public async Task ValidateAsync_RowStampRotated_ReturnsFalse( - SutProvider sutProvider, User user, Guid deviceId) + User user, Guid deviceId) { + var sutProvider = GetSutProvider(); var (_, row) = ArrangeValid(sutProvider, user, deviceId); row.Stamp = "rotated-stamp"; @@ -93,8 +106,9 @@ public async Task ValidateAsync_RowStampRotated_ReturnsFalse( /// The account-wide check inherited from the previous design. [Theory, BitAutoData] public async Task ValidateAsync_UserSecurityStampRotated_ReturnsFalse( - SutProvider sutProvider, User user, Guid deviceId) + User user, Guid deviceId) { + var sutProvider = GetSutProvider(); var (tokenable, _) = ArrangeValid(sutProvider, user, deviceId); tokenable.SecurityStamp = "a-different-security-stamp"; @@ -107,8 +121,9 @@ public async Task ValidateAsync_UserSecurityStampRotated_ReturnsFalse( /// [Theory, BitAutoData] public async Task ValidateAsync_UserHasNoTwoFactorEnabled_ReturnsFalse( - SutProvider sutProvider, User user, Guid deviceId) + User user, Guid deviceId) { + var sutProvider = GetSutProvider(); ArrangeValid(sutProvider, user, deviceId); sutProvider.GetDependency() .TwoFactorIsEnabledAsync(user) @@ -123,8 +138,9 @@ public async Task ValidateAsync_UserHasNoTwoFactorEnabled_ReturnsFalse( /// [Theory, BitAutoData] public async Task ValidateAsync_UserHasNoTwoFactorEnabled_DoesNotReadRow( - SutProvider sutProvider, User user, Guid deviceId) + User user, Guid deviceId) { + var sutProvider = GetSutProvider(); ArrangeValid(sutProvider, user, deviceId); sutProvider.GetDependency() .TwoFactorIsEnabledAsync(user) @@ -140,8 +156,9 @@ await sutProvider.GetDependency() /// A token issued to a different user. [Theory, BitAutoData] public async Task ValidateAsync_UserIdMismatch_ReturnsFalse( - SutProvider sutProvider, User user, Guid deviceId) + User user, Guid deviceId) { + var sutProvider = GetSutProvider(); var (tokenable, _) = ArrangeValid(sutProvider, user, deviceId); tokenable.UserId = Guid.NewGuid(); @@ -151,8 +168,9 @@ public async Task ValidateAsync_UserIdMismatch_ReturnsFalse( /// The device binding. [Theory, BitAutoData] public async Task ValidateAsync_DeviceIdentifierMismatch_ReturnsFalse( - SutProvider sutProvider, User user, Guid deviceId) + User user, Guid deviceId) { + var sutProvider = GetSutProvider(); ArrangeValid(sutProvider, user, deviceId); Assert.False(await sutProvider.Sut.ValidateAsync(user, "a-different-device", _token)); @@ -164,8 +182,9 @@ public async Task ValidateAsync_DeviceIdentifierMismatch_ReturnsFalse( /// [Theory, BitAutoData] public async Task ValidateAsync_DeviceIdentifierDiffersOnlyByCase_ReturnsTrue( - SutProvider sutProvider, User user, Guid deviceId) + User user, Guid deviceId) { + var sutProvider = GetSutProvider(); ArrangeValid(sutProvider, user, deviceId); Assert.True(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier.ToUpperInvariant(), _token)); @@ -174,8 +193,9 @@ public async Task ValidateAsync_DeviceIdentifierDiffersOnlyByCase_ReturnsTrue( /// No row for this device. [Theory, BitAutoData] public async Task ValidateAsync_RowMissing_ReturnsFalse( - SutProvider sutProvider, User user, Guid deviceId) + User user, Guid deviceId) { + var sutProvider = GetSutProvider(); ArrangeValid(sutProvider, user, deviceId); sutProvider.GetDependency() .GetByUserIdDeviceIdAsync(user.Id, deviceId) @@ -187,10 +207,11 @@ public async Task ValidateAsync_RowMissing_ReturnsFalse( /// The row outlived its expiry but the sweep has not reached it. [Theory, BitAutoData] public async Task ValidateAsync_RowExpired_ReturnsFalse( - SutProvider sutProvider, User user, Guid deviceId) + User user, Guid deviceId) { + var sutProvider = GetSutProvider(); var (_, row) = ArrangeValid(sutProvider, user, deviceId); - row.ExpirationDate = DateTime.UtcNow.AddMinutes(-1); + row.ExpirationDate = _now.AddMinutes(-1); Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); } @@ -202,11 +223,9 @@ public async Task ValidateAsync_RowExpired_ReturnsFalse( [Theory, BitAutoData] public async Task ValidateAsync_RowExpiresExactlyNow_ReturnsTrue(User user, Guid deviceId) { - var sutProvider = new SutProvider().WithFakeTimeProvider().Create(); - var now = new DateTime(2026, 10, 2, 12, 0, 0, DateTimeKind.Utc); - sutProvider.GetDependency().SetUtcNow(now); + var sutProvider = GetSutProvider(); var (_, row) = ArrangeValid(sutProvider, user, deviceId); - row.ExpirationDate = now; + row.ExpirationDate = _now; Assert.True(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); } @@ -214,19 +233,18 @@ public async Task ValidateAsync_RowExpiresExactlyNow_ReturnsTrue(User user, Guid [Theory, BitAutoData] public async Task ValidateAsync_RowExpiredOneTickAgo_ReturnsFalse(User user, Guid deviceId) { - var sutProvider = new SutProvider().WithFakeTimeProvider().Create(); - var now = new DateTime(2026, 10, 2, 12, 0, 0, DateTimeKind.Utc); - sutProvider.GetDependency().SetUtcNow(now); + var sutProvider = GetSutProvider(); var (_, row) = ArrangeValid(sutProvider, user, deviceId); - row.ExpirationDate = now.AddTicks(-1); + row.ExpirationDate = _now.AddTicks(-1); Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); } [Theory, BitAutoData] public async Task ValidateAsync_TokenCannotBeUnprotected_ReturnsFalse( - SutProvider sutProvider, User user) + User user) { + var sutProvider = GetSutProvider(); sutProvider.GetDependency>() .TryUnprotect(_token, out Arg.Any()) .Returns(c => { c[1] = null!; return false; }); @@ -236,9 +254,12 @@ public async Task ValidateAsync_TokenCannotBeUnprotected_ReturnsFalse( [Theory, BitAutoData] public async Task ValidateAsync_TokenItselfExpired_ReturnsFalse( - SutProvider sutProvider, User user, Guid deviceId) + User user, Guid deviceId) { + var sutProvider = GetSutProvider(); var (tokenable, _) = ArrangeValid(sutProvider, user, deviceId); + // Real clock on purpose: ExpiringTokenable.IsExpired reads DateTime.UtcNow directly, so the + // injected TimeProvider cannot reach it. tokenable.ExpirationDate = DateTime.UtcNow.AddMinutes(-1); Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); @@ -247,8 +268,9 @@ public async Task ValidateAsync_TokenItselfExpired_ReturnsFalse( /// Validation reads; it must never write. [Theory, BitAutoData] public async Task ValidateAsync_Always_DoesNotWrite( - SutProvider sutProvider, User user, Guid deviceId) + User user, Guid deviceId) { + var sutProvider = GetSutProvider(); ArrangeValid(sutProvider, user, deviceId); await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token); From 161cd50d0c3c425a9b507c5ded5ec86a2013b998 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:40:06 -0500 Subject: [PATCH 13/27] [PM-40673 Part 1] add a TwoFactorRememberTokenableFactory --- .../ITwoFactorRememberTokenableFactory.cs | 13 ++ .../Tokenables/TwoFactorRememberTokenable.cs | 89 ++++++++++++-- .../TwoFactorRememberTokenableFactory.cs | 18 +++ .../IssueTwoFactorRememberTokenCommand.cs | 17 +-- .../ValidateTwoFactorRememberTokenQuery.cs | 5 +- src/Core/Settings/GlobalSettings.cs | 1 + src/Core/Settings/IGlobalSettings.cs | 1 + .../Utilities/ServiceCollectionExtensions.cs | 1 + .../TwoFactorRememberTokenableFactoryTests.cs | 44 +++++++ .../TwoFactorRememberTokenableTests.cs | 113 +++++++++++++++--- ...IssueTwoFactorRememberTokenCommandTests.cs | 33 +++-- ...alidateTwoFactorRememberTokenQueryTests.cs | 2 + 12 files changed, 289 insertions(+), 48 deletions(-) create mode 100644 src/Core/Auth/Models/Business/Tokenables/ITwoFactorRememberTokenableFactory.cs create mode 100644 src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableFactory.cs create mode 100644 test/Core.Test/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableFactoryTests.cs diff --git a/src/Core/Auth/Models/Business/Tokenables/ITwoFactorRememberTokenableFactory.cs b/src/Core/Auth/Models/Business/Tokenables/ITwoFactorRememberTokenableFactory.cs new file mode 100644 index 000000000000..b6524da09d3f --- /dev/null +++ b/src/Core/Auth/Models/Business/Tokenables/ITwoFactorRememberTokenableFactory.cs @@ -0,0 +1,13 @@ +using Bit.Core.Entities; + +namespace Bit.Core.Auth.Models.Business.Tokenables; + +/// Mints instances with the operator-configured lifetime. +public interface ITwoFactorRememberTokenableFactory +{ + /// + /// Creates a token bound to the given user and device, carrying , and expiring + /// after the configured lifetime. + /// + TwoFactorRememberTokenable CreateToken(User user, Device device, string stamp); +} diff --git a/src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenable.cs b/src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenable.cs index 9110a5281a2d..fba34aad9dc1 100644 --- a/src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenable.cs +++ b/src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenable.cs @@ -11,37 +11,44 @@ namespace Bit.Core.Auth.Models.Business.Tokenables; /// The token is encrypted and signed, so a client can neither read nor alter its contents. It names /// the device it was issued to and carries two stamps that are compared against server-side state on /// every use: against the user, and against that -/// device's TwoFactorRememberToken row. +/// device's TwoFactorRememberToken row. Minted only through +/// , which applies the configured lifetime. /// public class TwoFactorRememberTokenable : ExpiringTokenable { - public static TimeSpan GetTokenLifetime() => TimeSpan.FromDays(30); - public const string ClearTextPrefix = "BwTwoFactorRemember_"; public const string DataProtectorPurpose = "TwoFactorRememberTokenDataProtector"; public const string TokenIdentifier = "TwoFactorRememberToken"; - public string Identifier { get; set; } = TokenIdentifier; + // Binding properties use [JsonInclude] internal set so only the factory (in Bit.Core) can mint a + // token. [JsonInclude] is required: JsonSerializer with default options ignores non-public + // setters, and deserialized tokens would silently come back with default values. + [JsonInclude] + public string Identifier { get; internal set; } = TokenIdentifier; - public Guid UserId { get; set; } + [JsonInclude] + public Guid UserId { get; internal set; } /// /// The primary key of the device this token was issued to, used to locate the row to compare /// against. /// - public Guid DeviceId { get; set; } + [JsonInclude] + public Guid DeviceId { get; internal set; } /// /// The client-generated device identifier from the request that minted this token. Compared /// against the presenting request's identifier, which binds the token to one device without /// needing a database read. /// - public string DeviceIdentifier { get; set; } = null!; + [JsonInclude] + public string DeviceIdentifier { get; internal set; } = null!; /// /// A copy of the issuing device's row stamp. Device-scoped. /// - public string Stamp { get; set; } = null!; + [JsonInclude] + public string Stamp { get; internal set; } = null!; // TODO: PM-44171 - Consider removing SecurityStamp from this token. The row's Stamp already // covers the same ground more precisely, and carrying the user stamp ties remember-me lifetime @@ -50,12 +57,50 @@ public class TwoFactorRememberTokenable : ExpiringTokenable /// /// A copy of User.SecurityStamp as it stood at issuance. Account-scoped. /// - public string SecurityStamp { get; set; } = null!; + [JsonInclude] + public string SecurityStamp { get; internal set; } = null!; + // Leaves ExpirationDate at its default, so a token constructed without going through the factory + // is expired and therefore never valid. [JsonConstructor] public TwoFactorRememberTokenable() { - ExpirationDate = DateTime.UtcNow.Add(GetTokenLifetime()); + } + + internal TwoFactorRememberTokenable( + Guid userId, Guid deviceId, string deviceIdentifier, string stamp, string securityStamp) + { + if (userId == default) + { + throw new ArgumentException("User id cannot be empty.", nameof(userId)); + } + + if (deviceId == default) + { + throw new ArgumentException("Device id cannot be empty.", nameof(deviceId)); + } + + if (string.IsNullOrWhiteSpace(deviceIdentifier)) + { + throw new ArgumentException("Device identifier cannot be null or whitespace.", nameof(deviceIdentifier)); + } + + if (string.IsNullOrWhiteSpace(stamp)) + { + throw new ArgumentException("Stamp cannot be null or whitespace.", nameof(stamp)); + } + + if (string.IsNullOrWhiteSpace(securityStamp)) + { + throw new ArgumentException("Security stamp cannot be null or whitespace.", nameof(securityStamp)); + } + + Identifier = TokenIdentifier; + UserId = userId; + DeviceId = deviceId; + DeviceIdentifier = deviceIdentifier; + Stamp = stamp; + SecurityStamp = securityStamp; } /// @@ -68,4 +113,28 @@ protected override bool TokenIsValid() => && !string.IsNullOrWhiteSpace(DeviceIdentifier) && !string.IsNullOrWhiteSpace(Stamp) && !string.IsNullOrWhiteSpace(SecurityStamp); + + /// + /// Unprotects a presented token and checks everything that can be checked without server-side + /// state: tampering, a wrong data-protection purpose, the token's own expiry, and the presence of + /// its fields. The caller still compares the result against the user and the device's row. + /// + /// The unprotected token, or null when it could not be unprotected. + public static TokenableValidationError? ValidateTwoFactorRememberToken( + IDataProtectorTokenFactory tokenFactory, + string token, + out TwoFactorRememberTokenable? tokenable) + { + if (!tokenFactory.TryUnprotect(token, out tokenable) || tokenable is null) + { + return TokenableValidationError.InvalidToken; + } + + if (tokenable.IsExpired) + { + return TokenableValidationError.ExpiringTokenables.Expired; + } + + return tokenable.Valid ? null : TokenableValidationError.InvalidToken; + } } diff --git a/src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableFactory.cs b/src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableFactory.cs new file mode 100644 index 000000000000..c6dd977afda0 --- /dev/null +++ b/src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableFactory.cs @@ -0,0 +1,18 @@ +using Bit.Core.Entities; +using Bit.Core.Settings; + +namespace Bit.Core.Auth.Models.Business.Tokenables; + +/// +public class TwoFactorRememberTokenableFactory( + IGlobalSettings globalSettings, + TimeProvider timeProvider) : ITwoFactorRememberTokenableFactory +{ + /// + public TwoFactorRememberTokenable CreateToken(User user, Device device, string stamp) => + new(user.Id, device.Id, device.Identifier, stamp, user.SecurityStamp) + { + ExpirationDate = timeProvider.GetUtcNow().UtcDateTime + .AddDays(globalSettings.TwoFactorRememberTokenLifetimeInDays), + }; +} diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/IssueTwoFactorRememberTokenCommand.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/IssueTwoFactorRememberTokenCommand.cs index 634702a62423..ccb8a53922ad 100644 --- a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/IssueTwoFactorRememberTokenCommand.cs +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/IssueTwoFactorRememberTokenCommand.cs @@ -8,19 +8,22 @@ namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations; public class IssueTwoFactorRememberTokenCommand( ITwoFactorRememberTokenRepository twoFactorRememberTokenRepository, + ITwoFactorRememberTokenableFactory tokenableFactory, IDataProtectorTokenFactory tokenFactory, TimeProvider timeProvider) : IIssueTwoFactorRememberTokenCommand { public async Task IssueAsync(User user, Device device) { var now = timeProvider.GetUtcNow().UtcDateTime; - var expirationDate = now.Add(TwoFactorRememberTokenable.GetTokenLifetime()); // Generated here rather than by the repository because the value has to go into the token // as well as the row. Guid.NewGuid rather than a comb: this is an opaque comparand, not a // table identifier, and it should carry no embedded timestamp. var stamp = Guid.NewGuid().ToString(); + // The row takes its expiry from the token, so the two always lapse together. + var tokenable = tokenableFactory.CreateToken(user, device, stamp); + var row = await twoFactorRememberTokenRepository.UpsertAsync(new TwoFactorRememberToken { UserId = user.Id, @@ -28,17 +31,9 @@ public async Task IssueAsync(User user, Device device) Stamp = stamp, CreationDate = now, RevisionDate = now, - ExpirationDate = expirationDate, + ExpirationDate = tokenable.ExpirationDate, }); - return tokenFactory.Protect(new TwoFactorRememberTokenable - { - UserId = user.Id, - DeviceId = device.Id, - DeviceIdentifier = device.Identifier, - Stamp = row.Stamp, - SecurityStamp = user.SecurityStamp, - ExpirationDate = row.ExpirationDate, - }); + return tokenFactory.Protect(tokenable); } } diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs index db282544c826..ff37fe43087c 100644 --- a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs @@ -14,9 +14,8 @@ public class ValidateTwoFactorRememberTokenQuery( { public async Task ValidateAsync(User user, string deviceIdentifier, string token) { - // Covers tampering, a wrong data-protection purpose, the token's own expiry, and the - // presence checks on its fields. - if (!tokenFactory.TryUnprotect(token, out var tokenable) || tokenable is null || !tokenable.Valid) + if (TwoFactorRememberTokenable.ValidateTwoFactorRememberToken(tokenFactory, token, out var tokenable) != null + || tokenable is null) { return false; } diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 1bc8c3dbe6b1..9b62a5b5c913 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -51,6 +51,7 @@ public virtual string MailTemplateDirectory public virtual bool EnableCloudCommunication { get; set; } = false; public virtual int OrganizationInviteExpirationHours { get; set; } = 120; // 5 days public virtual int TwoFactorUserVerificationTokenLifetimeInMinutes { get; set; } = 30; + public virtual int TwoFactorRememberTokenLifetimeInDays { get; set; } = 30; public virtual int DeviceLastActivityCacheTtlHours { get; set; } = 120; // 5 days public virtual string EventGridKey { get; set; } public virtual bool TestPlayIdTrackingEnabled { get; set; } = false; diff --git a/src/Core/Settings/IGlobalSettings.cs b/src/Core/Settings/IGlobalSettings.cs index 7ea8a45ec6ce..42ee4bfa955c 100644 --- a/src/Core/Settings/IGlobalSettings.cs +++ b/src/Core/Settings/IGlobalSettings.cs @@ -16,6 +16,7 @@ public interface IGlobalSettings string LicenseCertificatePath { get; set; } int OrganizationInviteExpirationHours { get; set; } int TwoFactorUserVerificationTokenLifetimeInMinutes { get; set; } + int TwoFactorRememberTokenLifetimeInDays { get; set; } int DeviceLastActivityCacheTtlHours { get; set; } bool DisableUserRegistration { get; set; } bool SuppressOnboardingInterstitials { get; set; } diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index c98b23e3ad9c..892450fb713b 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -267,6 +267,7 @@ public static void AddTokenizers(this IServiceCollection services) serviceProvider.GetDataProtectionProvider(), serviceProvider.GetRequiredService>>())); services.AddSingleton(); + services.AddSingleton(); services.AddSingleton>( serviceProvider => new DataProtectorTokenFactory( TwoFactorUserVerificationTokenable.ClearTextPrefix, diff --git a/test/Core.Test/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableFactoryTests.cs b/test/Core.Test/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableFactoryTests.cs new file mode 100644 index 000000000000..62f47f9e7162 --- /dev/null +++ b/test/Core.Test/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableFactoryTests.cs @@ -0,0 +1,44 @@ +using AutoFixture.Xunit2; +using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Entities; +using Bit.Core.Settings; +using Microsoft.Extensions.Time.Testing; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.Auth.Models.Business.Tokenables; + +public class TwoFactorRememberTokenableFactoryTests +{ + private static readonly DateTime _now = new(2026, 10, 2, 12, 0, 0, DateTimeKind.Utc); + + private static TwoFactorRememberTokenableFactory GetSut(int lifetimeInDays) + { + var globalSettings = Substitute.For(); + globalSettings.TwoFactorRememberTokenLifetimeInDays.Returns(lifetimeInDays); + var timeProvider = new FakeTimeProvider(); + timeProvider.SetUtcNow(_now); + return new TwoFactorRememberTokenableFactory(globalSettings, timeProvider); + } + + [Theory, AutoData] + public void CreateToken_BindsUserDeviceAndStamps(User user, Device device) + { + var token = GetSut(30).CreateToken(user, device, "row-stamp"); + + Assert.Equal(user.Id, token.UserId); + Assert.Equal(device.Id, token.DeviceId); + Assert.Equal(device.Identifier, token.DeviceIdentifier); + Assert.Equal("row-stamp", token.Stamp); + Assert.Equal(user.SecurityStamp, token.SecurityStamp); + Assert.True(token.Valid); + } + + [Theory, AutoData] + public void CreateToken_HonorsConfiguredLifetime(User user, Device device) + { + var token = GetSut(7).CreateToken(user, device, "row-stamp"); + + Assert.Equal(_now.AddDays(7), token.ExpirationDate); + } +} diff --git a/test/Core.Test/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableTests.cs b/test/Core.Test/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableTests.cs index 63712510a434..9eeb64fbc184 100644 --- a/test/Core.Test/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableTests.cs +++ b/test/Core.Test/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableTests.cs @@ -35,38 +35,119 @@ private static TwoFactorRememberTokenable NewTokenable( DeviceIdentifier = deviceIdentifier, Stamp = stamp, SecurityStamp = securityStamp, + ExpirationDate = DateTime.UtcNow.AddDays(30), }; /// - /// A freshly constructed token must be valid. If ExpirationDate were left at its default, - /// every token would be unusable the instant it was minted, which presents as a validation - /// problem everywhere downstream rather than as a problem with this class. + /// A token constructed without going through the factory has no expiry, so it is expired and never + /// valid. Callers that bypass the factory fail closed instead of getting an ad-hoc lifetime. /// [Fact] - public void Constructor_AfterInitialization_TokenIsValid() + public void Constructor_WithoutFactory_IsExpiredAndInvalid() { + var token = new TwoFactorRememberTokenable(); + + Assert.Equal(default, token.ExpirationDate); + Assert.True(token.IsExpired); + Assert.False(token.Valid); + } + + [Fact] + public void InternalConstructor_ValidArguments_BindsEveryField() + { + var userId = Guid.NewGuid(); + var deviceId = Guid.NewGuid(); + + var token = new TwoFactorRememberTokenable(userId, deviceId, "device-identifier", "row-stamp", "user-stamp"); + + Assert.Equal(userId, token.UserId); + Assert.Equal(deviceId, token.DeviceId); + Assert.Equal("device-identifier", token.DeviceIdentifier); + Assert.Equal("row-stamp", token.Stamp); + Assert.Equal("user-stamp", token.SecurityStamp); + Assert.Equal(TwoFactorRememberTokenable.TokenIdentifier, token.Identifier); + } + + [Fact] + public void InternalConstructor_DefaultUserId_Throws() + { + Assert.Throws(() => + new TwoFactorRememberTokenable(default, Guid.NewGuid(), "device-identifier", "row-stamp", "user-stamp")); + } + + [Fact] + public void InternalConstructor_DefaultDeviceId_Throws() + { + Assert.Throws(() => + new TwoFactorRememberTokenable(Guid.NewGuid(), default, "device-identifier", "row-stamp", "user-stamp")); + } + + [Theory] + [InlineData("", "row-stamp", "user-stamp")] + [InlineData(" ", "row-stamp", "user-stamp")] + [InlineData("device-identifier", "", "user-stamp")] + [InlineData("device-identifier", " ", "user-stamp")] + [InlineData("device-identifier", "row-stamp", "")] + [InlineData("device-identifier", "row-stamp", " ")] + public void InternalConstructor_BlankStringArgument_Throws( + string deviceIdentifier, string stamp, string securityStamp) + { + Assert.Throws(() => + new TwoFactorRememberTokenable(Guid.NewGuid(), Guid.NewGuid(), deviceIdentifier, stamp, securityStamp)); + } + + [Fact] + public void ValidateTwoFactorRememberToken_ValidToken_ReturnsNullAndTheToken() + { + var factory = GetSigningFactory(); var token = NewTokenable(); - Assert.False(token.IsExpired); - Assert.True(token.Valid); + var error = TwoFactorRememberTokenable.ValidateTwoFactorRememberToken( + factory, factory.Protect(token), out var recovered); + + Assert.Null(error); + Assert.NotNull(recovered); + Assert.Equal(token.UserId, recovered.UserId); + } + + [Fact] + public void ValidateTwoFactorRememberToken_UnprotectFails_ReturnsInvalidToken() + { + var error = TwoFactorRememberTokenable.ValidateTwoFactorRememberToken( + GetSigningFactory(), "not-a-token", out var recovered); + + Assert.Equal(TokenableValidationError.InvalidToken, error); + Assert.Null(recovered); } [Fact] - public void Constructor_AfterInitialization_ExpirationSetToExpectedDuration() + public void ValidateTwoFactorRememberToken_ExpiredToken_ReturnsExpired() { - var before = DateTime.UtcNow; - var token = new TwoFactorRememberTokenable(); - var after = DateTime.UtcNow; + var factory = GetSigningFactory(); + var token = NewTokenable(); + token.ExpirationDate = DateTime.UtcNow.AddMinutes(-1); + + var error = TwoFactorRememberTokenable.ValidateTwoFactorRememberToken( + factory, factory.Protect(token), out _); + + Assert.Equal(TokenableValidationError.ExpiringTokenables.Expired, error); + } + + [Fact] + public void ValidateTwoFactorRememberToken_MissingField_ReturnsInvalidToken() + { + var factory = GetSigningFactory(); + var token = NewTokenable(stamp: " "); + + var error = TwoFactorRememberTokenable.ValidateTwoFactorRememberToken( + factory, factory.Protect(token), out _); - Assert.InRange( - token.ExpirationDate, - before + TwoFactorRememberTokenable.GetTokenLifetime(), - after + TwoFactorRememberTokenable.GetTokenLifetime()); + Assert.Equal(TokenableValidationError.InvalidToken, error); } /// - /// Deserialization runs the constructor first and then overwrites ExpirationDate from the - /// payload, which is what stops a round trip from granting a fresh lifetime. + /// The binding properties have internal setters, so this round trip is also what proves + /// [JsonInclude] is doing its job: without it every field would come back as its default. /// [Fact] public void ProtectUnprotect_ValidToken_PreservesDataAndExpiration() diff --git a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/IssueTwoFactorRememberTokenCommandTests.cs b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/IssueTwoFactorRememberTokenCommandTests.cs index 14dba3b8c7da..0322715ad89d 100644 --- a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/IssueTwoFactorRememberTokenCommandTests.cs +++ b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/IssueTwoFactorRememberTokenCommandTests.cs @@ -14,6 +14,24 @@ namespace Bit.Core.Test.Auth.UserFeatures.TwoFactorAuth; [SutProviderCustomize] public class IssueTwoFactorRememberTokenCommandTests { + private static readonly DateTime _expiration = new(2026, 11, 1, 12, 0, 0, DateTimeKind.Utc); + + /// + /// Stands in for the real factory: binds the user and device it is given, carries whatever stamp the + /// command generated, and expires at a fixed instant. + /// + private static void ArrangeFactory( + SutProvider sutProvider, User user, Device device) + { + sutProvider.GetDependency() + .CreateToken(user, device, Arg.Any()) + .Returns(c => new TwoFactorRememberTokenable( + user.Id, device.Id, device.Identifier, c.ArgAt(2), user.SecurityStamp) + { + ExpirationDate = _expiration, + }); + } + /// /// The stamp written to the row is the stamp embedded in the token, or validation could /// never succeed. @@ -25,6 +43,7 @@ public async Task IssueAsync_UpsertsRowWithStampMatchingToken( Device device) { device.UserId = user.Id; + ArrangeFactory(sutProvider, user, device); TwoFactorRememberToken? saved = null; sutProvider.GetDependency() .UpsertAsync(Arg.Do(t => saved = t)) @@ -60,6 +79,7 @@ public async Task IssueAsync_CalledTwice_WritesDifferentStampEachTime( Device device) { device.UserId = user.Id; + ArrangeFactory(sutProvider, user, device); var stamps = new List(); sutProvider.GetDependency() .UpsertAsync(Arg.Do(t => stamps.Add(t.Stamp))) @@ -73,7 +93,7 @@ public async Task IssueAsync_CalledTwice_WritesDifferentStampEachTime( } /// - /// The row and the token expire together, both derived from the one lifetime constant. + /// The row takes its expiry from the minted token, so the two lapse at the same instant. /// [Theory, BitAutoData] public async Task IssueAsync_RowAndTokenShareExpiration( @@ -82,6 +102,7 @@ public async Task IssueAsync_RowAndTokenShareExpiration( Device device) { device.UserId = user.Id; + ArrangeFactory(sutProvider, user, device); TwoFactorRememberToken? saved = null; sutProvider.GetDependency() .UpsertAsync(Arg.Do(t => saved = t)) @@ -92,17 +113,12 @@ public async Task IssueAsync_RowAndTokenShareExpiration( .Protect(Arg.Do(t => minted = t)) .Returns("protected-token"); - var before = DateTime.UtcNow; await sutProvider.Sut.IssueAsync(user, device); - var after = DateTime.UtcNow; Assert.NotNull(saved); Assert.NotNull(minted); - Assert.InRange( - saved.ExpirationDate, - before + TwoFactorRememberTokenable.GetTokenLifetime(), - after + TwoFactorRememberTokenable.GetTokenLifetime()); - Assert.Equal(saved.ExpirationDate, minted.ExpirationDate, TimeSpan.FromSeconds(1)); + Assert.Equal(_expiration, saved.ExpirationDate); + Assert.Equal(_expiration, minted.ExpirationDate); } /// @@ -116,6 +132,7 @@ public async Task IssueAsync_StampIsLowerCaseGuidText( Device device) { device.UserId = user.Id; + ArrangeFactory(sutProvider, user, device); TwoFactorRememberToken? saved = null; sutProvider.GetDependency() .UpsertAsync(Arg.Do(t => saved = t)) diff --git a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs index f365d8dd67a9..4a4a391013d3 100644 --- a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs +++ b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs @@ -42,6 +42,8 @@ private static TwoFactorRememberTokenable Tokenable(User user, Guid deviceId, st DeviceIdentifier = _deviceIdentifier, Stamp = stamp, SecurityStamp = user.SecurityStamp, + // Real clock: ExpiringTokenable reads DateTime.UtcNow directly. + ExpirationDate = DateTime.UtcNow.AddDays(30), }; private static TwoFactorRememberToken Row(User user, Guid deviceId, string stamp) => From afec27c9149ce88733f3243f37d78b4367a194b0 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Fri, 2 Oct 2026 10:56:27 -0500 Subject: [PATCH 14/27] [PM-40673 Part 1] update UpsertAsync to prevent concurrent duplicate insert --- .../TwoFactorRememberTokenRepository.cs | 87 +++++++++++++++---- .../TwoFactorRememberTokenRepositoryTests.cs | 85 +++++++++++++----- 2 files changed, 131 insertions(+), 41 deletions(-) diff --git a/src/Infrastructure.EntityFramework/Auth/Repositories/TwoFactorRememberTokenRepository.cs b/src/Infrastructure.EntityFramework/Auth/Repositories/TwoFactorRememberTokenRepository.cs index 5ddf8bca145f..671c65276104 100644 --- a/src/Infrastructure.EntityFramework/Auth/Repositories/TwoFactorRememberTokenRepository.cs +++ b/src/Infrastructure.EntityFramework/Auth/Repositories/TwoFactorRememberTokenRepository.cs @@ -12,7 +12,9 @@ public TwoFactorRememberTokenRepository(IServiceScopeFactory serviceScopeFactory : base(serviceScopeFactory, mapper) { } - public async Task GetByUserIdDeviceIdAsync(Guid userId, Guid deviceId) + // Virtual so tests can simulate a stale read, which is what the fallbacks in UpsertAsync handle. + public virtual async Task GetByUserIdDeviceIdAsync( + Guid userId, Guid deviceId) { using var scope = ServiceScopeFactory.CreateScope(); var dbContext = GetDatabaseContext(scope); @@ -24,35 +26,86 @@ public TwoFactorRememberTokenRepository(IServiceScopeFactory serviceScopeFactory public async Task UpsertAsync( Core.Auth.Entities.TwoFactorRememberToken token) + { + var existing = await GetByUserIdDeviceIdAsync(token.UserId, token.DeviceId); + if (existing != null && await TryUpdateAsync(token, existing)) + { + return token; + } + + // Either there is no row, or it was removed between the read and the update (the expiry + // sweep can do that). + token.SetNewId(); + + try + { + await CreateAsync(token); + } + catch (DbUpdateException e) when (IsDuplicateKeyException(e)) + { + // Unique-index backstop: a concurrent remember-login on the same device inserted the row + // between our read and our insert. Update the winner's row instead of failing the login. + existing = await GetByUserIdDeviceIdAsync(token.UserId, token.DeviceId); + if (existing == null || !await TryUpdateAsync(token, existing)) + { + throw; + } + } + + return token; + } + + private async Task TryUpdateAsync( + Core.Auth.Entities.TwoFactorRememberToken token, Core.Auth.Entities.TwoFactorRememberToken existing) { using var scope = ServiceScopeFactory.CreateScope(); var dbContext = GetDatabaseContext(scope); - var existing = await dbContext.TwoFactorRememberTokens - .FirstOrDefaultAsync(t => t.UserId == token.UserId && t.DeviceId == token.DeviceId); + var rowsUpdated = await dbContext.TwoFactorRememberTokens + .Where(t => t.Id == existing.Id) + .ExecuteUpdateAsync(s => s + .SetProperty(t => t.Stamp, token.Stamp) + .SetProperty(t => t.RevisionDate, token.RevisionDate) + .SetProperty(t => t.ExpirationDate, token.ExpirationDate)); - if (existing != null) + if (rowsUpdated == 0) { - // CreationDate is deliberately left alone so the row still records when the device was - // first remembered, matching the MSSQL procedure. - existing.Stamp = token.Stamp; - existing.RevisionDate = token.RevisionDate; - existing.ExpirationDate = token.ExpirationDate; - await dbContext.SaveChangesAsync(); - - token.Id = existing.Id; - token.CreationDate = existing.CreationDate; - return token; + return false; } - token.SetNewId(); + // CreationDate is left alone so the row still records when the device was first remembered, + // matching the MSSQL procedure. + token.Id = existing.Id; + token.CreationDate = existing.CreationDate; + return true; + } + + private async Task CreateAsync(Core.Auth.Entities.TwoFactorRememberToken token) + { + using var scope = ServiceScopeFactory.CreateScope(); + var dbContext = GetDatabaseContext(scope); + var entity = Mapper.Map(token); await dbContext.AddAsync(entity); await dbContext.SaveChangesAsync(); - - return token; } + /// + /// Recognises unique-index violations, which report different codes from primary-key ones on + /// SQL Server (2601 rather than 2627) and SQLite (2067 rather than 1555). + /// + private static bool IsDuplicateKeyException(DbUpdateException e) => e.InnerException switch + { + MySqlConnector.MySqlException my => my.ErrorCode == MySqlConnector.MySqlErrorCode.DuplicateKeyEntry, + Microsoft.Data.SqlClient.SqlException ms => ms.Errors + .Cast() + .Any(error => error.Number is 2601 or 2627), + Npgsql.PostgresException pg => pg.SqlState == "23505", + Microsoft.Data.Sqlite.SqliteException lite => lite.SqliteErrorCode == 19 + && lite.SqliteExtendedErrorCode is 1555 or 2067, + _ => false, + }; + public async Task RotateStampsByUserIdAsync(Guid userId) { using var scope = ServiceScopeFactory.CreateScope(); diff --git a/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs index ac062ca16cca..69c27476a8ac 100644 --- a/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs @@ -1,13 +1,15 @@ +using AutoMapper; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Repositories; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Utilities; -using Bit.Infrastructure.Dapper.Auth.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.IntegrationTest.AdminConsole; +using DapperTwoFactorRememberTokenRepository = Bit.Infrastructure.Dapper.Auth.Repositories.TwoFactorRememberTokenRepository; using EfTwoFactorRememberToken = Bit.Infrastructure.EntityFramework.Auth.Models.TwoFactorRememberToken; +using EfTwoFactorRememberTokenRepository = Bit.Infrastructure.EntityFramework.Auth.Repositories.TwoFactorRememberTokenRepository; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; @@ -114,7 +116,7 @@ public async Task UpsertAsync_SameUserAndDevice_UpdatesInPlaceAndPreservesCreati /// /// /// Whether the two calls actually collide is timing-dependent, so this is a regression guard on the - /// common path. The two tests that follow force the Dapper repository's fallbacks deterministically. + /// common path. The two tests that follow force each repository's fallbacks deterministically. /// [Theory, DatabaseData] public async Task UpsertAsync_Concurrent_BothCompleteAndOneRowSurvives( @@ -139,8 +141,8 @@ await Task.WhenAll( /// A repository whose first read returns a canned value, standing in for a read that was stale by /// the time the write happened. /// - private sealed class StaleFirstReadRepository(string connectionString, TwoFactorRememberToken? staleRow) - : TwoFactorRememberTokenRepository(connectionString, connectionString) + private sealed class StaleFirstReadDapperRepository(string connectionString, TwoFactorRememberToken? staleRow) + : DapperTwoFactorRememberTokenRepository(connectionString, connectionString) { private bool _served; @@ -156,6 +158,56 @@ private sealed class StaleFirstReadRepository(string connectionString, TwoFactor } } + /// + private sealed class StaleFirstReadEfRepository( + IServiceScopeFactory serviceScopeFactory, IMapper mapper, TwoFactorRememberToken? staleRow) + : EfTwoFactorRememberTokenRepository(serviceScopeFactory, mapper) + { + private bool _served; + + public override Task GetByUserIdDeviceIdAsync(Guid userId, Guid deviceId) + { + if (_served) + { + return base.GetByUserIdDeviceIdAsync(userId, deviceId); + } + + _served = true; + return Task.FromResult(staleRow); + } + } + + private static ITwoFactorRememberTokenRepository CreateStaleFirstReadRepository( + Database database, IServiceProvider services, TwoFactorRememberToken? staleRow) => + database.Type == SupportedDatabaseProviders.SqlServer && !database.UseEf + ? new StaleFirstReadDapperRepository(database.ConnectionString, staleRow) + : new StaleFirstReadEfRepository( + services.GetRequiredService(), + services.GetRequiredService(), + staleRow); + + /// + /// Deletes a token row at the database level, standing in for the expiry sweep. + /// + private static async Task DeleteTokenRowAtDatabaseLevelAsync( + IServiceProvider services, Database database, Guid id) + { + if (database.Type == SupportedDatabaseProviders.SqlServer && !database.UseEf) + { + await using var connection = new SqlConnection(database.ConnectionString); + await connection.OpenAsync(TestContext.Current.CancellationToken); + await using var command = connection.CreateCommand(); + command.CommandText = "DELETE FROM [dbo].[TwoFactorRememberToken] WHERE [Id] = @Id"; + command.Parameters.Add(new SqlParameter("@Id", id)); + await command.ExecuteNonQueryAsync(TestContext.Current.CancellationToken); + return; + } + + using var scope = services.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + await dbContext.TwoFactorRememberTokens.Where(r => r.Id == id).ExecuteDeleteAsync(TestContext.Current.CancellationToken); + } + /// /// The read finds no row, but a concurrent login inserts one before this call's insert runs. The /// unique index rejects the insert, and the repository must update the winner's row rather than @@ -164,22 +216,18 @@ private sealed class StaleFirstReadRepository(string connectionString, TwoFactor [Theory, DatabaseData] public async Task UpsertAsync_RowAppearsAfterRead_UpdatesTheWinnersRow( Database database, + IServiceProvider services, ITwoFactorRememberTokenRepository sut, IUserRepository userRepository, IDeviceRepository deviceRepository) { - if (database.Type != SupportedDatabaseProviders.SqlServer || database.UseEf) - { - Assert.Skip("Exercises the Dapper repository's duplicate-key fallback."); - } - var user = await userRepository.CreateTestUserAsync(); var device = await CreateTestDeviceAsync(deviceRepository, user.Id); await sut.UpsertAsync(NewToken(user.Id, device.Id, "winner")); var winner = await sut.GetByUserIdDeviceIdAsync(user.Id, device.Id); Assert.NotNull(winner); - var loser = new StaleFirstReadRepository(database.ConnectionString, staleRow: null); + var loser = CreateStaleFirstReadRepository(database, services, staleRow: null); await loser.UpsertAsync(NewToken(user.Id, device.Id, "loser")); var row = await sut.GetByUserIdDeviceIdAsync(user.Id, device.Id); @@ -197,31 +245,20 @@ public async Task UpsertAsync_RowAppearsAfterRead_UpdatesTheWinnersRow( [Theory, DatabaseData] public async Task UpsertAsync_RowDeletedAfterRead_CreatesTheRow( Database database, + IServiceProvider services, ITwoFactorRememberTokenRepository sut, IUserRepository userRepository, IDeviceRepository deviceRepository) { - if (database.Type != SupportedDatabaseProviders.SqlServer || database.UseEf) - { - Assert.Skip("Exercises the Dapper repository's update-matched-nothing fallback."); - } - var user = await userRepository.CreateTestUserAsync(); var device = await CreateTestDeviceAsync(deviceRepository, user.Id); await sut.UpsertAsync(NewToken(user.Id, device.Id, "original")); var staleRow = await sut.GetByUserIdDeviceIdAsync(user.Id, device.Id); Assert.NotNull(staleRow); - await using (var connection = new SqlConnection(database.ConnectionString)) - { - await connection.OpenAsync(TestContext.Current.CancellationToken); - await using var command = connection.CreateCommand(); - command.CommandText = "DELETE FROM [dbo].[TwoFactorRememberToken] WHERE [Id] = @Id"; - command.Parameters.Add(new SqlParameter("@Id", staleRow.Id)); - await command.ExecuteNonQueryAsync(TestContext.Current.CancellationToken); - } + await DeleteTokenRowAtDatabaseLevelAsync(services, database, staleRow.Id); - var repository = new StaleFirstReadRepository(database.ConnectionString, staleRow); + var repository = CreateStaleFirstReadRepository(database, services, staleRow); await repository.UpsertAsync(NewToken(user.Id, device.Id, "replacement")); var row = await sut.GetByUserIdDeviceIdAsync(user.Id, device.Id); From 89181ff5f60b2916069d2e664b42639e41eaca14 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Fri, 2 Oct 2026 11:07:03 -0500 Subject: [PATCH 15/27] [PM-40673 Part 1] increase DeleteManyExpired batch size to 1000 --- .../TwoFactorRememberToken_DeleteManyExpired.sql | 2 +- .../DbScripts/2026-09-22_00_CreateTwoFactorRememberToken.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_DeleteManyExpired.sql b/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_DeleteManyExpired.sql index df061e154249..2a0091552cb9 100644 --- a/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_DeleteManyExpired.sql +++ b/src/Sql/dbo/Auth/Stored Procedures/TwoFactorRememberToken_DeleteManyExpired.sql @@ -7,7 +7,7 @@ BEGIN -- Deleted in batches so each statement holds few enough locks to avoid escalating to a table -- lock, which would block logins while the sweep runs. The loop ends when a batch deletes -- nothing. - DECLARE @BatchSize INT = 100 + DECLARE @BatchSize INT = 1000 WHILE @BatchSize > 0 BEGIN diff --git a/util/Migrator/DbScripts/2026-09-22_00_CreateTwoFactorRememberToken.sql b/util/Migrator/DbScripts/2026-09-22_00_CreateTwoFactorRememberToken.sql index 75589ddb08b6..a0ef9ee01dda 100644 --- a/util/Migrator/DbScripts/2026-09-22_00_CreateTwoFactorRememberToken.sql +++ b/util/Migrator/DbScripts/2026-09-22_00_CreateTwoFactorRememberToken.sql @@ -147,7 +147,7 @@ BEGIN -- Deleted in batches so each statement holds few enough locks to avoid escalating to a table -- lock, which would block logins while the sweep runs. The loop ends when a batch deletes -- nothing. - DECLARE @BatchSize INT = 100 + DECLARE @BatchSize INT = 1000 WHILE @BatchSize > 0 BEGIN From edda6339b22d188089efddb5da4996164243ed57 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Fri, 2 Oct 2026 11:28:16 -0500 Subject: [PATCH 16/27] [PM-40673 Part 1] restore snapshots and re-generate migrations --- ...10-02_00_CreateTwoFactorRememberToken.sql} | 0 ..._CreateTwoFactorRememberToken.Designer.cs} | 162 ++++++++++++++++-- ...002161550_CreateTwoFactorRememberToken.cs} | 0 ..._CreateTwoFactorRememberToken.Designer.cs} | 162 ++++++++++++++++-- ...002161429_CreateTwoFactorRememberToken.cs} | 0 ..._CreateTwoFactorRememberToken.Designer.cs} | 162 ++++++++++++++++-- ...002161440_CreateTwoFactorRememberToken.cs} | 0 7 files changed, 435 insertions(+), 51 deletions(-) rename util/Migrator/DbScripts/{2026-09-22_00_CreateTwoFactorRememberToken.sql => 2026-10-02_00_CreateTwoFactorRememberToken.sql} (100%) rename util/MySqlMigrations/Migrations/{20260923010021_CreateTwoFactorRememberToken.Designer.cs => 20261002161550_CreateTwoFactorRememberToken.Designer.cs} (96%) rename util/MySqlMigrations/Migrations/{20260923010021_CreateTwoFactorRememberToken.cs => 20261002161550_CreateTwoFactorRememberToken.cs} (100%) rename util/PostgresMigrations/Migrations/{20260923010041_CreateTwoFactorRememberToken.Designer.cs => 20261002161429_CreateTwoFactorRememberToken.Designer.cs} (96%) rename util/PostgresMigrations/Migrations/{20260923010041_CreateTwoFactorRememberToken.cs => 20261002161429_CreateTwoFactorRememberToken.cs} (100%) rename util/SqliteMigrations/Migrations/{20260923010055_CreateTwoFactorRememberToken.Designer.cs => 20261002161440_CreateTwoFactorRememberToken.Designer.cs} (96%) rename util/SqliteMigrations/Migrations/{20260923010055_CreateTwoFactorRememberToken.cs => 20261002161440_CreateTwoFactorRememberToken.cs} (100%) diff --git a/util/Migrator/DbScripts/2026-09-22_00_CreateTwoFactorRememberToken.sql b/util/Migrator/DbScripts/2026-10-02_00_CreateTwoFactorRememberToken.sql similarity index 100% rename from util/Migrator/DbScripts/2026-09-22_00_CreateTwoFactorRememberToken.sql rename to util/Migrator/DbScripts/2026-10-02_00_CreateTwoFactorRememberToken.sql diff --git a/util/MySqlMigrations/Migrations/20260923010021_CreateTwoFactorRememberToken.Designer.cs b/util/MySqlMigrations/Migrations/20261002161550_CreateTwoFactorRememberToken.Designer.cs similarity index 96% rename from util/MySqlMigrations/Migrations/20260923010021_CreateTwoFactorRememberToken.Designer.cs rename to util/MySqlMigrations/Migrations/20261002161550_CreateTwoFactorRememberToken.Designer.cs index f95c632d7809..ba33c89ca2ce 100644 --- a/util/MySqlMigrations/Migrations/20260923010021_CreateTwoFactorRememberToken.Designer.cs +++ b/util/MySqlMigrations/Migrations/20261002161550_CreateTwoFactorRememberToken.Designer.cs @@ -12,7 +12,7 @@ namespace Bit.MySqlMigrations.Migrations { [DbContext(typeof(DatabaseContext))] - [Migration("20260923010021_CreateTwoFactorRememberToken")] + [Migration("20261002161550_CreateTwoFactorRememberToken")] partial class CreateTwoFactorRememberToken { /// @@ -238,6 +238,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("LimitItemDeletion") .HasColumnType("tinyint(1)"); + b.Property("MaxAutoscalePamSeats") + .HasColumnType("int"); + b.Property("MaxAutoscaleSeats") .HasColumnType("int"); @@ -261,6 +264,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("OwnersNotifiedOfAutoscaling") .HasColumnType("datetime(6)"); + b.Property("PamSeats") + .HasColumnType("int"); + b.Property("Plan") .IsRequired() .HasMaxLength(50) @@ -2464,6 +2470,112 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("NotificationStatus", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessAuditEvent", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessConnectorId") + .HasColumnType("char(36)"); + + b.Property("AccessConnectorName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("AccessLeaseId") + .HasColumnType("char(36)"); + + b.Property("AccessRequestId") + .HasColumnType("char(36)"); + + b.Property("AccessRuleId") + .HasColumnType("char(36)"); + + b.Property("ActorEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ActorId") + .HasColumnType("char(36)"); + + b.Property("ActorName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CorrelationId") + .HasColumnType("char(36)"); + + b.Property("Detail") + .HasColumnType("longtext"); + + b.Property("Kind") + .HasColumnType("tinyint unsigned"); + + b.Property("LeaseNotAfter") + .HasColumnType("datetime(6)"); + + b.Property("LeaseNotBefore") + .HasColumnType("datetime(6)"); + + b.Property("OccurredDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Phase") + .HasColumnType("tinyint unsigned"); + + b.Property("RequesterEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("RequesterId") + .HasColumnType("char(36)"); + + b.Property("RequesterName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("RotationConfigId") + .HasColumnType("char(36)"); + + b.Property("RotationJobId") + .HasColumnType("char(36)"); + + b.Property("RotationSource") + .HasColumnType("tinyint unsigned"); + + b.Property("RuleName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("SyncState") + .HasColumnType("tinyint unsigned"); + + b.Property("TargetSystemId") + .HasColumnType("char(36)"); + + b.Property("TargetSystemName") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OrganizationId", "OccurredDate", "Id") + .IsDescending(false, true, true); + + b.ToTable("AccessAuditEvent", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessDecision", b => { b.Property("Id") @@ -2508,6 +2620,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("AccessRequestId") .HasColumnType("char(36)"); + b.Property("Action") + .HasColumnType("tinyint unsigned"); + b.Property("CipherId") .HasColumnType("char(36)"); @@ -2535,9 +2650,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("RevokedDate") .HasColumnType("datetime(6)"); - b.Property("Status") - .HasColumnType("tinyint unsigned"); - b.HasKey("Id"); b.HasIndex("AccessRequestId") @@ -2545,13 +2657,14 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasIndex("OrganizationId"); - b.HasIndex("CipherId", "Status"); + b.HasIndex("CollectionId", "Action"); - b.HasIndex("CollectionId", "Status"); + b.HasIndex("NotAfter", "Action"); - b.HasIndex("NotAfter", "Status"); + b.HasIndex("CipherId", "Action", "NotAfter") + .IsDescending(false, false, true); - b.HasIndex("RequesterId", "CipherId", "Status"); + b.HasIndex("RequesterId", "CipherId", "Action"); b.ToTable("AccessLease", (string)null); }); @@ -2561,6 +2674,12 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("Id") .HasColumnType("char(36)"); + b.Property("Action") + .HasColumnType("tinyint unsigned"); + + b.Property("ActionDate") + .HasColumnType("datetime(6)"); + b.Property("CipherId") .HasColumnType("char(36)"); @@ -2588,26 +2707,24 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("RequesterId") .HasColumnType("char(36)"); - b.Property("ResolvedDate") - .HasColumnType("datetime(6)"); - b.Property("RuleId") .HasColumnType("char(36)"); - b.Property("Status") - .HasColumnType("tinyint unsigned"); - b.HasKey("Id"); b.HasIndex("ExtensionOfLeaseId"); b.HasIndex("RuleId"); - b.HasIndex("CollectionId", "Status"); + b.HasIndex("CollectionId", "CreationDate"); + + b.HasIndex("OrganizationId", "Action"); - b.HasIndex("OrganizationId", "Status"); + b.HasIndex("RequesterId", "CreationDate"); - b.HasIndex("RequesterId", "CipherId", "Status"); + b.HasIndex("CollectionId", "Action", "NotAfter"); + + b.HasIndex("RequesterId", "CipherId", "Action"); b.ToTable("AccessRequest", (string)null); }); @@ -3766,6 +3883,17 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessAuditEvent", 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.Pam.Models.AccessDecision", b => { b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRequest", null) diff --git a/util/MySqlMigrations/Migrations/20260923010021_CreateTwoFactorRememberToken.cs b/util/MySqlMigrations/Migrations/20261002161550_CreateTwoFactorRememberToken.cs similarity index 100% rename from util/MySqlMigrations/Migrations/20260923010021_CreateTwoFactorRememberToken.cs rename to util/MySqlMigrations/Migrations/20261002161550_CreateTwoFactorRememberToken.cs diff --git a/util/PostgresMigrations/Migrations/20260923010041_CreateTwoFactorRememberToken.Designer.cs b/util/PostgresMigrations/Migrations/20261002161429_CreateTwoFactorRememberToken.Designer.cs similarity index 96% rename from util/PostgresMigrations/Migrations/20260923010041_CreateTwoFactorRememberToken.Designer.cs rename to util/PostgresMigrations/Migrations/20261002161429_CreateTwoFactorRememberToken.Designer.cs index 4d8e979eef64..c4b2ebb0c4e1 100644 --- a/util/PostgresMigrations/Migrations/20260923010041_CreateTwoFactorRememberToken.Designer.cs +++ b/util/PostgresMigrations/Migrations/20261002161429_CreateTwoFactorRememberToken.Designer.cs @@ -12,7 +12,7 @@ namespace Bit.PostgresMigrations.Migrations { [DbContext(typeof(DatabaseContext))] - [Migration("20260923010041_CreateTwoFactorRememberToken")] + [Migration("20261002161429_CreateTwoFactorRememberToken")] partial class CreateTwoFactorRememberToken { /// @@ -240,6 +240,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("LimitItemDeletion") .HasColumnType("boolean"); + b.Property("MaxAutoscalePamSeats") + .HasColumnType("integer"); + b.Property("MaxAutoscaleSeats") .HasColumnType("integer"); @@ -263,6 +266,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("OwnersNotifiedOfAutoscaling") .HasColumnType("timestamp with time zone"); + b.Property("PamSeats") + .HasColumnType("integer"); + b.Property("Plan") .IsRequired() .HasMaxLength(50) @@ -2470,6 +2476,112 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("NotificationStatus", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessAuditEvent", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessConnectorId") + .HasColumnType("uuid"); + + b.Property("AccessConnectorName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("AccessLeaseId") + .HasColumnType("uuid"); + + b.Property("AccessRequestId") + .HasColumnType("uuid"); + + b.Property("AccessRuleId") + .HasColumnType("uuid"); + + b.Property("ActorEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ActorId") + .HasColumnType("uuid"); + + b.Property("ActorName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("Detail") + .HasColumnType("text"); + + b.Property("Kind") + .HasColumnType("smallint"); + + b.Property("LeaseNotAfter") + .HasColumnType("timestamp with time zone"); + + b.Property("LeaseNotBefore") + .HasColumnType("timestamp with time zone"); + + b.Property("OccurredDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Phase") + .HasColumnType("smallint"); + + b.Property("RequesterEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RequesterId") + .HasColumnType("uuid"); + + b.Property("RequesterName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RotationConfigId") + .HasColumnType("uuid"); + + b.Property("RotationJobId") + .HasColumnType("uuid"); + + b.Property("RotationSource") + .HasColumnType("smallint"); + + b.Property("RuleName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SyncState") + .HasColumnType("smallint"); + + b.Property("TargetSystemId") + .HasColumnType("uuid"); + + b.Property("TargetSystemName") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OrganizationId", "OccurredDate", "Id") + .IsDescending(false, true, true); + + b.ToTable("AccessAuditEvent", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessDecision", b => { b.Property("Id") @@ -2514,6 +2626,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("AccessRequestId") .HasColumnType("uuid"); + b.Property("Action") + .HasColumnType("smallint"); + b.Property("CipherId") .HasColumnType("uuid"); @@ -2541,9 +2656,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("RevokedDate") .HasColumnType("timestamp with time zone"); - b.Property("Status") - .HasColumnType("smallint"); - b.HasKey("Id"); b.HasIndex("AccessRequestId") @@ -2551,13 +2663,14 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasIndex("OrganizationId"); - b.HasIndex("CipherId", "Status"); + b.HasIndex("CollectionId", "Action"); - b.HasIndex("CollectionId", "Status"); + b.HasIndex("NotAfter", "Action"); - b.HasIndex("NotAfter", "Status"); + b.HasIndex("CipherId", "Action", "NotAfter") + .IsDescending(false, false, true); - b.HasIndex("RequesterId", "CipherId", "Status"); + b.HasIndex("RequesterId", "CipherId", "Action"); b.ToTable("AccessLease", (string)null); }); @@ -2567,6 +2680,12 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("Id") .HasColumnType("uuid"); + b.Property("Action") + .HasColumnType("smallint"); + + b.Property("ActionDate") + .HasColumnType("timestamp with time zone"); + b.Property("CipherId") .HasColumnType("uuid"); @@ -2594,26 +2713,24 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("RequesterId") .HasColumnType("uuid"); - b.Property("ResolvedDate") - .HasColumnType("timestamp with time zone"); - b.Property("RuleId") .HasColumnType("uuid"); - b.Property("Status") - .HasColumnType("smallint"); - b.HasKey("Id"); b.HasIndex("ExtensionOfLeaseId"); b.HasIndex("RuleId"); - b.HasIndex("CollectionId", "Status"); + b.HasIndex("CollectionId", "CreationDate"); + + b.HasIndex("OrganizationId", "Action"); - b.HasIndex("OrganizationId", "Status"); + b.HasIndex("RequesterId", "CreationDate"); - b.HasIndex("RequesterId", "CipherId", "Status"); + b.HasIndex("CollectionId", "Action", "NotAfter"); + + b.HasIndex("RequesterId", "CipherId", "Action"); b.ToTable("AccessRequest", (string)null); }); @@ -3772,6 +3889,17 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessAuditEvent", 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.Pam.Models.AccessDecision", b => { b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRequest", null) diff --git a/util/PostgresMigrations/Migrations/20260923010041_CreateTwoFactorRememberToken.cs b/util/PostgresMigrations/Migrations/20261002161429_CreateTwoFactorRememberToken.cs similarity index 100% rename from util/PostgresMigrations/Migrations/20260923010041_CreateTwoFactorRememberToken.cs rename to util/PostgresMigrations/Migrations/20261002161429_CreateTwoFactorRememberToken.cs diff --git a/util/SqliteMigrations/Migrations/20260923010055_CreateTwoFactorRememberToken.Designer.cs b/util/SqliteMigrations/Migrations/20261002161440_CreateTwoFactorRememberToken.Designer.cs similarity index 96% rename from util/SqliteMigrations/Migrations/20260923010055_CreateTwoFactorRememberToken.Designer.cs rename to util/SqliteMigrations/Migrations/20261002161440_CreateTwoFactorRememberToken.Designer.cs index 02b9f3f65f09..782214452b01 100644 --- a/util/SqliteMigrations/Migrations/20260923010055_CreateTwoFactorRememberToken.Designer.cs +++ b/util/SqliteMigrations/Migrations/20261002161440_CreateTwoFactorRememberToken.Designer.cs @@ -11,7 +11,7 @@ namespace Bit.SqliteMigrations.Migrations { [DbContext(typeof(DatabaseContext))] - [Migration("20260923010055_CreateTwoFactorRememberToken")] + [Migration("20261002161440_CreateTwoFactorRememberToken")] partial class CreateTwoFactorRememberToken { /// @@ -233,6 +233,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("LimitItemDeletion") .HasColumnType("INTEGER"); + b.Property("MaxAutoscalePamSeats") + .HasColumnType("INTEGER"); + b.Property("MaxAutoscaleSeats") .HasColumnType("INTEGER"); @@ -256,6 +259,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("OwnersNotifiedOfAutoscaling") .HasColumnType("TEXT"); + b.Property("PamSeats") + .HasColumnType("INTEGER"); + b.Property("Plan") .IsRequired() .HasMaxLength(50) @@ -2453,6 +2459,112 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("NotificationStatus", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessAuditEvent", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessConnectorId") + .HasColumnType("TEXT"); + + b.Property("AccessConnectorName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("AccessLeaseId") + .HasColumnType("TEXT"); + + b.Property("AccessRequestId") + .HasColumnType("TEXT"); + + b.Property("AccessRuleId") + .HasColumnType("TEXT"); + + b.Property("ActorEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("ActorId") + .HasColumnType("TEXT"); + + b.Property("ActorName") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CollectionId") + .HasColumnType("TEXT"); + + b.Property("CorrelationId") + .HasColumnType("TEXT"); + + b.Property("Detail") + .HasColumnType("TEXT"); + + b.Property("Kind") + .HasColumnType("INTEGER"); + + b.Property("LeaseNotAfter") + .HasColumnType("TEXT"); + + b.Property("LeaseNotBefore") + .HasColumnType("TEXT"); + + b.Property("OccurredDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("Phase") + .HasColumnType("INTEGER"); + + b.Property("RequesterEmail") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("RequesterId") + .HasColumnType("TEXT"); + + b.Property("RequesterName") + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("RotationConfigId") + .HasColumnType("TEXT"); + + b.Property("RotationJobId") + .HasColumnType("TEXT"); + + b.Property("RotationSource") + .HasColumnType("INTEGER"); + + b.Property("RuleName") + .HasMaxLength(256) + .HasColumnType("TEXT"); + + b.Property("SyncState") + .HasColumnType("INTEGER"); + + b.Property("TargetSystemId") + .HasColumnType("TEXT"); + + b.Property("TargetSystemName") + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CorrelationId"); + + b.HasIndex("OrganizationId", "OccurredDate", "Id") + .IsDescending(false, true, true); + + b.ToTable("AccessAuditEvent", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessDecision", b => { b.Property("Id") @@ -2497,6 +2609,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("AccessRequestId") .HasColumnType("TEXT"); + b.Property("Action") + .HasColumnType("INTEGER"); + b.Property("CipherId") .HasColumnType("TEXT"); @@ -2524,9 +2639,6 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("RevokedDate") .HasColumnType("TEXT"); - b.Property("Status") - .HasColumnType("INTEGER"); - b.HasKey("Id"); b.HasIndex("AccessRequestId") @@ -2534,13 +2646,14 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.HasIndex("OrganizationId"); - b.HasIndex("CipherId", "Status"); + b.HasIndex("CollectionId", "Action"); - b.HasIndex("CollectionId", "Status"); + b.HasIndex("NotAfter", "Action"); - b.HasIndex("NotAfter", "Status"); + b.HasIndex("CipherId", "Action", "NotAfter") + .IsDescending(false, false, true); - b.HasIndex("RequesterId", "CipherId", "Status"); + b.HasIndex("RequesterId", "CipherId", "Action"); b.ToTable("AccessLease", (string)null); }); @@ -2550,6 +2663,12 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("Id") .HasColumnType("TEXT"); + b.Property("Action") + .HasColumnType("INTEGER"); + + b.Property("ActionDate") + .HasColumnType("TEXT"); + b.Property("CipherId") .HasColumnType("TEXT"); @@ -2577,26 +2696,24 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("RequesterId") .HasColumnType("TEXT"); - b.Property("ResolvedDate") - .HasColumnType("TEXT"); - b.Property("RuleId") .HasColumnType("TEXT"); - b.Property("Status") - .HasColumnType("INTEGER"); - b.HasKey("Id"); b.HasIndex("ExtensionOfLeaseId"); b.HasIndex("RuleId"); - b.HasIndex("CollectionId", "Status"); + b.HasIndex("CollectionId", "CreationDate"); + + b.HasIndex("OrganizationId", "Action"); - b.HasIndex("OrganizationId", "Status"); + b.HasIndex("RequesterId", "CreationDate"); - b.HasIndex("RequesterId", "CipherId", "Status"); + b.HasIndex("CollectionId", "Action", "NotAfter"); + + b.HasIndex("RequesterId", "CipherId", "Action"); b.ToTable("AccessRequest", (string)null); }); @@ -3755,6 +3872,17 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("User"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.AccessAuditEvent", 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.Pam.Models.AccessDecision", b => { b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessRequest", null) diff --git a/util/SqliteMigrations/Migrations/20260923010055_CreateTwoFactorRememberToken.cs b/util/SqliteMigrations/Migrations/20261002161440_CreateTwoFactorRememberToken.cs similarity index 100% rename from util/SqliteMigrations/Migrations/20260923010055_CreateTwoFactorRememberToken.cs rename to util/SqliteMigrations/Migrations/20261002161440_CreateTwoFactorRememberToken.cs From 4859929f4bb0a25f649c81c1da37cae3ecf9c786 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Fri, 2 Oct 2026 11:40:20 -0500 Subject: [PATCH 17/27] [PM-40673 Part 1] update trigger to be unique for remember token deletion --- src/Admin/Jobs/JobsHostedService.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Admin/Jobs/JobsHostedService.cs b/src/Admin/Jobs/JobsHostedService.cs index 864e2cb19319..62cfbfab7eb2 100644 --- a/src/Admin/Jobs/JobsHostedService.cs +++ b/src/Admin/Jobs/JobsHostedService.cs @@ -44,6 +44,11 @@ public override async Task StartAsync(CancellationToken cancellationToken) .StartNow() .WithCronSchedule("0 0 22 ? * FRI", x => x.InTimeZone(timeZone)) .Build(); + var everyFridayAt10pmTwoFactorRememberTokensTrigger = TriggerBuilder.Create() + .WithIdentity("EveryFridayAt10pmTwoFactorRememberTokensTrigger") + .StartNow() + .WithCronSchedule("0 0 22 ? * FRI", x => x.InTimeZone(timeZone)) + .Build(); var everySaturdayAtMidnightTrigger = TriggerBuilder.Create() .WithIdentity("EverySaturdayAtMidnightTrigger") .StartNow() @@ -88,7 +93,7 @@ public override async Task StartAsync(CancellationToken cancellationToken) { new Tuple(typeof(DeleteSendsJob), everyFiveMinutesTrigger), new Tuple(typeof(DatabaseExpiredGrantsJob), everyFridayAt10pmTrigger), - new Tuple(typeof(DeleteExpiredTwoFactorRememberTokensJob), everyFridayAt10pmTrigger), + new Tuple(typeof(DeleteExpiredTwoFactorRememberTokensJob), everyFridayAt10pmTwoFactorRememberTokensTrigger), new Tuple(typeof(DeleteCiphersJob), everyDayAtMidnightUtc), new Tuple(typeof(DatabaseExpiredSponsorshipsJob), everyMondayAtMidnightTrigger), new Tuple(typeof(DeleteAuthRequestsJob), everyFifteenMinutesTrigger), From 1c9958533b30ce51f83003cddce6c8e5c1e910ec Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Fri, 2 Oct 2026 11:43:32 -0500 Subject: [PATCH 18/27] [PM-40673 Part 1] remove unused row variable --- .../Implementations/IssueTwoFactorRememberTokenCommand.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/IssueTwoFactorRememberTokenCommand.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/IssueTwoFactorRememberTokenCommand.cs index ccb8a53922ad..72e8a9598d23 100644 --- a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/IssueTwoFactorRememberTokenCommand.cs +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/IssueTwoFactorRememberTokenCommand.cs @@ -24,7 +24,7 @@ public async Task IssueAsync(User user, Device device) // The row takes its expiry from the token, so the two always lapse together. var tokenable = tokenableFactory.CreateToken(user, device, stamp); - var row = await twoFactorRememberTokenRepository.UpsertAsync(new TwoFactorRememberToken + await twoFactorRememberTokenRepository.UpsertAsync(new TwoFactorRememberToken { UserId = user.Id, DeviceId = device.Id, From e87bf089f8ae89ef30b9184ff85adb41b9be8edb Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Fri, 2 Oct 2026 11:52:37 -0500 Subject: [PATCH 19/27] [PM-40673 Part 1] update rotate stamp methods to pass in revision time as param --- .../ITwoFactorRememberTokenRepository.cs | 6 +++- .../RevokeTwoFactorRememberTokensCommand.cs | 5 +-- .../TwoFactorRememberTokenRepository.cs | 4 +-- .../TwoFactorRememberTokenRepository.cs | 3 +- ...vokeTwoFactorRememberTokensCommandTests.cs | 33 ++++++++++++------- ...alidateTwoFactorRememberTokenQueryTests.cs | 2 +- .../TwoFactorRememberTokenRepositoryTests.cs | 4 +-- 7 files changed, 36 insertions(+), 21 deletions(-) diff --git a/src/Core/Auth/Repositories/ITwoFactorRememberTokenRepository.cs b/src/Core/Auth/Repositories/ITwoFactorRememberTokenRepository.cs index ebf93c7a6df0..58efb6e1c479 100644 --- a/src/Core/Auth/Repositories/ITwoFactorRememberTokenRepository.cs +++ b/src/Core/Auth/Repositories/ITwoFactorRememberTokenRepository.cs @@ -28,7 +28,11 @@ public interface ITwoFactorRememberTokenRepository /// The new stamp is generated here rather than by the caller: nothing above this layer needs to /// know the value, and generating it in one place keeps every provider writing the same thing. /// - Task RotateStampsByUserIdAsync(Guid userId); + /// + /// The value written to RevisionDate on every rotated row, supplied by the caller so that + /// every provider writes one instant. + /// + Task RotateStampsByUserIdAsync(Guid userId, DateTime revisionDate); /// /// Deletes every row that expired before . Hygiene only — an expired row is diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/RevokeTwoFactorRememberTokensCommand.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/RevokeTwoFactorRememberTokensCommand.cs index 2efcc929be12..cc5bcf3bb7df 100644 --- a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/RevokeTwoFactorRememberTokensCommand.cs +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/RevokeTwoFactorRememberTokensCommand.cs @@ -3,9 +3,10 @@ namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations; public class RevokeTwoFactorRememberTokensCommand( - ITwoFactorRememberTokenRepository twoFactorRememberTokenRepository) + ITwoFactorRememberTokenRepository twoFactorRememberTokenRepository, + TimeProvider timeProvider) : IRevokeTwoFactorRememberTokensCommand { public Task RevokeAllForUserAsync(Guid userId) => - twoFactorRememberTokenRepository.RotateStampsByUserIdAsync(userId); + twoFactorRememberTokenRepository.RotateStampsByUserIdAsync(userId, timeProvider.GetUtcNow().UtcDateTime); } diff --git a/src/Infrastructure.Dapper/Auth/Repositories/TwoFactorRememberTokenRepository.cs b/src/Infrastructure.Dapper/Auth/Repositories/TwoFactorRememberTokenRepository.cs index d203915be7dd..6e85b37d5dc3 100644 --- a/src/Infrastructure.Dapper/Auth/Repositories/TwoFactorRememberTokenRepository.cs +++ b/src/Infrastructure.Dapper/Auth/Repositories/TwoFactorRememberTokenRepository.cs @@ -113,7 +113,7 @@ await connection.ExecuteAsync( commandType: CommandType.StoredProcedure); } - public async Task RotateStampsByUserIdAsync(Guid userId) + public async Task RotateStampsByUserIdAsync(Guid userId, DateTime revisionDate) { await using var connection = new SqlConnection(ConnectionString); @@ -126,7 +126,7 @@ await connection.ExecuteAsync( // implementations write the same shape of value. One stamp covers every row for the // user; see the procedure for why that is safe. Stamp = Guid.NewGuid().ToString(), - RevisionDate = DateTime.UtcNow, + RevisionDate = revisionDate, }, commandType: CommandType.StoredProcedure); } diff --git a/src/Infrastructure.EntityFramework/Auth/Repositories/TwoFactorRememberTokenRepository.cs b/src/Infrastructure.EntityFramework/Auth/Repositories/TwoFactorRememberTokenRepository.cs index 671c65276104..e2dff22bb55e 100644 --- a/src/Infrastructure.EntityFramework/Auth/Repositories/TwoFactorRememberTokenRepository.cs +++ b/src/Infrastructure.EntityFramework/Auth/Repositories/TwoFactorRememberTokenRepository.cs @@ -106,7 +106,7 @@ private async Task CreateAsync(Core.Auth.Entities.TwoFactorRememberToken token) _ => false, }; - public async Task RotateStampsByUserIdAsync(Guid userId) + public async Task RotateStampsByUserIdAsync(Guid userId, DateTime revisionDate) { using var scope = ServiceScopeFactory.CreateScope(); var dbContext = GetDatabaseContext(scope); @@ -115,7 +115,6 @@ public async Task RotateStampsByUserIdAsync(Guid userId) // the MSSQL procedure write identical values. Rows are located by (UserId, DeviceId), so the // stamp never selects a row and a token naming one device cannot match another's. var stamp = Guid.NewGuid().ToString(); - var revisionDate = DateTime.UtcNow; await dbContext.TwoFactorRememberTokens .Where(t => t.UserId == userId) diff --git a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/RevokeTwoFactorRememberTokensCommandTests.cs b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/RevokeTwoFactorRememberTokensCommandTests.cs index 2c2b2101f2d9..0f9222acf3d8 100644 --- a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/RevokeTwoFactorRememberTokensCommandTests.cs +++ b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/RevokeTwoFactorRememberTokensCommandTests.cs @@ -2,6 +2,7 @@ using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; +using Microsoft.Extensions.Time.Testing; using NSubstitute; using Xunit; @@ -10,16 +11,27 @@ namespace Bit.Core.Test.Auth.UserFeatures.TwoFactorAuth; [SutProviderCustomize] public class RevokeTwoFactorRememberTokensCommandTests { + private static readonly DateTime _now = new(2026, 10, 2, 12, 0, 0, DateTimeKind.Utc); + + private static SutProvider GetSutProvider() + { + var sutProvider = new SutProvider() + .WithFakeTimeProvider() + .Create(); + sutProvider.GetDependency().SetUtcNow(_now); + return sutProvider; + } + [Theory, BitAutoData] - public async Task RevokeAllForUserAsync_RotatesStampsForThatUser( - SutProvider sutProvider, - Guid userId) + public async Task RevokeAllForUserAsync_RotatesStampsForThatUser(Guid userId) { + var sutProvider = GetSutProvider(); + await sutProvider.Sut.RevokeAllForUserAsync(userId); await sutProvider.GetDependency() .Received(1) - .RotateStampsByUserIdAsync(userId); + .RotateStampsByUserIdAsync(userId, _now); } /// @@ -27,10 +39,10 @@ await sutProvider.GetDependency() /// of when each device was first remembered. /// [Theory, BitAutoData] - public async Task RevokeAllForUserAsync_DoesNotDeleteRows( - SutProvider sutProvider, - Guid userId) + public async Task RevokeAllForUserAsync_DoesNotDeleteRows(Guid userId) { + var sutProvider = GetSutProvider(); + await sutProvider.Sut.RevokeAllForUserAsync(userId); await sutProvider.GetDependency() @@ -43,12 +55,11 @@ await sutProvider.GetDependency() /// teardown so that a failure leaves a state the caller can retry. /// [Theory, BitAutoData] - public async Task RevokeAllForUserAsync_RepositoryThrows_Propagates( - SutProvider sutProvider, - Guid userId) + public async Task RevokeAllForUserAsync_RepositoryThrows_Propagates(Guid userId) { + var sutProvider = GetSutProvider(); sutProvider.GetDependency() - .RotateStampsByUserIdAsync(userId) + .RotateStampsByUserIdAsync(userId, _now) .Returns(Task.FromException(new InvalidOperationException("database unavailable"))); await Assert.ThrowsAsync( diff --git a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs index 4a4a391013d3..7ef496379024 100644 --- a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs +++ b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs @@ -279,7 +279,7 @@ public async Task ValidateAsync_Always_DoesNotWrite( var repository = sutProvider.GetDependency(); await repository.DidNotReceiveWithAnyArgs().UpsertAsync(default!); - await repository.DidNotReceiveWithAnyArgs().RotateStampsByUserIdAsync(default); + await repository.DidNotReceiveWithAnyArgs().RotateStampsByUserIdAsync(default, default); await repository.DidNotReceiveWithAnyArgs().DeleteExpiredAsync(default); } } diff --git a/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs index 69c27476a8ac..4cc1603c0b8b 100644 --- a/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs @@ -288,7 +288,7 @@ public async Task RotateStampsByUserIdAsync_RotatesEveryRowForThatUser( Assert.NotNull(beforeOne); Assert.NotNull(beforeTwo); - await sut.RotateStampsByUserIdAsync(user.Id); + await sut.RotateStampsByUserIdAsync(user.Id, DateTime.UtcNow); var afterOne = await sut.GetByUserIdDeviceIdAsync(user.Id, deviceOne.Id); var afterTwo = await sut.GetByUserIdDeviceIdAsync(user.Id, deviceTwo.Id); @@ -322,7 +322,7 @@ public async Task RotateStampsByUserIdAsync_LeavesOtherUsersAlone( await sut.UpsertAsync(NewToken(revokedUser.Id, revokedDevice.Id, "stamp-revoked")); await sut.UpsertAsync(NewToken(untouchedUser.Id, untouchedDevice.Id, "stamp-untouched")); - await sut.RotateStampsByUserIdAsync(revokedUser.Id); + await sut.RotateStampsByUserIdAsync(revokedUser.Id, DateTime.UtcNow); var untouched = await sut.GetByUserIdDeviceIdAsync(untouchedUser.Id, untouchedDevice.Id); From 2717b9c1d49ca1ea323e0a4fff3bcc75934b0ae0 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Sun, 4 Oct 2026 21:29:33 -0500 Subject: [PATCH 20/27] [PM-40673 Part 1] use CombGuid.Generate() instead of obselete CoreHelpers.GenerateComb() --- src/Core/Auth/Entities/TwoFactorRememberToken.cs | 2 +- .../Repositories/TwoFactorRememberTokenRepositoryTests.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Core/Auth/Entities/TwoFactorRememberToken.cs b/src/Core/Auth/Entities/TwoFactorRememberToken.cs index c611cbbbe662..4534ac3a90fe 100644 --- a/src/Core/Auth/Entities/TwoFactorRememberToken.cs +++ b/src/Core/Auth/Entities/TwoFactorRememberToken.cs @@ -56,7 +56,7 @@ public void SetNewId() { if (Id == default) { - Id = CoreHelpers.GenerateComb(); + Id = CombGuid.Generate(); } } } diff --git a/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs index 4cc1603c0b8b..6439517dd352 100644 --- a/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs @@ -457,7 +457,7 @@ INSERT INTO [dbo].[TwoFactorRememberToken] VALUES (@Id, @UserId, @DeviceId, @Stamp, @Now, @Now, @Expiration) """; - command.Parameters.Add(new SqlParameter("@Id", CoreHelpers.GenerateComb())); + command.Parameters.Add(new SqlParameter("@Id", CombGuid.Generate())); command.Parameters.Add(new SqlParameter("@UserId", userId)); command.Parameters.Add(new SqlParameter("@DeviceId", deviceId)); command.Parameters.Add(new SqlParameter("@Stamp", "stamp-two")); @@ -471,7 +471,7 @@ INSERT INTO [dbo].[TwoFactorRememberToken] var dbContext = scope.ServiceProvider.GetRequiredService(); dbContext.TwoFactorRememberTokens.Add(new EfTwoFactorRememberToken { - Id = CoreHelpers.GenerateComb(), + Id = CombGuid.Generate(), UserId = userId, DeviceId = deviceId, Stamp = "stamp-two", From 500696c4e10a450070f2b5d409d401b8c704ee58 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Sun, 4 Oct 2026 22:45:15 -0500 Subject: [PATCH 21/27] [PM-40673 Part 1] run dotnet format --- ...DeleteExpiredTwoFactorRememberTokensJob.cs | 2 +- .../Auth/Entities/TwoFactorRememberToken.cs | 2 +- .../ITwoFactorRememberTokenableFactory.cs | 2 +- .../Tokenables/TwoFactorRememberTokenable.cs | 2 +- .../TwoFactorRememberTokenableFactory.cs | 2 +- .../ITwoFactorRememberTokenRepository.cs | 2 +- .../IIssueTwoFactorRememberTokenCommand.cs | 2 +- .../IRevokeTwoFactorRememberTokensCommand.cs | 2 +- .../IValidateTwoFactorRememberTokenQuery.cs | 2 +- .../IssueTwoFactorRememberTokenCommand.cs | 2 +- .../RevokeTwoFactorRememberTokensCommand.cs | 2 +- .../ValidateTwoFactorRememberTokenQuery.cs | 2 +- .../TwoFactorRememberTokenRepository.cs | 2 +- ...torRememberTokenEntityTypeConfiguration.cs | 2 +- .../Auth/Models/TwoFactorRememberToken.cs | 2 +- .../TwoFactorRememberTokenRepository.cs | 2 +- ...eExpiredTwoFactorRememberTokensJobTests.cs | 2 +- .../TwoFactorRememberTokenableFactoryTests.cs | 2 +- .../TwoFactorRememberTokenableTests.cs | 2 +- ...IssueTwoFactorRememberTokenCommandTests.cs | 2 +- ...vokeTwoFactorRememberTokensCommandTests.cs | 2 +- ...alidateTwoFactorRememberTokenQueryTests.cs | 2 +- .../TwoFactorRememberTokenRepositoryTests.cs | 8 +- ...1002161550_CreateTwoFactorRememberToken.cs | 110 +++++++++--------- ...1002161429_CreateTwoFactorRememberToken.cs | 106 +++++++++-------- ...1002161440_CreateTwoFactorRememberToken.cs | 106 +++++++++-------- 26 files changed, 184 insertions(+), 190 deletions(-) diff --git a/src/Admin/Auth/Jobs/DeleteExpiredTwoFactorRememberTokensJob.cs b/src/Admin/Auth/Jobs/DeleteExpiredTwoFactorRememberTokensJob.cs index 246e75a3c2c2..5427368491ed 100644 --- a/src/Admin/Auth/Jobs/DeleteExpiredTwoFactorRememberTokensJob.cs +++ b/src/Admin/Auth/Jobs/DeleteExpiredTwoFactorRememberTokensJob.cs @@ -1,4 +1,4 @@ -using Bit.Core; +using Bit.Core; using Bit.Core.Auth.Repositories; using Bit.Core.Jobs; using Quartz; diff --git a/src/Core/Auth/Entities/TwoFactorRememberToken.cs b/src/Core/Auth/Entities/TwoFactorRememberToken.cs index 4534ac3a90fe..d0274fe6f811 100644 --- a/src/Core/Auth/Entities/TwoFactorRememberToken.cs +++ b/src/Core/Auth/Entities/TwoFactorRememberToken.cs @@ -1,4 +1,4 @@ -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using Bit.Core.Entities; using Bit.Core.Utilities; diff --git a/src/Core/Auth/Models/Business/Tokenables/ITwoFactorRememberTokenableFactory.cs b/src/Core/Auth/Models/Business/Tokenables/ITwoFactorRememberTokenableFactory.cs index b6524da09d3f..236df8899493 100644 --- a/src/Core/Auth/Models/Business/Tokenables/ITwoFactorRememberTokenableFactory.cs +++ b/src/Core/Auth/Models/Business/Tokenables/ITwoFactorRememberTokenableFactory.cs @@ -1,4 +1,4 @@ -using Bit.Core.Entities; +using Bit.Core.Entities; namespace Bit.Core.Auth.Models.Business.Tokenables; diff --git a/src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenable.cs b/src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenable.cs index fba34aad9dc1..43fb86ccf063 100644 --- a/src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenable.cs +++ b/src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenable.cs @@ -1,4 +1,4 @@ -using System.Text.Json.Serialization; +using System.Text.Json.Serialization; using Bit.Core.Tokens; namespace Bit.Core.Auth.Models.Business.Tokenables; diff --git a/src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableFactory.cs b/src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableFactory.cs index c6dd977afda0..dedab6bb5a85 100644 --- a/src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableFactory.cs +++ b/src/Core/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableFactory.cs @@ -1,4 +1,4 @@ -using Bit.Core.Entities; +using Bit.Core.Entities; using Bit.Core.Settings; namespace Bit.Core.Auth.Models.Business.Tokenables; diff --git a/src/Core/Auth/Repositories/ITwoFactorRememberTokenRepository.cs b/src/Core/Auth/Repositories/ITwoFactorRememberTokenRepository.cs index 58efb6e1c479..b29eb5f67d75 100644 --- a/src/Core/Auth/Repositories/ITwoFactorRememberTokenRepository.cs +++ b/src/Core/Auth/Repositories/ITwoFactorRememberTokenRepository.cs @@ -1,4 +1,4 @@ -using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Entities; namespace Bit.Core.Auth.Repositories; diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/IIssueTwoFactorRememberTokenCommand.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/IIssueTwoFactorRememberTokenCommand.cs index a5085de5a48c..afe7961d66a7 100644 --- a/src/Core/Auth/UserFeatures/TwoFactorAuth/IIssueTwoFactorRememberTokenCommand.cs +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/IIssueTwoFactorRememberTokenCommand.cs @@ -1,4 +1,4 @@ -using Bit.Core.Entities; +using Bit.Core.Entities; namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth; diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/IRevokeTwoFactorRememberTokensCommand.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/IRevokeTwoFactorRememberTokensCommand.cs index 70b1fe653ba1..14976b2b18a6 100644 --- a/src/Core/Auth/UserFeatures/TwoFactorAuth/IRevokeTwoFactorRememberTokensCommand.cs +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/IRevokeTwoFactorRememberTokensCommand.cs @@ -1,4 +1,4 @@ -namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth; +namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth; public interface IRevokeTwoFactorRememberTokensCommand { diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/IValidateTwoFactorRememberTokenQuery.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/IValidateTwoFactorRememberTokenQuery.cs index 2a190d9346fd..0e6352033ad1 100644 --- a/src/Core/Auth/UserFeatures/TwoFactorAuth/IValidateTwoFactorRememberTokenQuery.cs +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/IValidateTwoFactorRememberTokenQuery.cs @@ -1,4 +1,4 @@ -using Bit.Core.Entities; +using Bit.Core.Entities; namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth; diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/IssueTwoFactorRememberTokenCommand.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/IssueTwoFactorRememberTokenCommand.cs index 72e8a9598d23..d98e4769e8a1 100644 --- a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/IssueTwoFactorRememberTokenCommand.cs +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/IssueTwoFactorRememberTokenCommand.cs @@ -1,4 +1,4 @@ -using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Entities; diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/RevokeTwoFactorRememberTokensCommand.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/RevokeTwoFactorRememberTokensCommand.cs index cc5bcf3bb7df..3731fc5d413e 100644 --- a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/RevokeTwoFactorRememberTokensCommand.cs +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/RevokeTwoFactorRememberTokensCommand.cs @@ -1,4 +1,4 @@ -using Bit.Core.Auth.Repositories; +using Bit.Core.Auth.Repositories; namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations; diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs index ff37fe43087c..7ce5298d2ecf 100644 --- a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs @@ -1,4 +1,4 @@ -using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Entities; diff --git a/src/Infrastructure.Dapper/Auth/Repositories/TwoFactorRememberTokenRepository.cs b/src/Infrastructure.Dapper/Auth/Repositories/TwoFactorRememberTokenRepository.cs index 6e85b37d5dc3..41d3ed6ef29f 100644 --- a/src/Infrastructure.Dapper/Auth/Repositories/TwoFactorRememberTokenRepository.cs +++ b/src/Infrastructure.Dapper/Auth/Repositories/TwoFactorRememberTokenRepository.cs @@ -1,4 +1,4 @@ -using System.Data; +using System.Data; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Repositories; using Bit.Core.Settings; diff --git a/src/Infrastructure.EntityFramework/Auth/Configurations/TwoFactorRememberTokenEntityTypeConfiguration.cs b/src/Infrastructure.EntityFramework/Auth/Configurations/TwoFactorRememberTokenEntityTypeConfiguration.cs index 7a42eff15df7..85e71eef82aa 100644 --- a/src/Infrastructure.EntityFramework/Auth/Configurations/TwoFactorRememberTokenEntityTypeConfiguration.cs +++ b/src/Infrastructure.EntityFramework/Auth/Configurations/TwoFactorRememberTokenEntityTypeConfiguration.cs @@ -1,4 +1,4 @@ -using Bit.Infrastructure.EntityFramework.Auth.Models; +using Bit.Infrastructure.EntityFramework.Auth.Models; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; diff --git a/src/Infrastructure.EntityFramework/Auth/Models/TwoFactorRememberToken.cs b/src/Infrastructure.EntityFramework/Auth/Models/TwoFactorRememberToken.cs index 635c20dcbe44..26c4e839ea4f 100644 --- a/src/Infrastructure.EntityFramework/Auth/Models/TwoFactorRememberToken.cs +++ b/src/Infrastructure.EntityFramework/Auth/Models/TwoFactorRememberToken.cs @@ -1,4 +1,4 @@ -using AutoMapper; +using AutoMapper; namespace Bit.Infrastructure.EntityFramework.Auth.Models; diff --git a/src/Infrastructure.EntityFramework/Auth/Repositories/TwoFactorRememberTokenRepository.cs b/src/Infrastructure.EntityFramework/Auth/Repositories/TwoFactorRememberTokenRepository.cs index e2dff22bb55e..8e7f1f403b5b 100644 --- a/src/Infrastructure.EntityFramework/Auth/Repositories/TwoFactorRememberTokenRepository.cs +++ b/src/Infrastructure.EntityFramework/Auth/Repositories/TwoFactorRememberTokenRepository.cs @@ -1,4 +1,4 @@ -using AutoMapper; +using AutoMapper; using Bit.Core.Auth.Repositories; using Bit.Infrastructure.EntityFramework.Repositories; using Microsoft.EntityFrameworkCore; diff --git a/test/Admin.Test/Jobs/DeleteExpiredTwoFactorRememberTokensJobTests.cs b/test/Admin.Test/Jobs/DeleteExpiredTwoFactorRememberTokensJobTests.cs index b3f761d0ea6e..be6358430b69 100644 --- a/test/Admin.Test/Jobs/DeleteExpiredTwoFactorRememberTokensJobTests.cs +++ b/test/Admin.Test/Jobs/DeleteExpiredTwoFactorRememberTokensJobTests.cs @@ -1,4 +1,4 @@ -using Bit.Admin.Auth.Jobs; +using Bit.Admin.Auth.Jobs; using Bit.Core.Auth.Repositories; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Time.Testing; diff --git a/test/Core.Test/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableFactoryTests.cs b/test/Core.Test/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableFactoryTests.cs index 62f47f9e7162..5d1b2052e687 100644 --- a/test/Core.Test/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableFactoryTests.cs +++ b/test/Core.Test/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableFactoryTests.cs @@ -1,4 +1,4 @@ -using AutoFixture.Xunit2; +using AutoFixture.Xunit2; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Entities; using Bit.Core.Settings; diff --git a/test/Core.Test/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableTests.cs b/test/Core.Test/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableTests.cs index 9eeb64fbc184..83248dca323f 100644 --- a/test/Core.Test/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableTests.cs +++ b/test/Core.Test/Auth/Models/Business/Tokenables/TwoFactorRememberTokenableTests.cs @@ -1,4 +1,4 @@ -using AutoFixture; +using AutoFixture; using AutoFixture.Xunit2; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Tokens; diff --git a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/IssueTwoFactorRememberTokenCommandTests.cs b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/IssueTwoFactorRememberTokenCommandTests.cs index 0322715ad89d..682789feadb4 100644 --- a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/IssueTwoFactorRememberTokenCommandTests.cs +++ b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/IssueTwoFactorRememberTokenCommandTests.cs @@ -1,4 +1,4 @@ -using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations; diff --git a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/RevokeTwoFactorRememberTokensCommandTests.cs b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/RevokeTwoFactorRememberTokensCommandTests.cs index 0f9222acf3d8..7df8efa7765b 100644 --- a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/RevokeTwoFactorRememberTokensCommandTests.cs +++ b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/RevokeTwoFactorRememberTokensCommandTests.cs @@ -1,4 +1,4 @@ -using Bit.Core.Auth.Repositories; +using Bit.Core.Auth.Repositories; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; diff --git a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs index 7ef496379024..2390bb3d0b6f 100644 --- a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs +++ b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs @@ -1,4 +1,4 @@ -using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations; diff --git a/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs b/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs index 6439517dd352..0ccf0ce482c8 100644 --- a/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs +++ b/test/Infrastructure.IntegrationTest/Auth/Repositories/TwoFactorRememberTokenRepositoryTests.cs @@ -1,4 +1,4 @@ -using AutoMapper; +using AutoMapper; using Bit.Core.Auth.Entities; using Bit.Core.Auth.Repositories; using Bit.Core.Entities; @@ -7,13 +7,13 @@ using Bit.Core.Utilities; using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.IntegrationTest.AdminConsole; -using DapperTwoFactorRememberTokenRepository = Bit.Infrastructure.Dapper.Auth.Repositories.TwoFactorRememberTokenRepository; -using EfTwoFactorRememberToken = Bit.Infrastructure.EntityFramework.Auth.Models.TwoFactorRememberToken; -using EfTwoFactorRememberTokenRepository = Bit.Infrastructure.EntityFramework.Auth.Repositories.TwoFactorRememberTokenRepository; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Xunit; +using DapperTwoFactorRememberTokenRepository = Bit.Infrastructure.Dapper.Auth.Repositories.TwoFactorRememberTokenRepository; +using EfTwoFactorRememberToken = Bit.Infrastructure.EntityFramework.Auth.Models.TwoFactorRememberToken; +using EfTwoFactorRememberTokenRepository = Bit.Infrastructure.EntityFramework.Auth.Repositories.TwoFactorRememberTokenRepository; namespace Bit.Infrastructure.IntegrationTest.Auth.Repositories; diff --git a/util/MySqlMigrations/Migrations/20261002161550_CreateTwoFactorRememberToken.cs b/util/MySqlMigrations/Migrations/20261002161550_CreateTwoFactorRememberToken.cs index fd8e02961a36..003aff75cfa7 100644 --- a/util/MySqlMigrations/Migrations/20261002161550_CreateTwoFactorRememberToken.cs +++ b/util/MySqlMigrations/Migrations/20261002161550_CreateTwoFactorRememberToken.cs @@ -1,68 +1,66 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace Bit.MySqlMigrations.Migrations +namespace Bit.MySqlMigrations.Migrations; + +/// +public partial class CreateTwoFactorRememberToken : Migration { /// - public partial class CreateTwoFactorRememberToken : Migration + protected override void Up(MigrationBuilder migrationBuilder) { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "TwoFactorRememberToken", - columns: table => new - { - Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), - UserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), - DeviceId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), - Stamp = table.Column(type: "varchar(50)", maxLength: 50, nullable: false) - .Annotation("MySql:CharSet", "utf8mb4"), - CreationDate = table.Column(type: "datetime(6)", nullable: false), - RevisionDate = table.Column(type: "datetime(6)", nullable: false), - ExpirationDate = table.Column(type: "datetime(6)", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_TwoFactorRememberToken", x => x.Id); - table.ForeignKey( - name: "FK_TwoFactorRememberToken_Device_DeviceId", - column: x => x.DeviceId, - principalTable: "Device", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_TwoFactorRememberToken_User_UserId", - column: x => x.UserId, - principalTable: "User", - principalColumn: "Id"); - }) - .Annotation("MySql:CharSet", "utf8mb4"); + migrationBuilder.CreateTable( + name: "TwoFactorRememberToken", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + UserId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + DeviceId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Stamp = table.Column(type: "varchar(50)", maxLength: 50, nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + CreationDate = table.Column(type: "datetime(6)", nullable: false), + RevisionDate = table.Column(type: "datetime(6)", nullable: false), + ExpirationDate = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TwoFactorRememberToken", x => x.Id); + table.ForeignKey( + name: "FK_TwoFactorRememberToken_Device_DeviceId", + column: x => x.DeviceId, + principalTable: "Device", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TwoFactorRememberToken_User_UserId", + column: x => x.UserId, + principalTable: "User", + principalColumn: "Id"); + }) + .Annotation("MySql:CharSet", "utf8mb4"); - migrationBuilder.CreateIndex( - name: "IX_TwoFactorRememberToken_DeviceId", - table: "TwoFactorRememberToken", - column: "DeviceId"); + migrationBuilder.CreateIndex( + name: "IX_TwoFactorRememberToken_DeviceId", + table: "TwoFactorRememberToken", + column: "DeviceId"); - migrationBuilder.CreateIndex( - name: "IX_TwoFactorRememberToken_ExpirationDate", - table: "TwoFactorRememberToken", - column: "ExpirationDate"); + migrationBuilder.CreateIndex( + name: "IX_TwoFactorRememberToken_ExpirationDate", + table: "TwoFactorRememberToken", + column: "ExpirationDate"); - migrationBuilder.CreateIndex( - name: "IX_TwoFactorRememberToken_UserId_DeviceId", - table: "TwoFactorRememberToken", - columns: new[] { "UserId", "DeviceId" }, - unique: true); - } + migrationBuilder.CreateIndex( + name: "IX_TwoFactorRememberToken_UserId_DeviceId", + table: "TwoFactorRememberToken", + columns: new[] { "UserId", "DeviceId" }, + unique: true); + } - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "TwoFactorRememberToken"); - } + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TwoFactorRememberToken"); } } diff --git a/util/PostgresMigrations/Migrations/20261002161429_CreateTwoFactorRememberToken.cs b/util/PostgresMigrations/Migrations/20261002161429_CreateTwoFactorRememberToken.cs index 2c6a95e3712b..c34dfbf60a0a 100644 --- a/util/PostgresMigrations/Migrations/20261002161429_CreateTwoFactorRememberToken.cs +++ b/util/PostgresMigrations/Migrations/20261002161429_CreateTwoFactorRememberToken.cs @@ -1,66 +1,64 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace Bit.PostgresMigrations.Migrations +namespace Bit.PostgresMigrations.Migrations; + +/// +public partial class CreateTwoFactorRememberToken : Migration { /// - public partial class CreateTwoFactorRememberToken : Migration + protected override void Up(MigrationBuilder migrationBuilder) { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "TwoFactorRememberToken", - columns: table => new - { - Id = table.Column(type: "uuid", nullable: false), - UserId = table.Column(type: "uuid", nullable: false), - DeviceId = table.Column(type: "uuid", nullable: false), - Stamp = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), - CreationDate = table.Column(type: "timestamp with time zone", nullable: false), - RevisionDate = table.Column(type: "timestamp with time zone", nullable: false), - ExpirationDate = table.Column(type: "timestamp with time zone", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_TwoFactorRememberToken", x => x.Id); - table.ForeignKey( - name: "FK_TwoFactorRememberToken_Device_DeviceId", - column: x => x.DeviceId, - principalTable: "Device", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_TwoFactorRememberToken_User_UserId", - column: x => x.UserId, - principalTable: "User", - principalColumn: "Id"); - }); + migrationBuilder.CreateTable( + name: "TwoFactorRememberToken", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + UserId = table.Column(type: "uuid", nullable: false), + DeviceId = table.Column(type: "uuid", nullable: false), + Stamp = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + CreationDate = table.Column(type: "timestamp with time zone", nullable: false), + RevisionDate = table.Column(type: "timestamp with time zone", nullable: false), + ExpirationDate = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TwoFactorRememberToken", x => x.Id); + table.ForeignKey( + name: "FK_TwoFactorRememberToken_Device_DeviceId", + column: x => x.DeviceId, + principalTable: "Device", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TwoFactorRememberToken_User_UserId", + column: x => x.UserId, + principalTable: "User", + principalColumn: "Id"); + }); - migrationBuilder.CreateIndex( - name: "IX_TwoFactorRememberToken_DeviceId", - table: "TwoFactorRememberToken", - column: "DeviceId"); + migrationBuilder.CreateIndex( + name: "IX_TwoFactorRememberToken_DeviceId", + table: "TwoFactorRememberToken", + column: "DeviceId"); - migrationBuilder.CreateIndex( - name: "IX_TwoFactorRememberToken_ExpirationDate", - table: "TwoFactorRememberToken", - column: "ExpirationDate"); + migrationBuilder.CreateIndex( + name: "IX_TwoFactorRememberToken_ExpirationDate", + table: "TwoFactorRememberToken", + column: "ExpirationDate"); - migrationBuilder.CreateIndex( - name: "IX_TwoFactorRememberToken_UserId_DeviceId", - table: "TwoFactorRememberToken", - columns: new[] { "UserId", "DeviceId" }, - unique: true); - } + migrationBuilder.CreateIndex( + name: "IX_TwoFactorRememberToken_UserId_DeviceId", + table: "TwoFactorRememberToken", + columns: new[] { "UserId", "DeviceId" }, + unique: true); + } - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "TwoFactorRememberToken"); - } + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TwoFactorRememberToken"); } } diff --git a/util/SqliteMigrations/Migrations/20261002161440_CreateTwoFactorRememberToken.cs b/util/SqliteMigrations/Migrations/20261002161440_CreateTwoFactorRememberToken.cs index 6dff2032cadf..d63242092ec8 100644 --- a/util/SqliteMigrations/Migrations/20261002161440_CreateTwoFactorRememberToken.cs +++ b/util/SqliteMigrations/Migrations/20261002161440_CreateTwoFactorRememberToken.cs @@ -1,66 +1,64 @@ -using System; -using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Migrations; #nullable disable -namespace Bit.SqliteMigrations.Migrations +namespace Bit.SqliteMigrations.Migrations; + +/// +public partial class CreateTwoFactorRememberToken : Migration { /// - public partial class CreateTwoFactorRememberToken : Migration + protected override void Up(MigrationBuilder migrationBuilder) { - /// - protected override void Up(MigrationBuilder migrationBuilder) - { - migrationBuilder.CreateTable( - name: "TwoFactorRememberToken", - columns: table => new - { - Id = table.Column(type: "TEXT", nullable: false), - UserId = table.Column(type: "TEXT", nullable: false), - DeviceId = table.Column(type: "TEXT", nullable: false), - Stamp = table.Column(type: "TEXT", maxLength: 50, nullable: false), - CreationDate = table.Column(type: "TEXT", nullable: false), - RevisionDate = table.Column(type: "TEXT", nullable: false), - ExpirationDate = table.Column(type: "TEXT", nullable: false) - }, - constraints: table => - { - table.PrimaryKey("PK_TwoFactorRememberToken", x => x.Id); - table.ForeignKey( - name: "FK_TwoFactorRememberToken_Device_DeviceId", - column: x => x.DeviceId, - principalTable: "Device", - principalColumn: "Id", - onDelete: ReferentialAction.Cascade); - table.ForeignKey( - name: "FK_TwoFactorRememberToken_User_UserId", - column: x => x.UserId, - principalTable: "User", - principalColumn: "Id"); - }); + migrationBuilder.CreateTable( + name: "TwoFactorRememberToken", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + UserId = table.Column(type: "TEXT", nullable: false), + DeviceId = table.Column(type: "TEXT", nullable: false), + Stamp = table.Column(type: "TEXT", maxLength: 50, nullable: false), + CreationDate = table.Column(type: "TEXT", nullable: false), + RevisionDate = table.Column(type: "TEXT", nullable: false), + ExpirationDate = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TwoFactorRememberToken", x => x.Id); + table.ForeignKey( + name: "FK_TwoFactorRememberToken_Device_DeviceId", + column: x => x.DeviceId, + principalTable: "Device", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_TwoFactorRememberToken_User_UserId", + column: x => x.UserId, + principalTable: "User", + principalColumn: "Id"); + }); - migrationBuilder.CreateIndex( - name: "IX_TwoFactorRememberToken_DeviceId", - table: "TwoFactorRememberToken", - column: "DeviceId"); + migrationBuilder.CreateIndex( + name: "IX_TwoFactorRememberToken_DeviceId", + table: "TwoFactorRememberToken", + column: "DeviceId"); - migrationBuilder.CreateIndex( - name: "IX_TwoFactorRememberToken_ExpirationDate", - table: "TwoFactorRememberToken", - column: "ExpirationDate"); + migrationBuilder.CreateIndex( + name: "IX_TwoFactorRememberToken_ExpirationDate", + table: "TwoFactorRememberToken", + column: "ExpirationDate"); - migrationBuilder.CreateIndex( - name: "IX_TwoFactorRememberToken_UserId_DeviceId", - table: "TwoFactorRememberToken", - columns: new[] { "UserId", "DeviceId" }, - unique: true); - } + migrationBuilder.CreateIndex( + name: "IX_TwoFactorRememberToken_UserId_DeviceId", + table: "TwoFactorRememberToken", + columns: new[] { "UserId", "DeviceId" }, + unique: true); + } - /// - protected override void Down(MigrationBuilder migrationBuilder) - { - migrationBuilder.DropTable( - name: "TwoFactorRememberToken"); - } + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "TwoFactorRememberToken"); } } From f9de219e41cfd17fa23fb2636aab4de8ffac9263 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Fri, 9 Oct 2026 11:49:24 -0500 Subject: [PATCH 22/27] [PM-40673 Part 1] restore snapshots and re-generate migrations --- ....sql => 2026-10-09_00_CreateTwoFactorRememberToken.sql} | 0 ...0261009164324_CreateTwoFactorRememberToken.Designer.cs} | 7 +++++-- ...n.cs => 20261009164324_CreateTwoFactorRememberToken.cs} | 0 ...0261009164244_CreateTwoFactorRememberToken.Designer.cs} | 7 +++++-- ...n.cs => 20261009164244_CreateTwoFactorRememberToken.cs} | 0 ...0261009164306_CreateTwoFactorRememberToken.Designer.cs} | 7 +++++-- ...n.cs => 20261009164306_CreateTwoFactorRememberToken.cs} | 0 7 files changed, 15 insertions(+), 6 deletions(-) rename util/Migrator/DbScripts/{2026-10-02_00_CreateTwoFactorRememberToken.sql => 2026-10-09_00_CreateTwoFactorRememberToken.sql} (100%) rename util/MySqlMigrations/Migrations/{20261002161550_CreateTwoFactorRememberToken.Designer.cs => 20261009164324_CreateTwoFactorRememberToken.Designer.cs} (99%) rename util/MySqlMigrations/Migrations/{20261002161550_CreateTwoFactorRememberToken.cs => 20261009164324_CreateTwoFactorRememberToken.cs} (100%) rename util/PostgresMigrations/Migrations/{20261002161429_CreateTwoFactorRememberToken.Designer.cs => 20261009164244_CreateTwoFactorRememberToken.Designer.cs} (99%) rename util/PostgresMigrations/Migrations/{20261002161429_CreateTwoFactorRememberToken.cs => 20261009164244_CreateTwoFactorRememberToken.cs} (100%) rename util/SqliteMigrations/Migrations/{20261002161440_CreateTwoFactorRememberToken.Designer.cs => 20261009164306_CreateTwoFactorRememberToken.Designer.cs} (99%) rename util/SqliteMigrations/Migrations/{20261002161440_CreateTwoFactorRememberToken.cs => 20261009164306_CreateTwoFactorRememberToken.cs} (100%) diff --git a/util/Migrator/DbScripts/2026-10-02_00_CreateTwoFactorRememberToken.sql b/util/Migrator/DbScripts/2026-10-09_00_CreateTwoFactorRememberToken.sql similarity index 100% rename from util/Migrator/DbScripts/2026-10-02_00_CreateTwoFactorRememberToken.sql rename to util/Migrator/DbScripts/2026-10-09_00_CreateTwoFactorRememberToken.sql diff --git a/util/MySqlMigrations/Migrations/20261002161550_CreateTwoFactorRememberToken.Designer.cs b/util/MySqlMigrations/Migrations/20261009164324_CreateTwoFactorRememberToken.Designer.cs similarity index 99% rename from util/MySqlMigrations/Migrations/20261002161550_CreateTwoFactorRememberToken.Designer.cs rename to util/MySqlMigrations/Migrations/20261009164324_CreateTwoFactorRememberToken.Designer.cs index ba33c89ca2ce..b803eec955f3 100644 --- a/util/MySqlMigrations/Migrations/20261002161550_CreateTwoFactorRememberToken.Designer.cs +++ b/util/MySqlMigrations/Migrations/20261009164324_CreateTwoFactorRememberToken.Designer.cs @@ -12,7 +12,7 @@ namespace Bit.MySqlMigrations.Migrations { [DbContext(typeof(DatabaseContext))] - [Migration("20261002161550_CreateTwoFactorRememberToken")] + [Migration("20261009164324_CreateTwoFactorRememberToken")] partial class CreateTwoFactorRememberToken { /// @@ -20,7 +20,7 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .HasAnnotation("ProductVersion", "8.0.8") + .HasAnnotation("ProductVersion", "8.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 64); MySqlModelBuilderExtensions.AutoIncrementColumns(modelBuilder); @@ -264,6 +264,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("OwnersNotifiedOfAutoscaling") .HasColumnType("datetime(6)"); + b.Property("PamSeatMinimum") + .HasColumnType("int"); + b.Property("PamSeats") .HasColumnType("int"); diff --git a/util/MySqlMigrations/Migrations/20261002161550_CreateTwoFactorRememberToken.cs b/util/MySqlMigrations/Migrations/20261009164324_CreateTwoFactorRememberToken.cs similarity index 100% rename from util/MySqlMigrations/Migrations/20261002161550_CreateTwoFactorRememberToken.cs rename to util/MySqlMigrations/Migrations/20261009164324_CreateTwoFactorRememberToken.cs diff --git a/util/PostgresMigrations/Migrations/20261002161429_CreateTwoFactorRememberToken.Designer.cs b/util/PostgresMigrations/Migrations/20261009164244_CreateTwoFactorRememberToken.Designer.cs similarity index 99% rename from util/PostgresMigrations/Migrations/20261002161429_CreateTwoFactorRememberToken.Designer.cs rename to util/PostgresMigrations/Migrations/20261009164244_CreateTwoFactorRememberToken.Designer.cs index c4b2ebb0c4e1..70c7eca0b713 100644 --- a/util/PostgresMigrations/Migrations/20261002161429_CreateTwoFactorRememberToken.Designer.cs +++ b/util/PostgresMigrations/Migrations/20261009164244_CreateTwoFactorRememberToken.Designer.cs @@ -12,7 +12,7 @@ namespace Bit.PostgresMigrations.Migrations { [DbContext(typeof(DatabaseContext))] - [Migration("20261002161429_CreateTwoFactorRememberToken")] + [Migration("20261009164244_CreateTwoFactorRememberToken")] partial class CreateTwoFactorRememberToken { /// @@ -21,7 +21,7 @@ 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("ProductVersion", "8.0.11") .HasAnnotation("Relational:MaxIdentifierLength", 63); NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); @@ -266,6 +266,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("OwnersNotifiedOfAutoscaling") .HasColumnType("timestamp with time zone"); + b.Property("PamSeatMinimum") + .HasColumnType("integer"); + b.Property("PamSeats") .HasColumnType("integer"); diff --git a/util/PostgresMigrations/Migrations/20261002161429_CreateTwoFactorRememberToken.cs b/util/PostgresMigrations/Migrations/20261009164244_CreateTwoFactorRememberToken.cs similarity index 100% rename from util/PostgresMigrations/Migrations/20261002161429_CreateTwoFactorRememberToken.cs rename to util/PostgresMigrations/Migrations/20261009164244_CreateTwoFactorRememberToken.cs diff --git a/util/SqliteMigrations/Migrations/20261002161440_CreateTwoFactorRememberToken.Designer.cs b/util/SqliteMigrations/Migrations/20261009164306_CreateTwoFactorRememberToken.Designer.cs similarity index 99% rename from util/SqliteMigrations/Migrations/20261002161440_CreateTwoFactorRememberToken.Designer.cs rename to util/SqliteMigrations/Migrations/20261009164306_CreateTwoFactorRememberToken.Designer.cs index 782214452b01..b2018f44d3bb 100644 --- a/util/SqliteMigrations/Migrations/20261002161440_CreateTwoFactorRememberToken.Designer.cs +++ b/util/SqliteMigrations/Migrations/20261009164306_CreateTwoFactorRememberToken.Designer.cs @@ -11,14 +11,14 @@ namespace Bit.SqliteMigrations.Migrations { [DbContext(typeof(DatabaseContext))] - [Migration("20261002161440_CreateTwoFactorRememberToken")] + [Migration("20261009164306_CreateTwoFactorRememberToken")] partial class CreateTwoFactorRememberToken { /// protected override void BuildTargetModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 - modelBuilder.HasAnnotation("ProductVersion", "8.0.8"); + modelBuilder.HasAnnotation("ProductVersion", "8.0.11"); modelBuilder.Entity("Bit.Core.Dirt.Reports.Models.Data.OrganizationMemberBaseDetail", b => { @@ -259,6 +259,9 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Property("OwnersNotifiedOfAutoscaling") .HasColumnType("TEXT"); + b.Property("PamSeatMinimum") + .HasColumnType("INTEGER"); + b.Property("PamSeats") .HasColumnType("INTEGER"); diff --git a/util/SqliteMigrations/Migrations/20261002161440_CreateTwoFactorRememberToken.cs b/util/SqliteMigrations/Migrations/20261009164306_CreateTwoFactorRememberToken.cs similarity index 100% rename from util/SqliteMigrations/Migrations/20261002161440_CreateTwoFactorRememberToken.cs rename to util/SqliteMigrations/Migrations/20261009164306_CreateTwoFactorRememberToken.cs From b1cd61dc1cdc440bd23784ad308d082eb16a55a4 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Fri, 9 Oct 2026 15:19:50 -0500 Subject: [PATCH 23/27] [PM-40673 Part 1] add check for org-required-2FA when checking if 2FA is enabled for user --- .../IValidateTwoFactorRememberTokenQuery.cs | 9 ++- .../ValidateTwoFactorRememberTokenQuery.cs | 12 +-- ...alidateTwoFactorRememberTokenQueryTests.cs | 73 +++++++++++++++---- 3 files changed, 71 insertions(+), 23 deletions(-) diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/IValidateTwoFactorRememberTokenQuery.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/IValidateTwoFactorRememberTokenQuery.cs index 0e6352033ad1..38e3f869d846 100644 --- a/src/Core/Auth/UserFeatures/TwoFactorAuth/IValidateTwoFactorRememberTokenQuery.cs +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/IValidateTwoFactorRememberTokenQuery.cs @@ -1,4 +1,5 @@ -using Bit.Core.Entities; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Entities; namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth; @@ -13,10 +14,14 @@ public interface IValidateTwoFactorRememberTokenQuery /// own command rather than folded in here. /// /// The authenticating user. + /// + /// The organization enforcing two-factor on this user, if any. Its two-factor counts as a second + /// factor, so a user with none of their own is still covered. + /// /// /// The client-generated device identifier from the current request. The device row itself is not /// available at this point in the login pipeline, which is why the identifier is passed in. /// /// The protected token string as presented by the client. - Task ValidateAsync(User user, string deviceIdentifier, string token); + Task ValidateAsync(User user, Organization? organization, string deviceIdentifier, string token); } diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs index 7ce5298d2ecf..940980bf9495 100644 --- a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs @@ -1,4 +1,5 @@ -using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Entities; @@ -12,7 +13,7 @@ public class ValidateTwoFactorRememberTokenQuery( ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, TimeProvider timeProvider) : IValidateTwoFactorRememberTokenQuery { - public async Task ValidateAsync(User user, string deviceIdentifier, string token) + public async Task ValidateAsync(User user, Organization? organization, string deviceIdentifier, string token) { if (TwoFactorRememberTokenable.ValidateTwoFactorRememberToken(tokenFactory, token, out var tokenable) != null || tokenable is null) @@ -40,9 +41,10 @@ public async Task ValidateAsync(User user, string deviceIdentifier, string } // A remember token stands in for a second factor, so it cannot be honored by an account that - // has no second factor configured. Ordered ahead of the row read because a user with no - // enabled providers resolves without a database call. - if (!await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user)) + // has none, whether configured by the user or enforced by their organization. Ordered ahead + // of the row read because a user with no enabled providers resolves without a database call. + if (organization?.TwoFactorIsEnabled() != true && + !await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user)) { return false; } diff --git a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs index 2390bb3d0b6f..04d88b0bef57 100644 --- a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs +++ b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs @@ -1,4 +1,5 @@ -using Bit.Core.Auth.Entities; +using Bit.Core.AdminConsole.Entities; +using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations; @@ -22,6 +23,8 @@ public class ValidateTwoFactorRememberTokenQueryTests { private const string _deviceIdentifier = "device-identifier"; private const string _token = "protected-token"; + private const string _organizationDuoProviders = + """{"6":{"Enabled":true,"MetaData":{"ClientSecret":"s","ClientId":"c","Host":"example.com"}}}"""; private static readonly DateTime _now = new(2026, 10, 2, 12, 0, 0, DateTimeKind.Utc); @@ -90,7 +93,7 @@ public async Task ValidateAsync_EverythingMatches_ReturnsTrue( var sutProvider = GetSutProvider(); ArrangeValid(sutProvider, user, deviceId); - Assert.True(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); + Assert.True(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token)); } /// The targeted revocation: the device's row stamp has been rotated. @@ -102,7 +105,7 @@ public async Task ValidateAsync_RowStampRotated_ReturnsFalse( var (_, row) = ArrangeValid(sutProvider, user, deviceId); row.Stamp = "rotated-stamp"; - Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); + Assert.False(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token)); } /// The account-wide check inherited from the previous design. @@ -114,7 +117,7 @@ public async Task ValidateAsync_UserSecurityStampRotated_ReturnsFalse( var (tokenable, _) = ArrangeValid(sutProvider, user, deviceId); tokenable.SecurityStamp = "a-different-security-stamp"; - Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); + Assert.False(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token)); } /// @@ -131,7 +134,45 @@ public async Task ValidateAsync_UserHasNoTwoFactorEnabled_ReturnsFalse( .TwoFactorIsEnabledAsync(user) .Returns(false); - Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); + Assert.False(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token)); + } + + /// + /// A user whose only second factor is enforced by their organization is still covered, so their + /// remember token is honored. + /// + [Theory, BitAutoData] + public async Task ValidateAsync_OrgEnforcesTwoFactor_NoPersonalTwoFactor_ReturnsTrue( + User user, Organization organization, Guid deviceId) + { + var sutProvider = GetSutProvider(); + ArrangeValid(sutProvider, user, deviceId); + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(user) + .Returns(false); + organization.Use2fa = true; + organization.TwoFactorProviders = _organizationDuoProviders; + + Assert.True(await sutProvider.Sut.ValidateAsync(user, organization, _deviceIdentifier, _token)); + } + + /// + /// An organization that does not enforce two-factor gives a user with none of their own no second + /// factor to stand in for. + /// + [Theory, BitAutoData] + public async Task ValidateAsync_OrgDoesNotEnforceTwoFactor_NoPersonalTwoFactor_ReturnsFalse( + User user, Organization organization, Guid deviceId) + { + var sutProvider = GetSutProvider(); + ArrangeValid(sutProvider, user, deviceId); + sutProvider.GetDependency() + .TwoFactorIsEnabledAsync(user) + .Returns(false); + organization.Use2fa = false; + organization.TwoFactorProviders = _organizationDuoProviders; + + Assert.False(await sutProvider.Sut.ValidateAsync(user, organization, _deviceIdentifier, _token)); } /// @@ -148,7 +189,7 @@ public async Task ValidateAsync_UserHasNoTwoFactorEnabled_DoesNotReadRow( .TwoFactorIsEnabledAsync(user) .Returns(false); - await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token); + await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token); await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() @@ -164,7 +205,7 @@ public async Task ValidateAsync_UserIdMismatch_ReturnsFalse( var (tokenable, _) = ArrangeValid(sutProvider, user, deviceId); tokenable.UserId = Guid.NewGuid(); - Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); + Assert.False(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token)); } /// The device binding. @@ -175,7 +216,7 @@ public async Task ValidateAsync_DeviceIdentifierMismatch_ReturnsFalse( var sutProvider = GetSutProvider(); ArrangeValid(sutProvider, user, deviceId); - Assert.False(await sutProvider.Sut.ValidateAsync(user, "a-different-device", _token)); + Assert.False(await sutProvider.Sut.ValidateAsync(user, null, "a-different-device", _token)); } /// @@ -189,7 +230,7 @@ public async Task ValidateAsync_DeviceIdentifierDiffersOnlyByCase_ReturnsTrue( var sutProvider = GetSutProvider(); ArrangeValid(sutProvider, user, deviceId); - Assert.True(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier.ToUpperInvariant(), _token)); + Assert.True(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier.ToUpperInvariant(), _token)); } /// No row for this device. @@ -203,7 +244,7 @@ public async Task ValidateAsync_RowMissing_ReturnsFalse( .GetByUserIdDeviceIdAsync(user.Id, deviceId) .Returns((TwoFactorRememberToken?)null); - Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); + Assert.False(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token)); } /// The row outlived its expiry but the sweep has not reached it. @@ -215,7 +256,7 @@ public async Task ValidateAsync_RowExpired_ReturnsFalse( var (_, row) = ArrangeValid(sutProvider, user, deviceId); row.ExpirationDate = _now.AddMinutes(-1); - Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); + Assert.False(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token)); } /// @@ -229,7 +270,7 @@ public async Task ValidateAsync_RowExpiresExactlyNow_ReturnsTrue(User user, Guid var (_, row) = ArrangeValid(sutProvider, user, deviceId); row.ExpirationDate = _now; - Assert.True(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); + Assert.True(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token)); } [Theory, BitAutoData] @@ -239,7 +280,7 @@ public async Task ValidateAsync_RowExpiredOneTickAgo_ReturnsFalse(User user, Gui var (_, row) = ArrangeValid(sutProvider, user, deviceId); row.ExpirationDate = _now.AddTicks(-1); - Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); + Assert.False(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token)); } [Theory, BitAutoData] @@ -251,7 +292,7 @@ public async Task ValidateAsync_TokenCannotBeUnprotected_ReturnsFalse( .TryUnprotect(_token, out Arg.Any()) .Returns(c => { c[1] = null!; return false; }); - Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); + Assert.False(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token)); } [Theory, BitAutoData] @@ -264,7 +305,7 @@ public async Task ValidateAsync_TokenItselfExpired_ReturnsFalse( // injected TimeProvider cannot reach it. tokenable.ExpirationDate = DateTime.UtcNow.AddMinutes(-1); - Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); + Assert.False(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token)); } /// Validation reads; it must never write. @@ -275,7 +316,7 @@ public async Task ValidateAsync_Always_DoesNotWrite( var sutProvider = GetSutProvider(); ArrangeValid(sutProvider, user, deviceId); - await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token); + await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token); var repository = sutProvider.GetDependency(); await repository.DidNotReceiveWithAnyArgs().UpsertAsync(default!); From e4c1ea9a21bca772ae6ea8588d97754b7103fbf8 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Mon, 12 Oct 2026 08:02:02 -0500 Subject: [PATCH 24/27] [PM-40673 Part 1] remove comment, update comment --- .../Implementations/ValidateTwoFactorRememberTokenQuery.cs | 2 +- .../TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs index 940980bf9495..56971118b42b 100644 --- a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs @@ -41,7 +41,7 @@ public async Task ValidateAsync(User user, Organization? organization, str } // A remember token stands in for a second factor, so it cannot be honored by an account that - // has none, whether configured by the user or enforced by their organization. Ordered ahead + // has no 2FA configured, whether by the user or enforced by their organization. Ordered ahead // of the row read because a user with no enabled providers resolves without a database call. if (organization?.TwoFactorIsEnabled() != true && !await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user)) diff --git a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs index 04d88b0bef57..e8331122aed3 100644 --- a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs +++ b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs @@ -156,10 +156,6 @@ public async Task ValidateAsync_OrgEnforcesTwoFactor_NoPersonalTwoFactor_Returns Assert.True(await sutProvider.Sut.ValidateAsync(user, organization, _deviceIdentifier, _token)); } - /// - /// An organization that does not enforce two-factor gives a user with none of their own no second - /// factor to stand in for. - /// [Theory, BitAutoData] public async Task ValidateAsync_OrgDoesNotEnforceTwoFactor_NoPersonalTwoFactor_ReturnsFalse( User user, Organization organization, Guid deviceId) From 1983d85679251cc3d5f414522756184a9fd7b7a0 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:57:07 -0500 Subject: [PATCH 25/27] [PM-40673 Part 1] move scheduled DeleteExpiredTwoFactorRememberTokensJob to run at a new time to avoid multiple jobs running at same time --- src/Admin/Jobs/JobsHostedService.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Admin/Jobs/JobsHostedService.cs b/src/Admin/Jobs/JobsHostedService.cs index 62cfbfab7eb2..984f656b7fd1 100644 --- a/src/Admin/Jobs/JobsHostedService.cs +++ b/src/Admin/Jobs/JobsHostedService.cs @@ -44,10 +44,10 @@ public override async Task StartAsync(CancellationToken cancellationToken) .StartNow() .WithCronSchedule("0 0 22 ? * FRI", x => x.InTimeZone(timeZone)) .Build(); - var everyFridayAt10pmTwoFactorRememberTokensTrigger = TriggerBuilder.Create() - .WithIdentity("EveryFridayAt10pmTwoFactorRememberTokensTrigger") + var everySaturdayAt430amTrigger = TriggerBuilder.Create() + .WithIdentity("EverySaturdayAt430amTrigger") .StartNow() - .WithCronSchedule("0 0 22 ? * FRI", x => x.InTimeZone(timeZone)) + .WithCronSchedule("0 30 4 ? * SAT", x => x.InTimeZone(timeZone)) .Build(); var everySaturdayAtMidnightTrigger = TriggerBuilder.Create() .WithIdentity("EverySaturdayAtMidnightTrigger") @@ -93,7 +93,7 @@ public override async Task StartAsync(CancellationToken cancellationToken) { new Tuple(typeof(DeleteSendsJob), everyFiveMinutesTrigger), new Tuple(typeof(DatabaseExpiredGrantsJob), everyFridayAt10pmTrigger), - new Tuple(typeof(DeleteExpiredTwoFactorRememberTokensJob), everyFridayAt10pmTwoFactorRememberTokensTrigger), + new Tuple(typeof(DeleteExpiredTwoFactorRememberTokensJob), everySaturdayAt430amTrigger), new Tuple(typeof(DeleteCiphersJob), everyDayAtMidnightUtc), new Tuple(typeof(DatabaseExpiredSponsorshipsJob), everyMondayAtMidnightTrigger), new Tuple(typeof(DeleteAuthRequestsJob), everyFifteenMinutesTrigger), From a27f3cd2faf875fcfe0ed4f97bc8010bc2ccbc37 Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Sat, 17 Oct 2026 06:01:54 -0500 Subject: [PATCH 26/27] [PM-40673 Part 1] remove redundant 2FA-enabled check and organization parameter from ValidateTwoFactorRememberTokenQuery --- .../IValidateTwoFactorRememberTokenQuery.cs | 13 +-- .../ValidateTwoFactorRememberTokenQuery.cs | 16 +-- ...alidateTwoFactorRememberTokenQueryTests.cs | 110 +++--------------- 3 files changed, 23 insertions(+), 116 deletions(-) diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/IValidateTwoFactorRememberTokenQuery.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/IValidateTwoFactorRememberTokenQuery.cs index 38e3f869d846..8ce9bbc64301 100644 --- a/src/Core/Auth/UserFeatures/TwoFactorAuth/IValidateTwoFactorRememberTokenQuery.cs +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/IValidateTwoFactorRememberTokenQuery.cs @@ -1,5 +1,4 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Entities; +using Bit.Core.Entities; namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth; @@ -12,16 +11,16 @@ public interface IValidateTwoFactorRememberTokenQuery /// /// A pure read: nothing here writes. If a last-used timestamp is ever wanted, it belongs in its /// own command rather than folded in here. + /// + /// This validates the token only. Whether the user is required to complete two-factor at all is + /// the caller's decision, made before a remember token is considered. + /// /// /// The authenticating user. - /// - /// The organization enforcing two-factor on this user, if any. Its two-factor counts as a second - /// factor, so a user with none of their own is still covered. - /// /// /// The client-generated device identifier from the current request. The device row itself is not /// available at this point in the login pipeline, which is why the identifier is passed in. /// /// The protected token string as presented by the client. - Task ValidateAsync(User user, Organization? organization, string deviceIdentifier, string token); + Task ValidateAsync(User user, string deviceIdentifier, string token); } diff --git a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs index 56971118b42b..9f7061c1885b 100644 --- a/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs +++ b/src/Core/Auth/UserFeatures/TwoFactorAuth/Implementations/ValidateTwoFactorRememberTokenQuery.cs @@ -1,7 +1,5 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Auth.Models.Business.Tokenables; +using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; -using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Entities; using Bit.Core.Tokens; @@ -10,10 +8,9 @@ namespace Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations; public class ValidateTwoFactorRememberTokenQuery( ITwoFactorRememberTokenRepository twoFactorRememberTokenRepository, IDataProtectorTokenFactory tokenFactory, - ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, TimeProvider timeProvider) : IValidateTwoFactorRememberTokenQuery { - public async Task ValidateAsync(User user, Organization? organization, string deviceIdentifier, string token) + public async Task ValidateAsync(User user, string deviceIdentifier, string token) { if (TwoFactorRememberTokenable.ValidateTwoFactorRememberToken(tokenFactory, token, out var tokenable) != null || tokenable is null) @@ -40,15 +37,6 @@ public async Task ValidateAsync(User user, Organization? organization, str return false; } - // A remember token stands in for a second factor, so it cannot be honored by an account that - // has no 2FA configured, whether by the user or enforced by their organization. Ordered ahead - // of the row read because a user with no enabled providers resolves without a database call. - if (organization?.TwoFactorIsEnabled() != true && - !await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user)) - { - return false; - } - // Safe to locate the row by the token's own DeviceId only because the identifier comparison // above already proved the presenter is on the device the token names. Do not reorder. var row = await twoFactorRememberTokenRepository.GetByUserIdDeviceIdAsync(user.Id, tokenable.DeviceId); diff --git a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs index e8331122aed3..17ac6c5cdd34 100644 --- a/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs +++ b/test/Core.Test/Auth/UserFeatures/TwoFactorAuth/ValidateTwoFactorRememberTokenQueryTests.cs @@ -1,9 +1,7 @@ -using Bit.Core.AdminConsole.Entities; -using Bit.Core.Auth.Entities; +using Bit.Core.Auth.Entities; using Bit.Core.Auth.Models.Business.Tokenables; using Bit.Core.Auth.Repositories; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Implementations; -using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; using Bit.Core.Entities; using Bit.Core.Tokens; using Bit.Test.Common.AutoFixture; @@ -23,8 +21,6 @@ public class ValidateTwoFactorRememberTokenQueryTests { private const string _deviceIdentifier = "device-identifier"; private const string _token = "protected-token"; - private const string _organizationDuoProviders = - """{"6":{"Enabled":true,"MetaData":{"ClientSecret":"s","ClientId":"c","Host":"example.com"}}}"""; private static readonly DateTime _now = new(2026, 10, 2, 12, 0, 0, DateTimeKind.Utc); @@ -60,7 +56,7 @@ private static TwoFactorRememberToken Row(User user, Guid deviceId, string stamp }; /// - /// Wires the happy path: token unprotects, 2FA is on, and the row exists with a matching stamp. + /// Wires the happy path: token unprotects and the row exists with a matching stamp. /// Individual tests then break exactly one of those. /// private static (TwoFactorRememberTokenable tokenable, TwoFactorRememberToken row) ArrangeValid( @@ -74,10 +70,6 @@ private static (TwoFactorRememberTokenable tokenable, TwoFactorRememberToken row .TryUnprotect(_token, out Arg.Any()) .Returns(c => { c[1] = tokenable; return true; }); - sutProvider.GetDependency() - .TwoFactorIsEnabledAsync(user) - .Returns(true); - sutProvider.GetDependency() .GetByUserIdDeviceIdAsync(user.Id, deviceId) .Returns(row); @@ -93,7 +85,7 @@ public async Task ValidateAsync_EverythingMatches_ReturnsTrue( var sutProvider = GetSutProvider(); ArrangeValid(sutProvider, user, deviceId); - Assert.True(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token)); + Assert.True(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); } /// The targeted revocation: the device's row stamp has been rotated. @@ -105,7 +97,7 @@ public async Task ValidateAsync_RowStampRotated_ReturnsFalse( var (_, row) = ArrangeValid(sutProvider, user, deviceId); row.Stamp = "rotated-stamp"; - Assert.False(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token)); + Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); } /// The account-wide check inherited from the previous design. @@ -117,79 +109,7 @@ public async Task ValidateAsync_UserSecurityStampRotated_ReturnsFalse( var (tokenable, _) = ArrangeValid(sutProvider, user, deviceId); tokenable.SecurityStamp = "a-different-security-stamp"; - Assert.False(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token)); - } - - /// - /// A remember token cannot stand in as a second factor for an account that currently has no - /// second factor configured. - /// - [Theory, BitAutoData] - public async Task ValidateAsync_UserHasNoTwoFactorEnabled_ReturnsFalse( - User user, Guid deviceId) - { - var sutProvider = GetSutProvider(); - ArrangeValid(sutProvider, user, deviceId); - sutProvider.GetDependency() - .TwoFactorIsEnabledAsync(user) - .Returns(false); - - Assert.False(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token)); - } - - /// - /// A user whose only second factor is enforced by their organization is still covered, so their - /// remember token is honored. - /// - [Theory, BitAutoData] - public async Task ValidateAsync_OrgEnforcesTwoFactor_NoPersonalTwoFactor_ReturnsTrue( - User user, Organization organization, Guid deviceId) - { - var sutProvider = GetSutProvider(); - ArrangeValid(sutProvider, user, deviceId); - sutProvider.GetDependency() - .TwoFactorIsEnabledAsync(user) - .Returns(false); - organization.Use2fa = true; - organization.TwoFactorProviders = _organizationDuoProviders; - - Assert.True(await sutProvider.Sut.ValidateAsync(user, organization, _deviceIdentifier, _token)); - } - - [Theory, BitAutoData] - public async Task ValidateAsync_OrgDoesNotEnforceTwoFactor_NoPersonalTwoFactor_ReturnsFalse( - User user, Organization organization, Guid deviceId) - { - var sutProvider = GetSutProvider(); - ArrangeValid(sutProvider, user, deviceId); - sutProvider.GetDependency() - .TwoFactorIsEnabledAsync(user) - .Returns(false); - organization.Use2fa = false; - organization.TwoFactorProviders = _organizationDuoProviders; - - Assert.False(await sutProvider.Sut.ValidateAsync(user, organization, _deviceIdentifier, _token)); - } - - /// - /// The 2FA-enabled check is ordered ahead of the row read, so the common teardown case rejects - /// without touching the table. - /// - [Theory, BitAutoData] - public async Task ValidateAsync_UserHasNoTwoFactorEnabled_DoesNotReadRow( - User user, Guid deviceId) - { - var sutProvider = GetSutProvider(); - ArrangeValid(sutProvider, user, deviceId); - sutProvider.GetDependency() - .TwoFactorIsEnabledAsync(user) - .Returns(false); - - await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token); - - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .GetByUserIdDeviceIdAsync(default, default); + Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); } /// A token issued to a different user. @@ -201,7 +121,7 @@ public async Task ValidateAsync_UserIdMismatch_ReturnsFalse( var (tokenable, _) = ArrangeValid(sutProvider, user, deviceId); tokenable.UserId = Guid.NewGuid(); - Assert.False(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token)); + Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); } /// The device binding. @@ -212,7 +132,7 @@ public async Task ValidateAsync_DeviceIdentifierMismatch_ReturnsFalse( var sutProvider = GetSutProvider(); ArrangeValid(sutProvider, user, deviceId); - Assert.False(await sutProvider.Sut.ValidateAsync(user, null, "a-different-device", _token)); + Assert.False(await sutProvider.Sut.ValidateAsync(user, "a-different-device", _token)); } /// @@ -226,7 +146,7 @@ public async Task ValidateAsync_DeviceIdentifierDiffersOnlyByCase_ReturnsTrue( var sutProvider = GetSutProvider(); ArrangeValid(sutProvider, user, deviceId); - Assert.True(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier.ToUpperInvariant(), _token)); + Assert.True(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier.ToUpperInvariant(), _token)); } /// No row for this device. @@ -240,7 +160,7 @@ public async Task ValidateAsync_RowMissing_ReturnsFalse( .GetByUserIdDeviceIdAsync(user.Id, deviceId) .Returns((TwoFactorRememberToken?)null); - Assert.False(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token)); + Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); } /// The row outlived its expiry but the sweep has not reached it. @@ -252,7 +172,7 @@ public async Task ValidateAsync_RowExpired_ReturnsFalse( var (_, row) = ArrangeValid(sutProvider, user, deviceId); row.ExpirationDate = _now.AddMinutes(-1); - Assert.False(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token)); + Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); } /// @@ -266,7 +186,7 @@ public async Task ValidateAsync_RowExpiresExactlyNow_ReturnsTrue(User user, Guid var (_, row) = ArrangeValid(sutProvider, user, deviceId); row.ExpirationDate = _now; - Assert.True(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token)); + Assert.True(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); } [Theory, BitAutoData] @@ -276,7 +196,7 @@ public async Task ValidateAsync_RowExpiredOneTickAgo_ReturnsFalse(User user, Gui var (_, row) = ArrangeValid(sutProvider, user, deviceId); row.ExpirationDate = _now.AddTicks(-1); - Assert.False(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token)); + Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); } [Theory, BitAutoData] @@ -288,7 +208,7 @@ public async Task ValidateAsync_TokenCannotBeUnprotected_ReturnsFalse( .TryUnprotect(_token, out Arg.Any()) .Returns(c => { c[1] = null!; return false; }); - Assert.False(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token)); + Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); } [Theory, BitAutoData] @@ -301,7 +221,7 @@ public async Task ValidateAsync_TokenItselfExpired_ReturnsFalse( // injected TimeProvider cannot reach it. tokenable.ExpirationDate = DateTime.UtcNow.AddMinutes(-1); - Assert.False(await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token)); + Assert.False(await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token)); } /// Validation reads; it must never write. @@ -312,7 +232,7 @@ public async Task ValidateAsync_Always_DoesNotWrite( var sutProvider = GetSutProvider(); ArrangeValid(sutProvider, user, deviceId); - await sutProvider.Sut.ValidateAsync(user, null, _deviceIdentifier, _token); + await sutProvider.Sut.ValidateAsync(user, _deviceIdentifier, _token); var repository = sutProvider.GetDependency(); await repository.DidNotReceiveWithAnyArgs().UpsertAsync(default!); From 523502cbf41d1f5d2d079fa3dfa3b1f58ee02ccb Mon Sep 17 00:00:00 2001 From: rr-bw <102181210+rr-bw@users.noreply.github.com> Date: Sat, 17 Oct 2026 07:29:03 -0500 Subject: [PATCH 27/27] [PM-40673 Part 1] bump migrations --- ...10-17_00_CreateTwoFactorRememberToken.sql} | 0 ..._CreateTwoFactorRememberToken.Designer.cs} | 373 +++++++++++++++++- ...017122324_CreateTwoFactorRememberToken.cs} | 0 ..._CreateTwoFactorRememberToken.Designer.cs} | 373 +++++++++++++++++- ...017122240_CreateTwoFactorRememberToken.cs} | 0 ..._CreateTwoFactorRememberToken.Designer.cs} | 373 +++++++++++++++++- ...017122301_CreateTwoFactorRememberToken.cs} | 0 7 files changed, 1116 insertions(+), 3 deletions(-) rename util/Migrator/DbScripts/{2026-10-09_00_CreateTwoFactorRememberToken.sql => 2026-10-17_00_CreateTwoFactorRememberToken.sql} (100%) rename util/MySqlMigrations/Migrations/{20261009164324_CreateTwoFactorRememberToken.Designer.cs => 20261017122324_CreateTwoFactorRememberToken.Designer.cs} (91%) rename util/MySqlMigrations/Migrations/{20261009164324_CreateTwoFactorRememberToken.cs => 20261017122324_CreateTwoFactorRememberToken.cs} (100%) rename util/PostgresMigrations/Migrations/{20261009164244_CreateTwoFactorRememberToken.Designer.cs => 20261017122240_CreateTwoFactorRememberToken.Designer.cs} (91%) rename util/PostgresMigrations/Migrations/{20261009164244_CreateTwoFactorRememberToken.cs => 20261017122240_CreateTwoFactorRememberToken.cs} (100%) rename util/SqliteMigrations/Migrations/{20261009164306_CreateTwoFactorRememberToken.Designer.cs => 20261017122301_CreateTwoFactorRememberToken.Designer.cs} (91%) rename util/SqliteMigrations/Migrations/{20261009164306_CreateTwoFactorRememberToken.cs => 20261017122301_CreateTwoFactorRememberToken.cs} (100%) diff --git a/util/Migrator/DbScripts/2026-10-09_00_CreateTwoFactorRememberToken.sql b/util/Migrator/DbScripts/2026-10-17_00_CreateTwoFactorRememberToken.sql similarity index 100% rename from util/Migrator/DbScripts/2026-10-09_00_CreateTwoFactorRememberToken.sql rename to util/Migrator/DbScripts/2026-10-17_00_CreateTwoFactorRememberToken.sql diff --git a/util/MySqlMigrations/Migrations/20261009164324_CreateTwoFactorRememberToken.Designer.cs b/util/MySqlMigrations/Migrations/20261017122324_CreateTwoFactorRememberToken.Designer.cs similarity index 91% rename from util/MySqlMigrations/Migrations/20261009164324_CreateTwoFactorRememberToken.Designer.cs rename to util/MySqlMigrations/Migrations/20261017122324_CreateTwoFactorRememberToken.Designer.cs index b803eec955f3..865a4524dd06 100644 --- a/util/MySqlMigrations/Migrations/20261009164324_CreateTwoFactorRememberToken.Designer.cs +++ b/util/MySqlMigrations/Migrations/20261017122324_CreateTwoFactorRememberToken.Designer.cs @@ -12,7 +12,7 @@ namespace Bit.MySqlMigrations.Migrations { [DbContext(typeof(DatabaseContext))] - [Migration("20261009164324_CreateTwoFactorRememberToken")] + [Migration("20261017122324_CreateTwoFactorRememberToken")] partial class CreateTwoFactorRememberToken { /// @@ -2787,6 +2787,273 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("AccessRule", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamAccessConnector", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKeyId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("LastHeartbeatDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("ApiKeyId") + .IsUnique(); + + b.HasIndex("OrganizationId"); + + b.ToTable("PamAccessConnector", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamAccessConnectorTargetAssignment", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessConnectorId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("TargetSystemId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("TargetSystemId"); + + b.HasIndex("AccessConnectorId", "TargetSystemId") + .IsUnique(); + + b.ToTable("PamAccessConnectorTargetAssignment", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamLeaseExpirySweep", b => + { + b.Property("AccessLeaseId") + .HasColumnType("char(36)"); + + b.Property("SweptDate") + .HasColumnType("datetime(6)"); + + b.HasKey("AccessLeaseId"); + + b.ToTable("PamLeaseExpirySweep", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationAttempt", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Action") + .HasColumnType("tinyint unsigned"); + + b.Property("CipherUpdated") + .HasColumnType("tinyint(1)"); + + b.Property("ClaimedByAccessConnectorId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("FailureReason") + .HasMaxLength(500) + .HasColumnType("varchar(500)"); + + b.Property("JobId") + .HasColumnType("char(36)"); + + b.Property("ResolvedDate") + .HasColumnType("datetime(6)"); + + b.Property("SessionTermination") + .HasColumnType("tinyint unsigned"); + + b.Property("SyncState") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("ClaimedByAccessConnectorId", "CreationDate") + .IsDescending(false, true); + + b.HasIndex("JobId", "Action"); + + b.ToTable("PamRotationAttempt", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationConfig", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("LastRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("NextRotationDate") + .HasColumnType("datetime(6)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("RotateOnAccessEnd") + .HasColumnType("tinyint(1)"); + + b.Property("ScheduleCron") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SealedData") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("varchar(10000)"); + + b.Property("TargetSystemId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("CipherId") + .IsUnique(); + + b.HasIndex("NextRotationDate"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("TargetSystemId"); + + b.ToTable("PamRotationConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationJob", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Action") + .HasColumnType("tinyint unsigned"); + + b.Property("ClaimedByAccessConnectorId") + .HasColumnType("char(36)"); + + b.Property("ClaimedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("NextClaimableDate") + .HasColumnType("datetime(6)"); + + b.Property("RotationConfigId") + .HasColumnType("char(36)"); + + b.Property("Source") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("Action", "ExpirationDate"); + + b.HasIndex("ClaimedByAccessConnectorId", "Action"); + + b.HasIndex("RotationConfigId", "Action"); + + b.ToTable("PamRotationJob", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationJobTimeoutSweep", b => + { + b.Property("RotationJobId") + .HasColumnType("char(36)"); + + b.Property("SweptDate") + .HasColumnType("datetime(6)"); + + b.HasKey("RotationJobId"); + + b.ToTable("PamRotationJobTimeoutSweep", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamTargetSystem", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Method") + .HasColumnType("tinyint unsigned"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SealedData") + .HasMaxLength(10000) + .HasColumnType("varchar(10000)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("PamTargetSystem", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => { b.Property("Id") @@ -3955,6 +4222,110 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Organization"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamAccessConnector", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", null) + .WithMany() + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamAccessConnectorTargetAssignment", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.PamAccessConnector", null) + .WithMany() + .HasForeignKey("AccessConnectorId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.PamTargetSystem", null) + .WithMany() + .HasForeignKey("TargetSystemId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamLeaseExpirySweep", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessLease", null) + .WithMany() + .HasForeignKey("AccessLeaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationAttempt", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationJob", null) + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.PamTargetSystem", null) + .WithMany() + .HasForeignKey("TargetSystemId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationJob", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationConfig", null) + .WithMany() + .HasForeignKey("RotationConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationJobTimeoutSweep", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationJob", null) + .WithMany() + .HasForeignKey("RotationJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamTargetSystem", 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.ApiKey", b => { b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") diff --git a/util/MySqlMigrations/Migrations/20261009164324_CreateTwoFactorRememberToken.cs b/util/MySqlMigrations/Migrations/20261017122324_CreateTwoFactorRememberToken.cs similarity index 100% rename from util/MySqlMigrations/Migrations/20261009164324_CreateTwoFactorRememberToken.cs rename to util/MySqlMigrations/Migrations/20261017122324_CreateTwoFactorRememberToken.cs diff --git a/util/PostgresMigrations/Migrations/20261009164244_CreateTwoFactorRememberToken.Designer.cs b/util/PostgresMigrations/Migrations/20261017122240_CreateTwoFactorRememberToken.Designer.cs similarity index 91% rename from util/PostgresMigrations/Migrations/20261009164244_CreateTwoFactorRememberToken.Designer.cs rename to util/PostgresMigrations/Migrations/20261017122240_CreateTwoFactorRememberToken.Designer.cs index 70c7eca0b713..7a144e39e0f0 100644 --- a/util/PostgresMigrations/Migrations/20261009164244_CreateTwoFactorRememberToken.Designer.cs +++ b/util/PostgresMigrations/Migrations/20261017122240_CreateTwoFactorRememberToken.Designer.cs @@ -12,7 +12,7 @@ namespace Bit.PostgresMigrations.Migrations { [DbContext(typeof(DatabaseContext))] - [Migration("20261009164244_CreateTwoFactorRememberToken")] + [Migration("20261017122240_CreateTwoFactorRememberToken")] partial class CreateTwoFactorRememberToken { /// @@ -2793,6 +2793,273 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("AccessRule", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamAccessConnector", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKeyId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("LastHeartbeatDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ApiKeyId") + .IsUnique(); + + b.HasIndex("OrganizationId"); + + b.ToTable("PamAccessConnector", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamAccessConnectorTargetAssignment", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessConnectorId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("TargetSystemId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("TargetSystemId"); + + b.HasIndex("AccessConnectorId", "TargetSystemId") + .IsUnique(); + + b.ToTable("PamAccessConnectorTargetAssignment", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamLeaseExpirySweep", b => + { + b.Property("AccessLeaseId") + .HasColumnType("uuid"); + + b.Property("SweptDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("AccessLeaseId"); + + b.ToTable("PamLeaseExpirySweep", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationAttempt", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Action") + .HasColumnType("smallint"); + + b.Property("CipherUpdated") + .HasColumnType("boolean"); + + b.Property("ClaimedByAccessConnectorId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("FailureReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("JobId") + .HasColumnType("uuid"); + + b.Property("ResolvedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SessionTermination") + .HasColumnType("smallint"); + + b.Property("SyncState") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("ClaimedByAccessConnectorId", "CreationDate") + .IsDescending(false, true); + + b.HasIndex("JobId", "Action"); + + b.ToTable("PamRotationAttempt", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationConfig", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("LastRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextRotationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RotateOnAccessEnd") + .HasColumnType("boolean"); + + b.Property("ScheduleCron") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SealedData") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("TargetSystemId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("CipherId") + .IsUnique(); + + b.HasIndex("NextRotationDate"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("TargetSystemId"); + + b.ToTable("PamRotationConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationJob", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Action") + .HasColumnType("smallint"); + + b.Property("ClaimedByAccessConnectorId") + .HasColumnType("uuid"); + + b.Property("ClaimedDate") + .HasColumnType("timestamp with time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("NextClaimableDate") + .HasColumnType("timestamp with time zone"); + + b.Property("RotationConfigId") + .HasColumnType("uuid"); + + b.Property("Source") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("Action", "ExpirationDate"); + + b.HasIndex("ClaimedByAccessConnectorId", "Action"); + + b.HasIndex("RotationConfigId", "Action"); + + b.ToTable("PamRotationJob", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationJobTimeoutSweep", b => + { + b.Property("RotationJobId") + .HasColumnType("uuid"); + + b.Property("SweptDate") + .HasColumnType("timestamp with time zone"); + + b.HasKey("RotationJobId"); + + b.ToTable("PamRotationJobTimeoutSweep", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamTargetSystem", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp with time zone"); + + b.Property("Method") + .HasColumnType("smallint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp with time zone"); + + b.Property("SealedData") + .HasMaxLength(10000) + .HasColumnType("character varying(10000)"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("PamTargetSystem", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => { b.Property("Id") @@ -3961,6 +4228,110 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Organization"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamAccessConnector", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", null) + .WithMany() + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamAccessConnectorTargetAssignment", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.PamAccessConnector", null) + .WithMany() + .HasForeignKey("AccessConnectorId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.PamTargetSystem", null) + .WithMany() + .HasForeignKey("TargetSystemId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamLeaseExpirySweep", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessLease", null) + .WithMany() + .HasForeignKey("AccessLeaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationAttempt", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationJob", null) + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.PamTargetSystem", null) + .WithMany() + .HasForeignKey("TargetSystemId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationJob", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationConfig", null) + .WithMany() + .HasForeignKey("RotationConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationJobTimeoutSweep", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationJob", null) + .WithMany() + .HasForeignKey("RotationJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamTargetSystem", 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.ApiKey", b => { b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") diff --git a/util/PostgresMigrations/Migrations/20261009164244_CreateTwoFactorRememberToken.cs b/util/PostgresMigrations/Migrations/20261017122240_CreateTwoFactorRememberToken.cs similarity index 100% rename from util/PostgresMigrations/Migrations/20261009164244_CreateTwoFactorRememberToken.cs rename to util/PostgresMigrations/Migrations/20261017122240_CreateTwoFactorRememberToken.cs diff --git a/util/SqliteMigrations/Migrations/20261009164306_CreateTwoFactorRememberToken.Designer.cs b/util/SqliteMigrations/Migrations/20261017122301_CreateTwoFactorRememberToken.Designer.cs similarity index 91% rename from util/SqliteMigrations/Migrations/20261009164306_CreateTwoFactorRememberToken.Designer.cs rename to util/SqliteMigrations/Migrations/20261017122301_CreateTwoFactorRememberToken.Designer.cs index b2018f44d3bb..5bc87f1b207a 100644 --- a/util/SqliteMigrations/Migrations/20261009164306_CreateTwoFactorRememberToken.Designer.cs +++ b/util/SqliteMigrations/Migrations/20261017122301_CreateTwoFactorRememberToken.Designer.cs @@ -11,7 +11,7 @@ namespace Bit.SqliteMigrations.Migrations { [DbContext(typeof(DatabaseContext))] - [Migration("20261009164306_CreateTwoFactorRememberToken")] + [Migration("20261017122301_CreateTwoFactorRememberToken")] partial class CreateTwoFactorRememberToken { /// @@ -2776,6 +2776,273 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.ToTable("AccessRule", (string)null); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamAccessConnector", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("ApiKeyId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("LastHeartbeatDate") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ApiKeyId") + .IsUnique(); + + b.HasIndex("OrganizationId"); + + b.ToTable("PamAccessConnector", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamAccessConnectorTargetAssignment", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("AccessConnectorId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("TargetSystemId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("TargetSystemId"); + + b.HasIndex("AccessConnectorId", "TargetSystemId") + .IsUnique(); + + b.ToTable("PamAccessConnectorTargetAssignment", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamLeaseExpirySweep", b => + { + b.Property("AccessLeaseId") + .HasColumnType("TEXT"); + + b.Property("SweptDate") + .HasColumnType("TEXT"); + + b.HasKey("AccessLeaseId"); + + b.ToTable("PamLeaseExpirySweep", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationAttempt", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Action") + .HasColumnType("INTEGER"); + + b.Property("CipherUpdated") + .HasColumnType("INTEGER"); + + b.Property("ClaimedByAccessConnectorId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("FailureReason") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("JobId") + .HasColumnType("TEXT"); + + b.Property("ResolvedDate") + .HasColumnType("TEXT"); + + b.Property("SessionTermination") + .HasColumnType("INTEGER"); + + b.Property("SyncState") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ClaimedByAccessConnectorId", "CreationDate") + .IsDescending(false, true); + + b.HasIndex("JobId", "Action"); + + b.ToTable("PamRotationAttempt", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationConfig", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CipherId") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("LastRotationDate") + .HasColumnType("TEXT"); + + b.Property("NextRotationDate") + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("RotateOnAccessEnd") + .HasColumnType("INTEGER"); + + b.Property("ScheduleCron") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("SealedData") + .IsRequired() + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("TargetSystemId") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CipherId") + .IsUnique(); + + b.HasIndex("NextRotationDate"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("TargetSystemId"); + + b.ToTable("PamRotationConfig", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationJob", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("Action") + .HasColumnType("INTEGER"); + + b.Property("ClaimedByAccessConnectorId") + .HasColumnType("TEXT"); + + b.Property("ClaimedDate") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("ExpirationDate") + .HasColumnType("TEXT"); + + b.Property("NextClaimableDate") + .HasColumnType("TEXT"); + + b.Property("RotationConfigId") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("Action", "ExpirationDate"); + + b.HasIndex("ClaimedByAccessConnectorId", "Action"); + + b.HasIndex("RotationConfigId", "Action"); + + b.ToTable("PamRotationJob", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationJobTimeoutSweep", b => + { + b.Property("RotationJobId") + .HasColumnType("TEXT"); + + b.Property("SweptDate") + .HasColumnType("TEXT"); + + b.HasKey("RotationJobId"); + + b.ToTable("PamRotationJobTimeoutSweep", (string)null); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamTargetSystem", b => + { + b.Property("Id") + .HasColumnType("TEXT"); + + b.Property("CreationDate") + .HasColumnType("TEXT"); + + b.Property("Method") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("TEXT"); + + b.Property("OrganizationId") + .HasColumnType("TEXT"); + + b.Property("RevisionDate") + .HasColumnType("TEXT"); + + b.Property("SealedData") + .HasMaxLength(10000) + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("PamTargetSystem", (string)null); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Platform.Installation", b => { b.Property("Id") @@ -3944,6 +4211,110 @@ protected override void BuildTargetModel(ModelBuilder modelBuilder) b.Navigation("Organization"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamAccessConnector", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ApiKey", null) + .WithMany() + .HasForeignKey("ApiKeyId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamAccessConnectorTargetAssignment", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.PamAccessConnector", null) + .WithMany() + .HasForeignKey("AccessConnectorId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.PamTargetSystem", null) + .WithMany() + .HasForeignKey("TargetSystemId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamLeaseExpirySweep", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.AccessLease", null) + .WithMany() + .HasForeignKey("AccessLeaseId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationAttempt", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationJob", null) + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.PamTargetSystem", null) + .WithMany() + .HasForeignKey("TargetSystemId") + .OnDelete(DeleteBehavior.NoAction) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationJob", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationConfig", null) + .WithMany() + .HasForeignKey("RotationConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationJobTimeoutSweep", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Pam.Models.PamRotationJob", null) + .WithMany() + .HasForeignKey("RotationJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Pam.Models.PamTargetSystem", 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.ApiKey", b => { b.HasOne("Bit.Infrastructure.EntityFramework.SecretsManager.Models.ServiceAccount", "ServiceAccount") diff --git a/util/SqliteMigrations/Migrations/20261009164306_CreateTwoFactorRememberToken.cs b/util/SqliteMigrations/Migrations/20261017122301_CreateTwoFactorRememberToken.cs similarity index 100% rename from util/SqliteMigrations/Migrations/20261009164306_CreateTwoFactorRememberToken.cs rename to util/SqliteMigrations/Migrations/20261017122301_CreateTwoFactorRememberToken.cs