1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-23 04:21:05 -05:00

introduce SendAuthenticationQuery

This commit is contained in:
✨ Audrey ✨ 2025-05-22 18:12:26 -04:00
parent 77865f071a
commit ffd5629766
No known key found for this signature in database
17 changed files with 9718 additions and 0 deletions

View File

@ -60,9 +60,21 @@ public class Send : ITableObject<Guid>
/// <summary>
/// Password provided by the user. Protected with pbkdf2.
/// </summary>
/// <remarks>
/// This field is mutually exclusive with <see cref="Emails" />
/// </remarks>
[MaxLength(300)]
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>
/// The send becomes unavailable to API callers when
/// <see cref="AccessCount"/> &gt;= <see cref="MaxAccessCount"/>.

View 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;

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -6,6 +6,9 @@
@Data VARCHAR(MAX),
@Key VARCHAR(MAX),
@Password NVARCHAR(300),
-- FIXME: remove null default value once this argument has been
-- in 2 server releases
@Emails NVARCHAR(1024) = NULL,
@MaxAccessCount INT,
@AccessCount INT,
@CreationDate DATETIME2(7),

View File

@ -6,6 +6,7 @@
@Data VARCHAR(MAX),
@Key VARCHAR(MAX),
@Password NVARCHAR(300),
@Emails NVARCHAR(1024) = NULL,
@MaxAccessCount INT,
@AccessCount INT,
@CreationDate DATETIME2(7),
@ -28,6 +29,7 @@ BEGIN
[Data] = @Data,
[Key] = @Key,
[Password] = @Password,
[Emails] = @Emails,
[MaxAccessCount] = @MaxAccessCount,
[AccessCount] = @AccessCount,
[CreationDate] = @CreationDate,

View File

@ -6,6 +6,7 @@
[Data] VARCHAR(MAX) NOT NULL,
[Key] VARCHAR (MAX) NOT NULL,
[Password] NVARCHAR (300) NULL,
[Emails] NVARCHAR (1024) NULL,
[MaxAccessCount] INT NULL,
[AccessCount] INT NOT NULL,
[CreationDate] DATETIME2 (7) NOT NULL,

View File

@ -0,0 +1,128 @@
-- 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 `*`.
ALTER TABLE [dbo].[Organization] ADD [Emails] NVARCHAR(1024) NULL;
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),
@Emails NVARCHAR(1024),
@MaxAccessCount INT,
@AccessCount INT,
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@ExpirationDate DATETIME2(7),
@DeletionDate DATETIME2(7),
@Disabled BIT,
@HideEmail BIT,
@CipherId UNIQUEIDENTIFIER = NULL
AS
BEGIN
SET NOCOUNT ON
UPDATE
[dbo].[Send]
SET
[UserId] = @UserId,
[OrganizationId] = @OrganizationId,
[Type] = @Type,
[Data] = @Data,
[Key] = @Key,
[Password] = @Password,
[Emails] = @Emails,
[MaxAccessCount] = @MaxAccessCount,
[AccessCount] = @AccessCount,
[CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate,
[ExpirationDate] = @ExpirationDate,
[DeletionDate] = @DeletionDate,
[Disabled] = @Disabled,
[HideEmail] = @HideEmail,
[CipherId] = @CipherId
WHERE
[Id] = @Id
IF @UserId IS NOT NULL
BEGIN
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
END
-- TODO: OrganizationId bump?
END
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),
@Emails NVARCHAR(1024),
@MaxAccessCount INT,
@AccessCount INT,
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@ExpirationDate DATETIME2(7),
@DeletionDate DATETIME2(7),
@Disabled BIT,
@HideEmail BIT,
@CipherId UNIQUEIDENTIFIER = 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]
)
VALUES
(
@Id,
@UserId,
@OrganizationId,
@Type,
@Data,
@Key,
@Password,
@MaxAccessCount,
@AccessCount,
@CreationDate,
@RevisionDate,
@ExpirationDate,
@DeletionDate,
@Disabled,
@HideEmail,
@CipherId
)
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

File diff suppressed because it is too large Load Diff

View File

@ -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");
}
}

View File

@ -1437,6 +1437,10 @@ namespace Bit.MySqlMigrations.Migrations
b.Property<bool>("Disabled")
.HasColumnType("tinyint(1)");
b.Property<string>("Emails")
.HasMaxLength(1024)
.HasColumnType("varchar(1024)");
b.Property<DateTime?>("ExpirationDate")
.HasColumnType("datetime(6)");

File diff suppressed because it is too large Load Diff

View File

@ -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");
}
}

View File

@ -1442,6 +1442,10 @@ namespace Bit.PostgresMigrations.Migrations
b.Property<bool>("Disabled")
.HasColumnType("boolean");
b.Property<string>("Emails")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
b.Property<DateTime?>("ExpirationDate")
.HasColumnType("timestamp with time zone");

File diff suppressed because it is too large Load Diff

View File

@ -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");
}
}

View File

@ -1426,6 +1426,10 @@ namespace Bit.SqliteMigrations.Migrations
b.Property<bool>("Disabled")
.HasColumnType("INTEGER");
b.Property<string>("Emails")
.HasMaxLength(1024)
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpirationDate")
.HasColumnType("TEXT");