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>
|
/// <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);
|
||||||
|
}
|
||||||
|
}
|
@ -6,6 +6,9 @@
|
|||||||
@Data VARCHAR(MAX),
|
@Data VARCHAR(MAX),
|
||||||
@Key VARCHAR(MAX),
|
@Key VARCHAR(MAX),
|
||||||
@Password NVARCHAR(300),
|
@Password NVARCHAR(300),
|
||||||
|
-- FIXME: remove null default value once this argument has been
|
||||||
|
-- in 2 server releases
|
||||||
|
@Emails NVARCHAR(1024) = NULL,
|
||||||
@MaxAccessCount INT,
|
@MaxAccessCount INT,
|
||||||
@AccessCount INT,
|
@AccessCount INT,
|
||||||
@CreationDate DATETIME2(7),
|
@CreationDate DATETIME2(7),
|
||||||
|
@ -6,6 +6,7 @@
|
|||||||
@Data VARCHAR(MAX),
|
@Data VARCHAR(MAX),
|
||||||
@Key VARCHAR(MAX),
|
@Key VARCHAR(MAX),
|
||||||
@Password NVARCHAR(300),
|
@Password NVARCHAR(300),
|
||||||
|
@Emails NVARCHAR(1024) = NULL,
|
||||||
@MaxAccessCount INT,
|
@MaxAccessCount INT,
|
||||||
@AccessCount INT,
|
@AccessCount INT,
|
||||||
@CreationDate DATETIME2(7),
|
@CreationDate DATETIME2(7),
|
||||||
@ -28,6 +29,7 @@ BEGIN
|
|||||||
[Data] = @Data,
|
[Data] = @Data,
|
||||||
[Key] = @Key,
|
[Key] = @Key,
|
||||||
[Password] = @Password,
|
[Password] = @Password,
|
||||||
|
[Emails] = @Emails,
|
||||||
[MaxAccessCount] = @MaxAccessCount,
|
[MaxAccessCount] = @MaxAccessCount,
|
||||||
[AccessCount] = @AccessCount,
|
[AccessCount] = @AccessCount,
|
||||||
[CreationDate] = @CreationDate,
|
[CreationDate] = @CreationDate,
|
||||||
|
@ -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,
|
||||||
|
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")
|
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