mirror of
https://github.com/bitwarden/server.git
synced 2025-05-29 23:34:53 -05:00
[PM-21917] Introduce SendAuthenticationQuery
(#5857)
This commit is contained in:
parent
542941818a
commit
c989abdb82
@ -60,9 +60,21 @@ public class Send : ITableObject<Guid>
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Password provided by the user. Protected with pbkdf2.
|
/// Password provided by the user. Protected with pbkdf2.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This field is mutually exclusive with <see cref="Emails" />
|
||||||
|
/// </remarks>
|
||||||
[MaxLength(300)]
|
[MaxLength(300)]
|
||||||
public string? Password { get; set; }
|
public string? Password { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Comma-separated list of emails for OTP authentication.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This field is mutually exclusive with <see cref="Password" />
|
||||||
|
/// </remarks>
|
||||||
|
[MaxLength(1024)]
|
||||||
|
public string? Emails { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The send becomes unavailable to API callers when
|
/// The send becomes unavailable to API callers when
|
||||||
/// <see cref="AccessCount"/> >= <see cref="MaxAccessCount"/>.
|
/// <see cref="AccessCount"/> >= <see cref="MaxAccessCount"/>.
|
||||||
|
50
src/Core/Tools/Models/Data/SendAuthenticationTypes.cs
Normal file
50
src/Core/Tools/Models/Data/SendAuthenticationTypes.cs
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Bit.Core.Tools.Models.Data;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A discriminated union for send authentication.
|
||||||
|
/// </summary>
|
||||||
|
/// <example>
|
||||||
|
/// const method : SendAuthenticationMethod;
|
||||||
|
/// // other variable definitions omitted
|
||||||
|
///
|
||||||
|
/// var token = method switch
|
||||||
|
/// {
|
||||||
|
/// NotAuthenticated => issueTokenFor(sendId),
|
||||||
|
/// ResourcePassword(var expected) => tryIssueTokenFor(sendId, expected, actual),
|
||||||
|
/// EmailOtp(_) => tryIssueTokenFor(sendId, email, actualOtp),
|
||||||
|
/// _ => throw new Exception()
|
||||||
|
/// };
|
||||||
|
/// </example>
|
||||||
|
public abstract record SendAuthenticationMethod;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Never issue a send claim.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>
|
||||||
|
/// This claim is issued when a send does not exist or when a send
|
||||||
|
/// has exceeded its max access attempts.
|
||||||
|
/// </remarks>
|
||||||
|
public record NeverAuthenticate : SendAuthenticationMethod;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a send claim automatically.
|
||||||
|
/// </summary>
|
||||||
|
public record NotAuthenticated : SendAuthenticationMethod;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a send claim by requesting a password confirmation hash.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Hash">
|
||||||
|
/// A base64 encoded hash that permits access to the send.
|
||||||
|
/// </param>
|
||||||
|
public record ResourcePassword(string Hash) : SendAuthenticationMethod;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a send claim by requesting a one time password (OTP) confirmation code.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="Emails">
|
||||||
|
/// The list of email addresses permitted access to the send.
|
||||||
|
/// </param>
|
||||||
|
public record EmailOtp(string[] Emails) : SendAuthenticationMethod;
|
@ -0,0 +1,20 @@
|
|||||||
|
using Bit.Core.Tools.Models.Data;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Integration with authentication layer for generating send access claims.
|
||||||
|
/// </summary>
|
||||||
|
public interface ISendAuthenticationQuery
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the authentication method of a Send.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sendId">Identifies the send to inspect.</param>
|
||||||
|
/// <returns>
|
||||||
|
/// The authentication method that should be performed for the send.
|
||||||
|
/// </returns>
|
||||||
|
Task<SendAuthenticationMethod> GetAuthenticationMethod(Guid sendId);
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
using Bit.Core.Tools.Models.Data;
|
||||||
|
using Bit.Core.Tools.Repositories;
|
||||||
|
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Bit.Core.Tools.SendFeatures.Queries;
|
||||||
|
|
||||||
|
/// <inheritdoc cref="ISendAuthenticationQuery"/>
|
||||||
|
public class SendAuthenticationQuery : ISendAuthenticationQuery
|
||||||
|
{
|
||||||
|
private static readonly NotAuthenticated NOT_AUTHENTICATED = new NotAuthenticated();
|
||||||
|
private static readonly NeverAuthenticate NEVER_AUTHENTICATE = new NeverAuthenticate();
|
||||||
|
|
||||||
|
private readonly ISendRepository _sendRepository;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Instantiates the command
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="sendRepository">
|
||||||
|
/// Retrieves send records
|
||||||
|
/// </param>
|
||||||
|
/// <exception cref="ArgumentNullException">
|
||||||
|
/// Thrown when <paramref name="sendRepository"/> is <see langword="null"/>.
|
||||||
|
/// </exception>
|
||||||
|
public SendAuthenticationQuery(ISendRepository sendRepository)
|
||||||
|
{
|
||||||
|
_sendRepository = sendRepository ?? throw new ArgumentNullException(nameof(sendRepository));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc cref="ISendAuthenticationQuery.GetAuthenticationMethod"/>
|
||||||
|
public async Task<SendAuthenticationMethod> GetAuthenticationMethod(Guid sendId)
|
||||||
|
{
|
||||||
|
var send = await _sendRepository.GetByIdAsync(sendId);
|
||||||
|
|
||||||
|
SendAuthenticationMethod method = send switch
|
||||||
|
{
|
||||||
|
null => NEVER_AUTHENTICATE,
|
||||||
|
var s when s.AccessCount >= s.MaxAccessCount => NEVER_AUTHENTICATE,
|
||||||
|
var s when s.Emails is not null => emailOtp(s.Emails),
|
||||||
|
var s when s.Password is not null => new ResourcePassword(s.Password),
|
||||||
|
_ => NOT_AUTHENTICATED
|
||||||
|
};
|
||||||
|
|
||||||
|
return method;
|
||||||
|
}
|
||||||
|
|
||||||
|
private EmailOtp emailOtp(string emails)
|
||||||
|
{
|
||||||
|
var list = emails.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
|
||||||
|
return new EmailOtp(list);
|
||||||
|
}
|
||||||
|
}
|
@ -1,5 +1,7 @@
|
|||||||
using Bit.Core.Tools.SendFeatures.Commands;
|
using Bit.Core.Tools.SendFeatures.Commands;
|
||||||
using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
|
using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
|
||||||
|
using Bit.Core.Tools.SendFeatures.Queries;
|
||||||
|
using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
||||||
using Bit.Core.Tools.Services;
|
using Bit.Core.Tools.Services;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
@ -14,5 +16,6 @@ public static class SendServiceCollectionExtension
|
|||||||
services.AddScoped<ISendAuthorizationService, SendAuthorizationService>();
|
services.AddScoped<ISendAuthorizationService, SendAuthorizationService>();
|
||||||
services.AddScoped<ISendValidationService, SendValidationService>();
|
services.AddScoped<ISendValidationService, SendValidationService>();
|
||||||
services.AddScoped<ISendCoreHelperService, SendCoreHelperService>();
|
services.AddScoped<ISendCoreHelperService, SendCoreHelperService>();
|
||||||
|
services.AddScoped<ISendAuthenticationQuery, SendAuthenticationQuery>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,10 @@
|
|||||||
@DeletionDate DATETIME2(7),
|
@DeletionDate DATETIME2(7),
|
||||||
@Disabled BIT,
|
@Disabled BIT,
|
||||||
@HideEmail BIT,
|
@HideEmail BIT,
|
||||||
@CipherId UNIQUEIDENTIFIER = NULL
|
@CipherId UNIQUEIDENTIFIER = NULL,
|
||||||
|
-- FIXME: remove null default value once this argument has been
|
||||||
|
-- in 2 server releases
|
||||||
|
@Emails NVARCHAR(1024) = NULL
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@ -36,7 +39,8 @@ BEGIN
|
|||||||
[DeletionDate],
|
[DeletionDate],
|
||||||
[Disabled],
|
[Disabled],
|
||||||
[HideEmail],
|
[HideEmail],
|
||||||
[CipherId]
|
[CipherId],
|
||||||
|
[Emails]
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(
|
(
|
||||||
@ -55,7 +59,8 @@ BEGIN
|
|||||||
@DeletionDate,
|
@DeletionDate,
|
||||||
@Disabled,
|
@Disabled,
|
||||||
@HideEmail,
|
@HideEmail,
|
||||||
@CipherId
|
@CipherId,
|
||||||
|
@Emails
|
||||||
)
|
)
|
||||||
|
|
||||||
IF @UserId IS NOT NULL
|
IF @UserId IS NOT NULL
|
||||||
|
@ -14,7 +14,8 @@
|
|||||||
@DeletionDate DATETIME2(7),
|
@DeletionDate DATETIME2(7),
|
||||||
@Disabled BIT,
|
@Disabled BIT,
|
||||||
@HideEmail BIT,
|
@HideEmail BIT,
|
||||||
@CipherId UNIQUEIDENTIFIER = NULL
|
@CipherId UNIQUEIDENTIFIER = NULL,
|
||||||
|
@Emails NVARCHAR(1024) = NULL
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@ -36,7 +37,8 @@ BEGIN
|
|||||||
[DeletionDate] = @DeletionDate,
|
[DeletionDate] = @DeletionDate,
|
||||||
[Disabled] = @Disabled,
|
[Disabled] = @Disabled,
|
||||||
[HideEmail] = @HideEmail,
|
[HideEmail] = @HideEmail,
|
||||||
[CipherId] = @CipherId
|
[CipherId] = @CipherId,
|
||||||
|
[Emails] = @Emails
|
||||||
WHERE
|
WHERE
|
||||||
[Id] = @Id
|
[Id] = @Id
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
[Data] VARCHAR(MAX) NOT NULL,
|
[Data] VARCHAR(MAX) NOT NULL,
|
||||||
[Key] VARCHAR (MAX) NOT NULL,
|
[Key] VARCHAR (MAX) NOT NULL,
|
||||||
[Password] NVARCHAR (300) NULL,
|
[Password] NVARCHAR (300) NULL,
|
||||||
|
[Emails] NVARCHAR (1024) NULL,
|
||||||
[MaxAccessCount] INT NULL,
|
[MaxAccessCount] INT NULL,
|
||||||
[AccessCount] INT NOT NULL,
|
[AccessCount] INT NOT NULL,
|
||||||
[CreationDate] DATETIME2 (7) NOT NULL,
|
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||||
|
135
test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs
Normal file
135
test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs
Normal file
@ -0,0 +1,135 @@
|
|||||||
|
using Bit.Core.Tools.Entities;
|
||||||
|
using Bit.Core.Tools.Models.Data;
|
||||||
|
using Bit.Core.Tools.Repositories;
|
||||||
|
using Bit.Core.Tools.SendFeatures.Queries;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.Tools.Services;
|
||||||
|
|
||||||
|
public class SendAuthenticationQueryTests
|
||||||
|
{
|
||||||
|
private readonly ISendRepository _sendRepository;
|
||||||
|
private readonly SendAuthenticationQuery _sendAuthenticationQuery;
|
||||||
|
|
||||||
|
public SendAuthenticationQueryTests()
|
||||||
|
{
|
||||||
|
_sendRepository = Substitute.For<ISendRepository>();
|
||||||
|
_sendAuthenticationQuery = new SendAuthenticationQuery(_sendRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Constructor_WithNullRepository_ThrowsArgumentNullException()
|
||||||
|
{
|
||||||
|
// Act & Assert
|
||||||
|
var exception = Assert.Throws<ArgumentNullException>(() => new SendAuthenticationQuery(null));
|
||||||
|
Assert.Equal("sendRepository", exception.ParamName);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(AuthenticationMethodTestCases))]
|
||||||
|
public async Task GetAuthenticationMethod_ReturnsExpectedAuthenticationMethod(Send? send, Type expectedType)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sendId = Guid.NewGuid();
|
||||||
|
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType(expectedType, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(EmailParsingTestCases))]
|
||||||
|
public async Task GetAuthenticationMethod_WithEmails_ParsesEmailsCorrectly(string emailString, string[] expectedEmails)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sendId = Guid.NewGuid();
|
||||||
|
var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: emailString, password: null);
|
||||||
|
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var emailOtp = Assert.IsType<EmailOtp>(result);
|
||||||
|
Assert.Equal(expectedEmails, emailOtp.Emails);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetAuthenticationMethod_WithBothEmailsAndPassword_ReturnsEmailOtp()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sendId = Guid.NewGuid();
|
||||||
|
var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: "test@example.com", password: "hashedpassword");
|
||||||
|
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.IsType<EmailOtp>(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetAuthenticationMethod_CallsRepositoryWithCorrectSendId()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sendId = Guid.NewGuid();
|
||||||
|
var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: null);
|
||||||
|
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
await _sendRepository.Received(1).GetByIdAsync(sendId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetAuthenticationMethod_WhenRepositoryThrows_PropagatesException()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var sendId = Guid.NewGuid();
|
||||||
|
var expectedException = new InvalidOperationException("Repository error");
|
||||||
|
_sendRepository.GetByIdAsync(sendId).Returns(Task.FromException<Send?>(expectedException));
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||||
|
_sendAuthenticationQuery.GetAuthenticationMethod(sendId));
|
||||||
|
Assert.Same(expectedException, exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<object[]> AuthenticationMethodTestCases()
|
||||||
|
{
|
||||||
|
yield return new object[] { null, typeof(NeverAuthenticate) };
|
||||||
|
yield return new object[] { CreateSend(accessCount: 5, maxAccessCount: 5, emails: null, password: null), typeof(NeverAuthenticate) };
|
||||||
|
yield return new object[] { CreateSend(accessCount: 6, maxAccessCount: 5, emails: null, password: null), typeof(NeverAuthenticate) };
|
||||||
|
yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: "test@example.com", password: null), typeof(EmailOtp) };
|
||||||
|
yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: "hashedpassword"), typeof(ResourcePassword) };
|
||||||
|
yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: null), typeof(NotAuthenticated) };
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<object[]> EmailParsingTestCases()
|
||||||
|
{
|
||||||
|
yield return new object[] { "test@example.com", new[] { "test@example.com" } };
|
||||||
|
yield return new object[] { "test1@example.com,test2@example.com", new[] { "test1@example.com", "test2@example.com" } };
|
||||||
|
yield return new object[] { " test@example.com , other@example.com ", new[] { "test@example.com", "other@example.com" } };
|
||||||
|
yield return new object[] { "test@example.com,,other@example.com", new[] { "test@example.com", "other@example.com" } };
|
||||||
|
yield return new object[] { " , test@example.com, ,other@example.com, ", new[] { "test@example.com", "other@example.com" } };
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Send CreateSend(int accessCount, int? maxAccessCount, string? emails, string? password)
|
||||||
|
{
|
||||||
|
return new Send
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
AccessCount = accessCount,
|
||||||
|
MaxAccessCount = maxAccessCount,
|
||||||
|
Emails = emails,
|
||||||
|
Password = password
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
139
util/Migrator/DbScripts/2025-05-20_00_AddSendEmails.sql
Normal file
139
util/Migrator/DbScripts/2025-05-20_00_AddSendEmails.sql
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
-- Add `Emails` field that stores a comma-separated list of email addresses for
|
||||||
|
-- email/OTP authentication to table and write methods. The read methods
|
||||||
|
-- don't need to be updated because they all use `*`.
|
||||||
|
IF NOT EXISTS(
|
||||||
|
SELECT *
|
||||||
|
FROM [sys].[columns]
|
||||||
|
WHERE [object_id] = OBJECT_ID(N'[dbo].[Send]')
|
||||||
|
AND [name] = 'Emails')
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE [dbo].[Send] ADD [Emails] NVARCHAR(1024) NULL;
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Send_Update]
|
||||||
|
@Id UNIQUEIDENTIFIER,
|
||||||
|
@UserId UNIQUEIDENTIFIER,
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER,
|
||||||
|
@Type TINYINT,
|
||||||
|
@Data VARCHAR(MAX),
|
||||||
|
@Key VARCHAR(MAX),
|
||||||
|
@Password NVARCHAR(300),
|
||||||
|
@MaxAccessCount INT,
|
||||||
|
@AccessCount INT,
|
||||||
|
@CreationDate DATETIME2(7),
|
||||||
|
@RevisionDate DATETIME2(7),
|
||||||
|
@ExpirationDate DATETIME2(7),
|
||||||
|
@DeletionDate DATETIME2(7),
|
||||||
|
@Disabled BIT,
|
||||||
|
@HideEmail BIT,
|
||||||
|
@CipherId UNIQUEIDENTIFIER = NULL,
|
||||||
|
@Emails NVARCHAR(1024) = NULL
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
UPDATE
|
||||||
|
[dbo].[Send]
|
||||||
|
SET
|
||||||
|
[UserId] = @UserId,
|
||||||
|
[OrganizationId] = @OrganizationId,
|
||||||
|
[Type] = @Type,
|
||||||
|
[Data] = @Data,
|
||||||
|
[Key] = @Key,
|
||||||
|
[Password] = @Password,
|
||||||
|
[MaxAccessCount] = @MaxAccessCount,
|
||||||
|
[AccessCount] = @AccessCount,
|
||||||
|
[CreationDate] = @CreationDate,
|
||||||
|
[RevisionDate] = @RevisionDate,
|
||||||
|
[ExpirationDate] = @ExpirationDate,
|
||||||
|
[DeletionDate] = @DeletionDate,
|
||||||
|
[Disabled] = @Disabled,
|
||||||
|
[HideEmail] = @HideEmail,
|
||||||
|
[CipherId] = @CipherId,
|
||||||
|
[Emails] = @Emails
|
||||||
|
WHERE
|
||||||
|
[Id] = @Id
|
||||||
|
|
||||||
|
IF @UserId IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
|
||||||
|
END
|
||||||
|
-- TODO: OrganizationId bump?
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Send_Create]
|
||||||
|
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||||
|
@UserId UNIQUEIDENTIFIER,
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER,
|
||||||
|
@Type TINYINT,
|
||||||
|
@Data VARCHAR(MAX),
|
||||||
|
@Key VARCHAR(MAX),
|
||||||
|
@Password NVARCHAR(300),
|
||||||
|
@MaxAccessCount INT,
|
||||||
|
@AccessCount INT,
|
||||||
|
@CreationDate DATETIME2(7),
|
||||||
|
@RevisionDate DATETIME2(7),
|
||||||
|
@ExpirationDate DATETIME2(7),
|
||||||
|
@DeletionDate DATETIME2(7),
|
||||||
|
@Disabled BIT,
|
||||||
|
@HideEmail BIT,
|
||||||
|
@CipherId UNIQUEIDENTIFIER = NULL,
|
||||||
|
@Emails NVARCHAR(1024) = NULL
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
INSERT INTO [dbo].[Send]
|
||||||
|
(
|
||||||
|
[Id],
|
||||||
|
[UserId],
|
||||||
|
[OrganizationId],
|
||||||
|
[Type],
|
||||||
|
[Data],
|
||||||
|
[Key],
|
||||||
|
[Password],
|
||||||
|
[MaxAccessCount],
|
||||||
|
[AccessCount],
|
||||||
|
[CreationDate],
|
||||||
|
[RevisionDate],
|
||||||
|
[ExpirationDate],
|
||||||
|
[DeletionDate],
|
||||||
|
[Disabled],
|
||||||
|
[HideEmail],
|
||||||
|
[CipherId],
|
||||||
|
[Emails]
|
||||||
|
)
|
||||||
|
VALUES
|
||||||
|
(
|
||||||
|
@Id,
|
||||||
|
@UserId,
|
||||||
|
@OrganizationId,
|
||||||
|
@Type,
|
||||||
|
@Data,
|
||||||
|
@Key,
|
||||||
|
@Password,
|
||||||
|
@MaxAccessCount,
|
||||||
|
@AccessCount,
|
||||||
|
@CreationDate,
|
||||||
|
@RevisionDate,
|
||||||
|
@ExpirationDate,
|
||||||
|
@DeletionDate,
|
||||||
|
@Disabled,
|
||||||
|
@HideEmail,
|
||||||
|
@CipherId,
|
||||||
|
@Emails
|
||||||
|
)
|
||||||
|
|
||||||
|
IF @UserId IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
IF @Type = 1 --File
|
||||||
|
BEGIN
|
||||||
|
EXEC [dbo].[User_UpdateStorage] @UserId
|
||||||
|
END
|
||||||
|
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
|
||||||
|
END
|
||||||
|
-- TODO: OrganizationId bump?
|
||||||
|
END
|
||||||
|
GO
|
3119
util/MySqlMigrations/Migrations/20250522205018_2025-05-20_00_AddSendEmails.Designer.cs
generated
Normal file
3119
util/MySqlMigrations/Migrations/20250522205018_2025-05-20_00_AddSendEmails.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,29 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.MySqlMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class _20250520_00_AddSendEmails : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Emails",
|
||||||
|
table: "Send",
|
||||||
|
type: "varchar(1024)",
|
||||||
|
maxLength: 1024,
|
||||||
|
nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Emails",
|
||||||
|
table: "Send");
|
||||||
|
}
|
||||||
|
}
|
@ -1437,6 +1437,10 @@ namespace Bit.MySqlMigrations.Migrations
|
|||||||
b.Property<bool>("Disabled")
|
b.Property<bool>("Disabled")
|
||||||
.HasColumnType("tinyint(1)");
|
.HasColumnType("tinyint(1)");
|
||||||
|
|
||||||
|
b.Property<string>("Emails")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("varchar(1024)");
|
||||||
|
|
||||||
b.Property<DateTime?>("ExpirationDate")
|
b.Property<DateTime?>("ExpirationDate")
|
||||||
.HasColumnType("datetime(6)");
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
3125
util/PostgresMigrations/Migrations/20250520201209_2025-05-20_00_AddSendEmails.Designer.cs
generated
Normal file
3125
util/PostgresMigrations/Migrations/20250520201209_2025-05-20_00_AddSendEmails.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.PostgresMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class _20250520_00_AddSendEmails : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Emails",
|
||||||
|
table: "Send",
|
||||||
|
type: "character varying(1024)",
|
||||||
|
maxLength: 1024,
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Emails",
|
||||||
|
table: "Send");
|
||||||
|
}
|
||||||
|
}
|
@ -1442,6 +1442,10 @@ namespace Bit.PostgresMigrations.Migrations
|
|||||||
b.Property<bool>("Disabled")
|
b.Property<bool>("Disabled")
|
||||||
.HasColumnType("boolean");
|
.HasColumnType("boolean");
|
||||||
|
|
||||||
|
b.Property<string>("Emails")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("character varying(1024)");
|
||||||
|
|
||||||
b.Property<DateTime?>("ExpirationDate")
|
b.Property<DateTime?>("ExpirationDate")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
3108
util/SqliteMigrations/Migrations/20250520201216_2025-05-20_00_AddSendEmails.Designer.cs
generated
Normal file
3108
util/SqliteMigrations/Migrations/20250520201216_2025-05-20_00_AddSendEmails.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,28 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.SqliteMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class _20250520_00_AddSendEmails : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.AddColumn<string>(
|
||||||
|
name: "Emails",
|
||||||
|
table: "Send",
|
||||||
|
type: "TEXT",
|
||||||
|
maxLength: 1024,
|
||||||
|
nullable: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Emails",
|
||||||
|
table: "Send");
|
||||||
|
}
|
||||||
|
}
|
@ -1426,6 +1426,10 @@ namespace Bit.SqliteMigrations.Migrations
|
|||||||
b.Property<bool>("Disabled")
|
b.Property<bool>("Disabled")
|
||||||
.HasColumnType("INTEGER");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<string>("Emails")
|
||||||
|
.HasMaxLength(1024)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<DateTime?>("ExpirationDate")
|
b.Property<DateTime?>("ExpirationDate")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user