1
0
mirror of https://github.com/bitwarden/server.git synced 2025-07-01 16:12:49 -05:00

Merge branch 'master' into feature/flexible-collections

This commit is contained in:
Thomas Rittson
2023-09-29 09:23:47 +10:00
committed by GitHub
36 changed files with 7061 additions and 55 deletions

View File

@ -36,6 +36,7 @@ public class CiphersController : Controller
private readonly ICurrentContext _currentContext;
private readonly ILogger<CiphersController> _logger;
private readonly GlobalSettings _globalSettings;
private readonly Version _cipherKeyEncryptionMinimumVersion = new Version(Constants.CipherKeyEncryptionMinimumVersion);
public CiphersController(
ICipherRepository cipherRepository,
@ -177,6 +178,8 @@ public class CiphersController : Controller
throw new NotFoundException();
}
ValidateItemLevelEncryptionIsAvailable(cipher);
var collectionIds = (await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(userId, id)).Select(c => c.CollectionId).ToList();
var modelOrgId = string.IsNullOrWhiteSpace(model.OrganizationId) ?
(Guid?)null : new Guid(model.OrganizationId);
@ -198,6 +201,9 @@ public class CiphersController : Controller
{
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(id);
ValidateItemLevelEncryptionIsAvailable(cipher);
if (cipher == null || !cipher.OrganizationId.HasValue ||
!await _currentContext.EditAnyCollection(cipher.OrganizationId.Value))
{
@ -576,6 +582,8 @@ public class CiphersController : Controller
throw new NotFoundException();
}
ValidateItemLevelEncryptionIsAvailable(cipher);
if (request.FileSize > CipherService.MAX_FILE_SIZE)
{
throw new BadRequestException($"Max file size is {CipherService.MAX_FILE_SIZE_READABLE}.");
@ -795,4 +803,12 @@ public class CiphersController : Controller
throw new BadRequestException("Invalid content.");
}
}
private void ValidateItemLevelEncryptionIsAvailable(Cipher cipher)
{
if (cipher.Key != null && _currentContext.ClientVersion < _cipherKeyEncryptionMinimumVersion)
{
throw new BadRequestException("Cannot edit item. Update to the latest version of Bitwarden and try again.");
}
}
}

View File

@ -18,6 +18,7 @@ public class CipherRequestModel
public string FolderId { get; set; }
public bool Favorite { get; set; }
public CipherRepromptType Reprompt { get; set; }
public string Key { get; set; }
[Required]
[EncryptedString]
[EncryptedStringLength(1000)]
@ -86,6 +87,7 @@ public class CipherRequestModel
}
existingCipher.Reprompt = Reprompt;
existingCipher.Key = Key;
var hasAttachments2 = (Attachments2?.Count ?? 0) > 0;
var hasAttachments = (Attachments?.Count ?? 0) > 0;

View File

@ -63,6 +63,7 @@ public class CipherMiniResponseModel : ResponseModel
CreationDate = cipher.CreationDate;
DeletedDate = cipher.DeletedDate;
Reprompt = cipher.Reprompt.GetValueOrDefault(CipherRepromptType.None);
Key = cipher.Key;
}
public Guid Id { get; set; }
@ -83,6 +84,7 @@ public class CipherMiniResponseModel : ResponseModel
public DateTime CreationDate { get; set; }
public DateTime? DeletedDate { get; set; }
public CipherRepromptType Reprompt { get; set; }
public string Key { get; set; }
}
public class CipherResponseModel : CipherMiniResponseModel

View File

@ -19,6 +19,8 @@ public static class Constants
/// their subscription has expired.
/// </summary>
public const int OrganizationSelfHostSubscriptionGracePeriodDays = 60;
public const string CipherKeyEncryptionMinimumVersion = "2023.9.1";
}
public static class TokenPurposes

View File

