Skip to content
9 changes: 3 additions & 6 deletions src/Api/Tools/Controllers/ImportCiphersController.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable

using Bit.Api.Tools.Models.Request.Accounts;
using Bit.Api.Tools.Models.Request.Accounts;
using Bit.Api.Tools.Models.Request.Organizations;
using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Core.Context;
Expand Down Expand Up @@ -56,7 +53,7 @@ public async Task PostImport([FromBody] ImportCiphersRequestModel model)
throw new BadRequestException("You cannot import this much data at once.");
}

var userId = _userService.GetProperUserId(User).Value;
var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException("User ID not found");
var folders = model.Folders.Select(f => f.ToFolder(userId)).ToList();
var ciphers = model.Ciphers.Select(c => c.ToCipherDetails(userId, false)).ToList();
await _importCiphersCommand.ImportIntoIndividualVaultAsync(folders, ciphers, model.FolderRelationships, userId);
Expand Down Expand Up @@ -84,7 +81,7 @@ public async Task PostImportOrganization([FromQuery] string organizationId,
throw new BadRequestException("Not enough privileges to import into this organization.");
}

var userId = _userService.GetProperUserId(User).Value;
var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException("User ID not found");
var ciphers = model.Ciphers.Select(l => l.ToOrganizationCipherDetails(orgId)).ToList();
await _importCiphersCommand.ImportIntoOrganizationalVaultAsync(collections, ciphers, model.CollectionRelationships, userId);
}
Expand Down
4 changes: 3 additions & 1 deletion src/Api/Tools/Controllers/SendsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,8 @@ public async Task<SendFileUploadDataResponseModel> PostFile([FromBody] SendReque
throw new BadRequestException($"Max file size is {SendFileSettingHelper.MAX_FILE_SIZE_READABLE}.");
}

var file = model.File ?? throw new BadRequestException("File metadata is required for file sends.");

model.ValidateCreation();
var userId = _userService.GetProperUserId(User) ?? throw new InvalidOperationException("User ID not found");
var hasPremium = await _hasPremiumAccessQuery.HasPremiumAccessAsync(userId);
Expand All @@ -351,7 +353,7 @@ public async Task<SendFileUploadDataResponseModel> PostFile([FromBody] SendReque
throw new BadRequestException("Email verified Sends require a premium membership");
}

