mirror of
https://github.com/bitwarden/server.git
synced 2025-05-23 12:31:06 -05:00
introduce SendAuthenticationQuery
This commit is contained in:
parent
77865f071a
commit
ffd5629766
@ -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"/> >= <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);
|
||||
}
|
||||
}
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
128
util/Migrator/DbScripts/2025-05-20_00_AddSendEmails.sql
Normal file
128
util/Migrator/DbScripts/2025-05-20_00_AddSendEmails.sql
Normal 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
|
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")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Emails")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("varchar(1024)");
|
||||
|
||||
b.Property<DateTime?>("ExpirationDate")
|
||||
.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")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Emails")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<DateTime?>("ExpirationDate")
|
||||
.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")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Emails")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ExpirationDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user