@ -21,6 +21,7 @@ public class Cipher : ITableObject<Guid>, ICloneable
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
public DateTime? DeletedDate { get; set; }
public Enums.CipherRepromptType? Reprompt { get; set; }
public string Key { get; set; }
public void SetNewId()
{

View File

@ -392,7 +392,8 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
SET
[Data] = TC.[Data],
[Attachments] = TC.[Attachments],
[RevisionDate] = TC.[RevisionDate]
[RevisionDate] = TC.[RevisionDate],
[Key] = TC.[Key]
FROM
[dbo].[Cipher] C
INNER JOIN
@ -506,7 +507,8 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
[Data] = TC.[Data],
[Attachments] = TC.[Attachments],
[RevisionDate] = TC.[RevisionDate],
[DeletedDate] = TC.[DeletedDate]
[DeletedDate] = TC.[DeletedDate],
[Key] = TC.[Key]
FROM
[dbo].[Cipher] C
INNER JOIN
@ -728,6 +730,8 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
ciphersTable.Columns.Add(deletedDateColumn);
var repromptColumn = new DataColumn(nameof(c.Reprompt), typeof(short));
ciphersTable.Columns.Add(repromptColumn);
var keyColummn = new DataColumn(nameof(c.Key), typeof(string));
ciphersTable.Columns.Add(keyColummn);
foreach (DataColumn col in ciphersTable.Columns)
{
@ -754,6 +758,7 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
row[revisionDateColumn] = cipher.RevisionDate;
row[deletedDateColumn] = cipher.DeletedDate.HasValue ? (object)cipher.DeletedDate : DBNull.Value;
row[repromptColumn] = cipher.Reprompt;
row[keyColummn] = cipher.Key;
ciphersTable.Rows.Add(row);
}

View File

@ -73,6 +73,7 @@ public class UserCipherDetailsQuery : IQuery<CipherDetails>
Reprompt = c.Reprompt,
ViewPassword = true,
OrganizationUseTotp = false,
Key = c.Key
});
return union;
}

View File

@ -366,6 +366,7 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
Reprompt = c.Reprompt,
ViewPassword = true,
OrganizationUseTotp = false,
Key = c.Key
};
}
var ciphers = await cipherDetailsView.ToListAsync();
@ -591,6 +592,7 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
trackedCipher.Attachments = cipher.Attachments;
trackedCipher.RevisionDate = cipher.RevisionDate;
trackedCipher.DeletedDate = cipher.DeletedDate;
trackedCipher.Key = cipher.Key;
await transaction.CommitAsync();

View File

@ -29,6 +29,7 @@ public class CipherDetailsQuery : IQuery<CipherDetails>
RevisionDate = c.RevisionDate,
DeletedDate = c.DeletedDate,
Reprompt = c.Reprompt,
Key = c.Key,
Favorite = _userId.HasValue && c.Favorites != null && c.Favorites.ToLowerInvariant().Contains($"\"{_userId}\":true"),
FolderId = (_ignoreFolders || !_userId.HasValue || c.Folders == null || !c.Folders.ToLowerInvariant().Contains(_userId.Value.ToString())) ?
null :

View File

@ -26,6 +26,7 @@ SELECT
ELSE TRY_CONVERT(UNIQUEIDENTIFIER, JSON_VALUE(C.[Folders], CONCAT('$."', @UserId, '"')))
END [FolderId],
C.[DeletedDate],
C.[Reprompt]
C.[Reprompt],
C.[Key]
FROM
[dbo].[Cipher] C

View File

@ -15,7 +15,8 @@
@ViewPassword BIT, -- not used
@OrganizationUseTotp BIT, -- not used
@DeletedDate DATETIME2(7),
@Reprompt TINYINT
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL
AS
BEGIN
SET NOCOUNT ON
@ -35,7 +36,8 @@ BEGIN
[CreationDate],
[RevisionDate],
[DeletedDate],
[Reprompt]
[Reprompt],
[Key]
)
VALUES
(
@ -49,7 +51,8 @@ BEGIN
@CreationDate,
@RevisionDate,
@DeletedDate,
@Reprompt
@Reprompt,
@Key
)
IF @OrganizationId IS NOT NULL

View File

@ -16,6 +16,7 @@
@OrganizationUseTotp BIT, -- not used
@DeletedDate DATETIME2(7),
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL,
@CollectionIds AS [dbo].[GuidIdArray] READONLY
AS
BEGIN
@ -23,7 +24,7 @@ BEGIN
EXEC [dbo].[CipherDetails_Create] @Id, @UserId, @OrganizationId, @Type, @Data, @Favorites, @Folders,
@Attachments, @CreationDate, @RevisionDate, @FolderId, @Favorite, @Edit, @ViewPassword,
@OrganizationUseTotp, @DeletedDate, @Reprompt
@OrganizationUseTotp, @DeletedDate, @Reprompt, @Key
DECLARE @UpdateCollectionsSuccess INT
EXEC @UpdateCollectionsSuccess = [dbo].[Cipher_UpdateCollections] @Id, @UserId, @OrganizationId, @CollectionIds