var (send, data) = model.ToSend(userId, model.File.FileName, _sendAuthorizationService);
var (send, data) = model.ToSend(userId, file.FileName!, _sendAuthorizationService);
var uploadUrl = await _nonAnonymousSendCommand.SaveFileSendAsync(send, data, model.FileLength.Value);
return new SendFileUploadDataResponseModel
{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable

using Bit.Api.Vault.Models.Request;
using Bit.Api.Vault.Models.Request;

namespace Bit.Api.Tools.Models.Request.Accounts;

public class ImportCiphersRequestModel
{
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public FolderWithIdRequestModel[] Folders { get; set; }
public CipherRequestModel[] Ciphers { get; set; }
public KeyValuePair<int, int>[] FolderRelationships { get; set; }
public FolderWithIdRequestModel[] Folders { get; set; } = [];
public CipherRequestModel[] Ciphers { get; set; } = [];
public KeyValuePair<int, int>[] FolderRelationships { get; set; } = [];
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable

using Bit.Api.Models.Request;
using Bit.Api.Models.Request;
using Bit.Api.Vault.Models.Request;

namespace Bit.Api.Tools.Models.Request.Organizations;

public class ImportOrganizationCiphersRequestModel
{
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public CollectionWithIdRequestModel[] Collections { get; set; }
public CipherRequestModel[] Ciphers { get; set; }
public KeyValuePair<int, int>[] CollectionRelationships { get; set; }
public CollectionWithIdRequestModel[] Collections { get; set; } = [];
public CipherRequestModel[] Ciphers { get; set; } = [];
public KeyValuePair<int, int>[] CollectionRelationships { get; set; } = [];
}
7 changes: 2 additions & 5 deletions src/Api/Tools/Models/Request/SendAccessRequestModel.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;

namespace Bit.Api.Tools.Models.Request;

public class SendAccessRequestModel
{
[StringLength(300)]
public string Password { get; set; }
public string? Password { get; set; }
}
72 changes: 35 additions & 37 deletions src/Api/Tools/Models/Request/SendRequestModel.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using Bit.Api.Tools.Utilities;
using Bit.Core.Exceptions;
Expand All @@ -10,7 +7,6 @@
using Bit.Core.Tools.Models.Data;
using Bit.Core.Tools.Services;
using Bit.Core.Utilities;

using static System.StringSplitOptions;

namespace Bit.Api.Tools.Models.Request;
Expand Down Expand Up @@ -41,14 +37,14 @@ public class SendRequestModel
/// </summary>
[EncryptedString]
[EncryptedStringLength(1000)]
public string Name { get; set; }
public string? Name { get; set; }

/// <summary>
/// Notes for the send. This is only visible to the owner of the send.
/// </summary>
[EncryptedString]
[EncryptedStringLength(1000)]
public string Notes { get; set; }
public string? Notes { get; set; }

/// <summary>
/// A base64-encoded byte array containing the Send's encryption key. This key is
Expand All @@ -57,7 +53,7 @@ public class SendRequestModel
[Required]
[EncryptedString]
[EncryptedStringLength(1000)]
public string Key { get; set; }
public required string Key { get; set; }

/// <summary>
/// The maximum number of times a send can be accessed before it expires.
Expand All @@ -74,8 +70,8 @@ public class SendRequestModel

/// <summary>
/// The date after which a send may be automatically deleted from the server.
/// When this is <see langword="null" />, the send may be deleted after it has
/// exceeded the global send timeout limit.
/// The server enforces a maximum of 31 days from creation. A background job
/// deletes sends once this date has passed.
/// </summary>
[Required]
public DateTime? DeletionDate { get; set; }
Expand All @@ -84,26 +80,26 @@ public class SendRequestModel
/// Contains file metadata uploaded with the send.
/// The file content is uploaded separately.
/// </summary>
public SendFileModel File { get; set; }
public SendFileModel? File { get; set; }

/// <summary>
/// Contains text data uploaded with the send.
/// </summary>
public SendTextModel Text { get; set; }
public SendTextModel? Text { get; set; }

/// <summary>
/// Base64-encoded byte array of a password hash that grants access to the send.
/// Mutually exclusive with <see cref="Emails"/>.
/// </summary>
[StringLength(1000)]
public string Password { get; set; }
public string? Password { get; set; }

/// <summary>
/// Comma-separated list of emails that may access the send using OTP
/// authentication. Mutually exclusive with <see cref="Password"/>.
/// </summary>
[StringLength(4000)]
public string Emails { get; set; }
public string? Emails { get; set; }

/// <summary>
/// When <see langword="true"/>, send access is disabled.
Expand All @@ -126,11 +122,7 @@ public class SendRequestModel
/// <returns>The send object</returns>
public Send ToSend(Guid userId, ISendAuthorizationService sendAuthorizationService)
{
var send = new Send
{
Type = Type,
UserId = (Guid?)userId
};
var send = new Send { Type = Type, UserId = (Guid?)userId };
send = UpdateSend(send, sendAuthorizationService);
return send;
}
Expand All @@ -146,12 +138,8 @@ public Send ToSend(Guid userId, ISendAuthorizationService sendAuthorizationServi
{
// FIXME: This method does two things: creates a send and a send file data.
// It should only do one thing.
var send = ToSendBase(new Send
{
Type = Type,
UserId = (Guid?)userId
}, sendAuthorizationService);
var data = new SendFileData(Name, Notes, fileName);
var send = ToSendBase(new Send { Type = Type, UserId = (Guid?)userId }, sendAuthorizationService);
var data = new SendFileData(Name ?? string.Empty, Notes, fileName);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only the CLI can create or edit Sends having no name, the former behavior serialized and deserialized them as 'null', I've taken this opportunity to instead assign an empty string. Suggestions for a different default are welcome.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think empty string is fine, definitely an improvement over the former behavior. It does seem weird that the CLI is allowed to do that though, was that a deliberate design choice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about this, I'd like to raise this with Product next time we sync. Typically, we strive for parity among clients. I didn't want to increase the scope of this work by making any changes in clients without raising this to product first; I think the CLI appeals to power-admins and wasn't sure if this would lead to any breaking changes in scrips used for batch generating Sends.

return (send, data);
}

Expand All @@ -167,8 +155,10 @@ public Send UpdateSend(Send existingSend, ISendAuthorizationService sendAuthoriz
switch (existingSend.Type)
{
case SendType.File:
var fileData = JsonSerializer.Deserialize<SendFileData>(existingSend.Data);
fileData.Name = Name;
// See Send.cs property definition for Data, it can safely be assumed non-null
var fileData = JsonSerializer.Deserialize<SendFileData>(existingSend.Data!) ??
throw new JsonException("Failed to deserialize send file data.");
fileData.Name = Name ?? string.Empty;
fileData.Notes = Notes;
existingSend.Data = JsonSerializer.Serialize(fileData, JsonHelpers.IgnoreWritingNull);
break;
Expand All @@ -178,6 +168,7 @@ public Send UpdateSend(Send existingSend, ISendAuthorizationService sendAuthoriz
default:
throw new ArgumentException("Unsupported type: " + nameof(Type) + ".");
}

return existingSend;
}

Expand All @@ -195,8 +186,9 @@ public void ValidateCreation()
if (ExpirationDate.HasValue && ExpirationDate.Value <= nowPlus1Minute)
{
throw new BadRequestException("You cannot create a Send that is already expired. " +
"Adjust the expiration date and try again.");
"Adjust the expiration date and try again.");
}

ValidateEdit();
}

Expand All @@ -217,25 +209,29 @@ public void ValidateEdit()
if (DeletionDate.Value <= nowPlus1Minute)
{
throw new BadRequestException("You cannot have a Send with a deletion date in the past. " +
"Adjust the deletion date and try again.");
"Adjust the deletion date and try again.");
}

if (DeletionDate.Value > now.AddDays(31))
{
throw new BadRequestException("You cannot have a Send with a deletion date that far " +
"into the future. Adjust the Deletion Date to a value less than 31 days from now " +
"and try again.");
"into the future. Adjust the Deletion Date to a value less than 31 days from now " +
"and try again.");
}
}