View File

@ -6,7 +6,7 @@
@Data NVARCHAR(MAX),
@Favorites NVARCHAR(MAX), -- not used
@Folders NVARCHAR(MAX), -- not used
@Attachments NVARCHAR(MAX), -- not used
@Attachments NVARCHAR(MAX),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@FolderId UNIQUEIDENTIFIER,
@ -15,7 +15,8 @@
@ViewPassword BIT, -- not used
@OrganizationUseTotp BIT, -- not used
@DeletedDate DATETIME2(2),
@Reprompt TINYINT
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL
AS
BEGIN
SET NOCOUNT ON
@ -48,10 +49,12 @@ BEGIN
ELSE
JSON_MODIFY([Favorites], @UserIdPath, NULL)
END,
[Attachments] = @Attachments,
[Reprompt] = @Reprompt,
[CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate,
[DeletedDate] = @DeletedDate
[DeletedDate] = @DeletedDate,
[Key] = @Key
WHERE
[Id] = @Id

View File

@ -10,7 +10,8 @@
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@DeletedDate DATETIME2(7),
@Reprompt TINYINT
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL
AS
BEGIN
SET NOCOUNT ON
@ -28,7 +29,8 @@ BEGIN
[CreationDate],
[RevisionDate],
[DeletedDate],
[Reprompt]
[Reprompt],
[Key]
)
VALUES
(
@ -43,7 +45,8 @@ BEGIN
@CreationDate,
@RevisionDate,
@DeletedDate,
@Reprompt
@Reprompt,
@Key
)
IF @OrganizationId IS NOT NULL

View File

@ -11,13 +11,14 @@
@RevisionDate DATETIME2(7),
@DeletedDate DATETIME2(7),
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL,
@CollectionIds AS [dbo].[GuidIdArray] READONLY
AS
BEGIN
SET NOCOUNT ON
EXEC [dbo].[Cipher_Create] @Id, @UserId, @OrganizationId, @Type, @Data, @Favorites, @Folders,
@Attachments, @CreationDate, @RevisionDate, @DeletedDate, @Reprompt
@Attachments, @CreationDate, @RevisionDate, @DeletedDate, @Reprompt, @Key
DECLARE @UpdateCollectionsSuccess INT
EXEC @UpdateCollectionsSuccess = [dbo].[Cipher_UpdateCollections] @Id, @UserId, @OrganizationId, @CollectionIds

View File

@ -10,7 +10,8 @@
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@DeletedDate DATETIME2(7),
@Reprompt TINYINT
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL
AS
BEGIN
SET NOCOUNT ON
@ -28,7 +29,8 @@ BEGIN
[CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate,
[DeletedDate] = @DeletedDate,
[Reprompt] = @Reprompt
[Reprompt] = @Reprompt,
[Key] = @Key
WHERE
[Id] = @Id

View File

@ -11,6 +11,7 @@
@RevisionDate DATETIME2(7),
@DeletedDate DATETIME2(7),
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL,
@CollectionIds AS [dbo].[GuidIdArray] READONLY
AS
BEGIN
@ -36,7 +37,8 @@ BEGIN
[Data] = @Data,
[Attachments] = @Attachments,
[RevisionDate] = @RevisionDate,
[DeletedDate] = @DeletedDate
[DeletedDate] = @DeletedDate,
[Key] = @Key
-- No need to update CreationDate, Favorites, Folders, or Type since that data will not change
WHERE
[Id] = @Id

View File

@ -12,6 +12,7 @@ CREATE TABLE [dbo].[Cipher] (
[RevisionDate] DATETIME2 (7) NOT NULL,
[DeletedDate] DATETIME2 (7) NULL,
[Reprompt] TINYINT NULL,
[Key] VARCHAR(MAX) NULL,
CONSTRAINT [PK_Cipher] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_Cipher_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]),
CONSTRAINT [FK_Cipher_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])

View File

@ -2,6 +2,7 @@
using System.Reflection;
using Bit.Core;
using DbUp;
using DbUp.Helpers;
using Microsoft.Data.SqlClient;
using Microsoft.Extensions.Logging;
@ -24,6 +25,8 @@ public class DbMigrator
}
public bool MigrateMsSqlDatabaseWithRetries(bool enableLogging = true,
bool repeatable = false,
string folderName = MigratorConstants.DefaultMigrationsFolderName,
CancellationToken cancellationToken = default(CancellationToken))
{
var attempt = 1;
@ -32,7 +35,7 @@ public class DbMigrator
{
try
{
var success = MigrateDatabase(enableLogging, cancellationToken);
var success = MigrateDatabase(enableLogging, repeatable, folderName, cancellationToken);
return success;
}
catch (SqlException ex)
@ -54,6 +57,8 @@ public class DbMigrator
}
public bool MigrateDatabase(bool enableLogging = true,
bool repeatable = false,
string folderName = MigratorConstants.DefaultMigrationsFolderName,
CancellationToken cancellationToken = default(CancellationToken))
{
if (_logger != null)
@ -98,12 +103,20 @@ public class DbMigrator
cancellationToken.ThrowIfCancellationRequested();
var builder = DeployChanges.To
.SqlDatabase(_connectionString)
.JournalToSqlTable("dbo", "Migration")
.WithScriptsAndCodeEmbeddedInAssembly(Assembly.GetExecutingAssembly(),
s => s.Contains($".DbScripts.") && !s.Contains(".Archive."))
s => s.Contains($".{folderName}.") && !s.Contains(".Archive."))
.WithTransaction()
.WithExecutionTimeout(new TimeSpan(0, 5, 0));
if (repeatable)
{
builder.JournalTo(new NullJournal());
}
else
{
builder.JournalToSqlTable("dbo", MigratorConstants.SqlTableJournalName);
}
if (enableLogging)
{
if (_logger != null)

View File

@ -0,0 +1,428 @@
-- Add Key Column
IF COL_LENGTH('[dbo].[Cipher]', 'Key') IS NULL
BEGIN
ALTER TABLE [dbo].[Cipher] ADD [Key] VARCHAR (MAX) NULL;
END
GO
-- Remove ForceKeyRotation Column from the Cipher table
-- (this is to help with the dev work as the column was no longer needed)
IF COL_LENGTH('[dbo].[Cipher]', 'ForceKeyRotation') IS NOT NULL
BEGIN
ALTER TABLE [dbo].[Cipher] DROP CONSTRAINT [D_Cipher_ForceKeyRotation];
ALTER TABLE [dbo].[Cipher] DROP COLUMN [ForceKeyRotation];
END
GO
CREATE OR ALTER FUNCTION [dbo].[CipherDetails](@UserId UNIQUEIDENTIFIER)
RETURNS TABLE
AS RETURN
SELECT
C.[Id],
C.[UserId],
C.[OrganizationId],
C.[Type],
C.[Data],
C.[Attachments],
C.[CreationDate],
C.[RevisionDate],
CASE
WHEN
@UserId IS NULL
OR C.[Favorites] IS NULL
OR JSON_VALUE(C.[Favorites], CONCAT('$."', @UserId, '"')) IS NULL
THEN 0
ELSE 1
END [Favorite],
CASE
WHEN
@UserId IS NULL
OR C.[Folders] IS NULL
THEN NULL
ELSE TRY_CONVERT(UNIQUEIDENTIFIER, JSON_VALUE(C.[Folders], CONCAT('$."', @UserId, '"')))
END [FolderId],
C.[DeletedDate],
C.[Reprompt],
C.[Key]
FROM
[dbo].[Cipher] C
GO
IF OBJECT_ID('[dbo].[UserCipherDetails]') IS NOT NULL
BEGIN
EXECUTE sp_refreshsqlmodule N'[dbo].[UserCipherDetails]';
END
GO
IF OBJECT_ID('[dbo].[CipherView]') IS NOT NULL
BEGIN
EXECUTE sp_refreshsqlmodule N'[dbo].[CipherView]';
END
GO
CREATE OR ALTER PROCEDURE [dbo].[CipherDetails_Create]
@Id UNIQUEIDENTIFIER,
@UserId UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
@Type TINYINT,
@Data NVARCHAR(MAX),
@Favorites NVARCHAR(MAX), -- not used
@Folders NVARCHAR(MAX), -- not used
@Attachments NVARCHAR(MAX), -- not used
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@FolderId UNIQUEIDENTIFIER,
@Favorite BIT,
@Edit BIT, -- not used
@ViewPassword BIT, -- not used
@OrganizationUseTotp BIT, -- not used
@DeletedDate DATETIME2(7),
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL
AS
BEGIN
SET NOCOUNT ON
DECLARE @UserIdKey VARCHAR(50) = CONCAT('"', @UserId, '"')
DECLARE @UserIdPath VARCHAR(50) = CONCAT('$.', @UserIdKey)
INSERT INTO [dbo].[Cipher]
(
[Id],
[UserId],
[OrganizationId],
[Type],
[Data],
[Favorites],
[Folders],
[CreationDate],
[RevisionDate],
[DeletedDate],
[Reprompt],
[Key]
)
VALUES
(
@Id,
CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END,
@OrganizationId,
@Type,
@Data,
CASE WHEN @Favorite = 1 THEN CONCAT('{', @UserIdKey, ':true}') ELSE NULL END,
CASE WHEN @FolderId IS NOT NULL THEN CONCAT('{', @UserIdKey, ':"', @FolderId, '"', '}') ELSE NULL END,
@CreationDate,
@RevisionDate,
@DeletedDate,
@Reprompt,
@Key
)
IF @OrganizationId IS NOT NULL
BEGIN
EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId
END
ELSE IF @UserId IS NOT NULL
BEGIN
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
END
END
GO
CREATE OR ALTER PROCEDURE [dbo].[CipherDetails_Update]
@Id UNIQUEIDENTIFIER,
@UserId UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
@Type TINYINT,
@Data NVARCHAR(MAX),
@Favorites NVARCHAR(MAX), -- not used
@Folders NVARCHAR(MAX), -- not used
@Attachments NVARCHAR(MAX),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@FolderId UNIQUEIDENTIFIER,
@Favorite BIT,
@Edit BIT, -- not used
@ViewPassword BIT, -- not used
@OrganizationUseTotp BIT, -- not used
@DeletedDate DATETIME2(2),
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL
AS
BEGIN
SET NOCOUNT ON
DECLARE @UserIdKey VARCHAR(50) = CONCAT('"', @UserId, '"')
DECLARE @UserIdPath VARCHAR(50) = CONCAT('$.', @UserIdKey)
UPDATE
[dbo].[Cipher]
SET
[UserId] = CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END,
[OrganizationId] = @OrganizationId,
[Type] = @Type,
[Data] = @Data,
[Folders] =
CASE
WHEN @FolderId IS NOT NULL AND [Folders] IS NULL THEN
CONCAT('{', @UserIdKey, ':"', @FolderId, '"', '}')
WHEN @FolderId IS NOT NULL THEN
JSON_MODIFY([Folders], @UserIdPath, CAST(@FolderId AS VARCHAR(50)))
ELSE
JSON_MODIFY([Folders], @UserIdPath, NULL)
END,
[Favorites] =
CASE
WHEN @Favorite = 1 AND [Favorites] IS NULL THEN
CONCAT('{', @UserIdKey, ':true}')
WHEN @Favorite = 1 THEN
JSON_MODIFY([Favorites], @UserIdPath, CAST(1 AS BIT))
ELSE
JSON_MODIFY([Favorites], @UserIdPath, NULL)
END,
[Attachments] = @Attachments,
[Reprompt] = @Reprompt,
[CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate,
[DeletedDate] = @DeletedDate,
[Key] = @Key
WHERE
[Id] = @Id
IF @OrganizationId IS NOT NULL
BEGIN
EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId
END
ELSE IF @UserId IS NOT NULL
BEGIN
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
END
END
GO
CREATE OR ALTER PROCEDURE [dbo].[Cipher_Update]
@Id UNIQUEIDENTIFIER,
@UserId UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
@Type TINYINT,
@Data NVARCHAR(MAX),
@Favorites NVARCHAR(MAX),
@Folders NVARCHAR(MAX),
@Attachments NVARCHAR(MAX),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@DeletedDate DATETIME2(7),
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL
AS
BEGIN
SET NOCOUNT ON
UPDATE
[dbo].[Cipher]
SET
[UserId] = CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END,
[OrganizationId] = @OrganizationId,
[Type] = @Type,
[Data] = @Data,
[Favorites] = @Favorites,
[Folders] = @Folders,
[Attachments] = @Attachments,
[CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate,
[DeletedDate] = @DeletedDate,
[Reprompt] = @Reprompt,
[Key] = @Key
WHERE
[Id] = @Id
IF @OrganizationId IS NOT NULL
BEGIN
EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId
END
ELSE IF @UserId IS NOT NULL
BEGIN
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
END
END
GO
CREATE OR ALTER PROCEDURE [dbo].[Cipher_Create]
@Id UNIQUEIDENTIFIER OUTPUT,
@UserId UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
@Type TINYINT,
@Data NVARCHAR(MAX),
@Favorites NVARCHAR(MAX),
@Folders NVARCHAR(MAX),
@Attachments NVARCHAR(MAX),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@DeletedDate DATETIME2(7),
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL
AS
BEGIN
SET NOCOUNT ON
INSERT INTO [dbo].[Cipher]
(
[Id],
[UserId],
[OrganizationId],
[Type],
[Data],
[Favorites],
[Folders],
[Attachments],
[CreationDate],
[RevisionDate],
[DeletedDate],
[Reprompt],
[Key]
)
VALUES
(
@Id,
CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END,
@OrganizationId,
@Type,
@Data,
@Favorites,
@Folders,
@Attachments,
@CreationDate,
@RevisionDate,
@DeletedDate,
@Reprompt,
@Key
)
IF @OrganizationId IS NOT NULL
BEGIN
EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId
END
ELSE IF @UserId IS NOT NULL
BEGIN
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
END
END
GO
CREATE OR ALTER PROCEDURE [dbo].[Cipher_CreateWithCollections]
@Id UNIQUEIDENTIFIER,
@UserId UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
@Type TINYINT,
@Data NVARCHAR(MAX),
@Favorites NVARCHAR(MAX),
@Folders NVARCHAR(MAX),
@Attachments NVARCHAR(MAX),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@DeletedDate DATETIME2(7),
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL,
@CollectionIds AS [dbo].[GuidIdArray] READONLY
AS
BEGIN
SET NOCOUNT ON
EXEC [dbo].[Cipher_Create] @Id, @UserId, @OrganizationId, @Type, @Data, @Favorites, @Folders,
@Attachments, @CreationDate, @RevisionDate, @DeletedDate, @Reprompt, @Key
DECLARE @UpdateCollectionsSuccess INT
EXEC @UpdateCollectionsSuccess = [dbo].[Cipher_UpdateCollections] @Id, @UserId, @OrganizationId, @CollectionIds
END
GO
CREATE OR ALTER PROCEDURE [dbo].[Cipher_UpdateWithCollections]
@Id UNIQUEIDENTIFIER,
@UserId UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
@Type TINYINT,
@Data NVARCHAR(MAX),
@Favorites NVARCHAR(MAX),
@Folders NVARCHAR(MAX),
@Attachments NVARCHAR(MAX),
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@DeletedDate DATETIME2(7),
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL,
@CollectionIds AS [dbo].[GuidIdArray] READONLY
AS
BEGIN
SET NOCOUNT ON
BEGIN TRANSACTION Cipher_UpdateWithCollections
DECLARE @UpdateCollectionsSuccess INT
EXEC @UpdateCollectionsSuccess = [dbo].[Cipher_UpdateCollections] @Id, @UserId, @OrganizationId, @CollectionIds
IF @UpdateCollectionsSuccess < 0
BEGIN
COMMIT TRANSACTION Cipher_UpdateWithCollections
SELECT -1 -- -1 = Failure
RETURN
END
UPDATE
[dbo].[Cipher]
SET
[UserId] = NULL,
[OrganizationId] = @OrganizationId,
[Data] = @Data,
[Attachments] = @Attachments,
[RevisionDate] = @RevisionDate,
[DeletedDate] = @DeletedDate,
[Key] = @Key
-- No need to update CreationDate, Favorites, Folders, or Type since that data will not change
WHERE
[Id] = @Id
COMMIT TRANSACTION Cipher_UpdateWithCollections
IF @Attachments IS NOT NULL
BEGIN
EXEC [dbo].[Organization_UpdateStorage] @OrganizationId
EXEC [dbo].[User_UpdateStorage] @UserId
END
EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId
SELECT 0 -- 0 = Success
END
GO
CREATE OR ALTER PROCEDURE [dbo].[CipherDetails_CreateWithCollections]
@Id UNIQUEIDENTIFIER,
@UserId UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
@Type TINYINT,
@Data NVARCHAR(MAX),
@Favorites NVARCHAR(MAX), -- not used
@Folders NVARCHAR(MAX), -- not used
@Attachments NVARCHAR(MAX), -- not used
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@FolderId UNIQUEIDENTIFIER,
@Favorite BIT,
@Edit BIT, -- not used
@ViewPassword BIT, -- not used
@OrganizationUseTotp BIT, -- not used
@DeletedDate DATETIME2(7),
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL,
@CollectionIds AS [dbo].[GuidIdArray] READONLY
AS
BEGIN
SET NOCOUNT ON
EXEC [dbo].[CipherDetails_Create] @Id, @UserId, @OrganizationId, @Type, @Data, @Favorites, @Folders,
@Attachments, @CreationDate, @RevisionDate, @FolderId, @Favorite, @Edit, @ViewPassword,
@OrganizationUseTotp, @DeletedDate, @Reprompt, @Key
DECLARE @UpdateCollectionsSuccess INT
EXEC @UpdateCollectionsSuccess = [dbo].[Cipher_UpdateCollections] @Id, @UserId, @OrganizationId, @CollectionIds
END
GO

View File

@ -2,6 +2,7 @@
<ItemGroup>
<EmbeddedResource Include="DbScripts\**\*.sql" />
<EmbeddedResource Include="DbScripts_data_migration\**\*.sql" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,8 @@
namespace Bit.Migrator;
public static class MigratorConstants
{
public const string SqlTableJournalName = "Migration";
public const string DefaultMigrationsFolderName = "DbScripts";
public const string TransitionMigrationsFolderName = "DbScripts_data_migration";
}

View File

@ -70,7 +70,7 @@ public class SqlServerDbMigrator : IDbMigrator
cancellationToken.ThrowIfCancellationRequested();
var builder = DeployChanges.To
.SqlDatabase(_connectionString)
.JournalToSqlTable("dbo", "Migration")
.JournalToSqlTable("dbo", MigratorConstants.SqlTableJournalName)
.WithScriptsAndCodeEmbeddedInAssembly(Assembly.GetExecutingAssembly(),
s => s.Contains($".DbScripts.") && !s.Contains(".Archive."))
.WithTransaction()

View File

@ -10,6 +10,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommandDotNet" Version="7.0.2" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="6.0.0" />
</ItemGroup>

View File

@ -1,55 +1,51 @@
using Bit.Migrator;
using CommandDotNet;
using Microsoft.Extensions.Logging;
internal class Program
{
private static IDictionary<string, string> Parameters { get; set; }
private static int Main(string[] args)
{
if (args.Length == 0)
{
Console.WriteLine("Please enter a database connection string argument.");
WriteUsageToConsole();
return 1;
return new AppRunner<Program>().Run(args);
}
if (args.Length == 1 && (args[0] == "--verbose" || args[0] == "-v"))
{
Console.WriteLine($"Please enter a database connection string argument before {args[0]} option.");
WriteUsageToConsole();
return 1;
}
var databaseConnectionString = args[0];
var verbose = false;
if (args.Length == 2 && (args[1] == "--verbose" || args[1] == "-v"))
{
verbose = true;
}
var success = MigrateDatabase(databaseConnectionString, verbose);
if (!success)
{
return -1;
}
return 0;
}
[DefaultCommand]
public void Execute(
[Operand(Description = "Database connection string")]
string databaseConnectionString,
[Option('v', "verbose", Description = "Enable verbose output of migrator logs")]
bool verbose = false,
[Option('r', "repeatable", Description = "Mark scripts as repeatable")]
bool repeatable = false,
[Option('f', "folder", Description = "Folder name of database scripts")]
string folderName = MigratorConstants.DefaultMigrationsFolderName) => MigrateDatabase(databaseConnectionString, verbose, repeatable, folderName);
private static void WriteUsageToConsole()
{
Console.WriteLine("Usage: MsSqlMigratorUtility <database-connection-string>");
Console.WriteLine("Usage: MsSqlMigratorUtility <database-connection-string> -v|--verbose (for verbose output of migrator logs)");
Console.WriteLine("Usage: MsSqlMigratorUtility <database-connection-string> -r|--repeatable (for marking scripts as repeatable) -f|--folder <folder-name-in-migrator-project> (for specifying folder name of scripts)");
Console.WriteLine("Usage: MsSqlMigratorUtility <database-connection-string> -v|--verbose (for verbose output of migrator logs) -r|--repeatable (for marking scripts as repeatable) -f|--folder <folder-name-in-migrator-project> (for specifying folder name of scripts)");
}
private static bool MigrateDatabase(string databaseConnectionString, bool verbose = false, int attempt = 1)
private static bool MigrateDatabase(string databaseConnectionString, bool verbose = false, bool repeatable = false, string folderName = "")
{
var logger = CreateLogger(verbose);
logger.LogInformation($"Migrating database with repeatable: {repeatable} and folderName: {folderName}.");
var migrator = new DbMigrator(databaseConnectionString, logger);
var success = migrator.MigrateMsSqlDatabaseWithRetries(verbose);
bool success = false;
if (!string.IsNullOrWhiteSpace(folderName))
{
success = migrator.MigrateMsSqlDatabaseWithRetries(verbose, repeatable, folderName);
}
else
{
success = migrator.MigrateMsSqlDatabaseWithRetries(verbose, repeatable);
}
return success;
}

View File

@ -2,6 +2,15 @@
"version": 1,
"dependencies": {
"net6.0": {
"CommandDotNet": {
"type": "Direct",
"requested": "[7.0.2, )",
"resolved": "7.0.2",
"contentHash": "sFfqn4T6Ux4AbGnhS+BZvf9iVeP6b9p9bwaMT8nsefVcYH2tC9MHj3AoY+GG0nnBPmFawRqdgud08cBjBdPByQ==",
"dependencies": {
"Microsoft.CSharp": "4.7.0"
}
},
"Microsoft.Extensions.Logging": {
"type": "Direct",
"requested": "[6.0.0, )",

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.MySqlMigrations.Migrations;
public partial class CipherKeyUpdate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Key",
table: "Cipher",
type: "longtext",
nullable: true)
.Annotation("MySql:CharSet", "utf8mb4");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Key",
table: "Cipher");
}
}

View File

@ -1509,6 +1509,9 @@ namespace Bit.MySqlMigrations.Migrations
b.Property<string>("Folders")
.HasColumnType("longtext");
b.Property<string>("Key")
.HasColumnType("longtext");
b.Property<Guid?>("OrganizationId")
.HasColumnType("char(36)");

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.PostgresMigrations.Migrations;
public partial class CipherKeyUpdate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Key",
table: "Cipher",
type: "text",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Key",
table: "Cipher");
}
}

View File

@ -1520,6 +1520,9 @@ namespace Bit.PostgresMigrations.Migrations
b.Property<string>("Folders")
.HasColumnType("text");
b.Property<string>("Key")
.HasColumnType("text");
b.Property<Guid?>("OrganizationId")
.HasColumnType("uuid");

View File

@ -190,7 +190,12 @@ public class Program
var vaultConnectionString = Helpers.GetValueFromEnvFile("global",
"globalSettings__sqlServer__connectionString");
var migrator = new DbMigrator(vaultConnectionString, null);
migrator.MigrateMsSqlDatabaseWithRetries(false);
var log = false;
migrator.MigrateMsSqlDatabaseWithRetries(log);
migrator.MigrateMsSqlDatabaseWithRetries(log, true, MigratorConstants.TransitionMigrationsFolderName);
}
private static bool ValidateInstallation()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.SqliteMigrations.Migrations;
public partial class CipherKeyUpdate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Key",
table: "Cipher",
type: "TEXT",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Key",
table: "Cipher");
}
}

View File

@ -1507,6 +1507,9 @@ namespace Bit.SqliteMigrations.Migrations
b.Property<string>("Folders")
.HasColumnType("TEXT");
b.Property<string>("Key")
.HasColumnType("TEXT");
b.Property<Guid?>("OrganizationId")
.HasColumnType("TEXT");