if (ExpirationDate.HasValue)
{
if (ExpirationDate.Value <= nowPlus1Minute)
{
throw new BadRequestException("You cannot have a Send with an expiration date in the past. " +
"Adjust the expiration date and try again.");
"Adjust the expiration date and try again.");
}
if (ExpirationDate.Value > DeletionDate.Value)

if (DeletionDate.HasValue && ExpirationDate.Value > DeletionDate.Value)
{
throw new BadRequestException("You cannot have a Send with an expiration date greater than the deletion date. " +
throw new BadRequestException(
"You cannot have a Send with an expiration date greater than the deletion date. " +
"Adjust the expiration date and try again.");
}
}
Expand All @@ -245,7 +241,7 @@ private Send ToSendBase(Send existingSend, ISendAuthorizationService authorizati
{
existingSend.Key = Key;
existingSend.ExpirationDate = ExpirationDate;
existingSend.DeletionDate = DeletionDate.Value;
existingSend.DeletionDate = DeletionDate!.Value;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is safe since DeletionDate is required during model binding.

existingSend.MaxAccessCount = MaxAccessCount;
existingSend.Disabled = Disabled.GetValueOrDefault();
existingSend.HideEmail = HideEmail.GetValueOrDefault();
Expand All @@ -268,7 +264,7 @@ private Send ToSendBase(Send existingSend, ISendAuthorizationService authorizati
existingSend.Password = null;
break;
case Core.Tools.Enums.AuthType.Password:
existingSend.Password = authorizationService.HashPassword(Password);
existingSend.Password = authorizationService.HashPassword(Password!);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clients all enforce that a password protected Send must have a password

existingSend.Emails = null;
break;
case Core.Tools.Enums.AuthType.None:
Expand Down Expand Up @@ -312,9 +308,11 @@ private Send ToSendBase(Send existingSend, ISendAuthorizationService authorizati
return existingSend;
}

// Only called from the SendType.Text branch of UpdateSend, Text is required by client and not null
private SendTextData ToSendTextData()
{
return new SendTextData(Name, Notes, Text.Text, Text.Hidden);
var text = Text ?? throw new ArgumentNullException(nameof(Text), "Text is required for text sends.");
return new SendTextData(Name ?? string.Empty, Notes, text.Text, text.Hidden);
}
}

Expand Down
23 changes: 14 additions & 9 deletions src/Api/Tools/Models/Response/SendAccessResponseModel.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable

using System.Text.Json;
using System.Text.Json;
using Bit.Core.Models.Api;
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Enums;
Expand Down Expand Up @@ -41,12 +38,20 @@ public SendAccessResponseModel(Send send)
switch (send.Type)
{
case SendType.File:
var fileData = JsonSerializer.Deserialize<SendFileData>(send.Data);
var fileData =
JsonSerializer.Deserialize<SendFileData>(send.Data ??
throw new NullReferenceException(
"Send Data is required")) ??
throw new JsonException("Failed to deserialize send file data.");
sendData = fileData;
File = new SendFileModel(fileData);
break;
case SendType.Text:
var textData = JsonSerializer.Deserialize<SendTextData>(send.Data);
var textData =
JsonSerializer.Deserialize<SendTextData>(send.Data ??
throw new NullReferenceException(
"Send Data is required")) ??
throw new JsonException("Failed to deserialize send text data.");
sendData = textData;
Text = new SendTextModel(textData);
break;
Expand Down Expand Up @@ -89,12 +94,12 @@ public SendAccessResponseModel(Send send)
/// File content is downloaded separately using
/// <see cref="Bit.Api.Tools.Controllers.SendsController.GetSendFileDownloadData" />
/// </remarks>
public SendFileModel File { get; set; }
public SendFileModel? File { get; set; }

/// <summary>
/// Contains text data uploaded with the send.
/// </summary>
public SendTextModel Text { get; set; }
public SendTextModel? Text { get; set; }

/// <summary>
/// The date after which a send cannot be accessed. When this value is
Expand All @@ -105,5 +110,5 @@ public SendAccessResponseModel(Send send)
/// <summary>
/// Indicates the person that created the send to the accessor.
/// </summary>
public string CreatorIdentifier { get; set; }
public string? CreatorIdentifier { get; set; }
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable

using Bit.Core.Models.Api;
using Bit.Core.Models.Api;

namespace Bit.Api.Tools.Models.Response;

public class SendFileDownloadDataResponseModel : ResponseModel
{
public string Id { get; set; }
public string Url { get; set; }
public string? Id { get; set; }
public string? Url { get; set; }

public SendFileDownloadDataResponseModel() : base("send-fileDownload") { }
}
Loading
Loading