mirror of
https://github.com/bitwarden/server.git
synced 2026-02-04 16:13:12 +08:00
* Add last updated validation to cipher replacements * Add AutoFixture scaffolding. AutoDataAttributes and ICustomizations are meant to automatically produce valid test input. Examples are the Cipher customizations, which enforce the model's mutual exclusivity of UserId and OrganizationId. FixtureExtensions create a fluent way to generate SUTs. We currently use parameter injection to fascilitate service testing, which is nicely handled by AutoNSubstitute. However, in order to gain access to the substitutions, we need to Freeze them onto the Fixture. The For fluent method allows specifying a Freeze to a specific type's constructor and optionally to a parameter name in that constructor. * Unit tests for single Cipher update version checks * Fix test runner Test runner requires Microsoft.NET.Test.Sdk * Move to provider model for SUT generation This model differs from previous in that you no longer need to specify which dependencies you would like access to. Instead, all are remembered and can be queried through the sutProvider. * User cipher provided by Put method reads Every put method already reads all relevant ciphers from database, there's no need to re-read them. JSON serialization of datetimes seems to leave truncate at second precision. Verify last known date time is within one second rather than exact. * validate revision date for share many requests * Update build script to use Github environment path Co-authored-by: Matt Gibson <mdgibson@Matts-MBP.lan>
270 lines
9.4 KiB
C#
270 lines
9.4 KiB
C#
using System;
|
|
using System.ComponentModel.DataAnnotations;
|
|
using Bit.Core.Utilities;
|
|
using Bit.Core.Models.Table;
|
|
using Bit.Core.Enums;
|
|
using Newtonsoft.Json;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using Core.Models.Data;
|
|
using Bit.Core.Models.Data;
|
|
using Newtonsoft.Json.Linq;
|
|
|
|
namespace Bit.Core.Models.Api
|
|
{
|
|
public class CipherRequestModel
|
|
{
|
|
public CipherType Type { get; set; }
|
|
|
|
[StringLength(36)]
|
|
public string OrganizationId { get; set; }
|
|
public string FolderId { get; set; }
|
|
public bool Favorite { get; set; }
|
|
[Required]
|
|
[EncryptedString]
|
|
[EncryptedStringLength(1000)]
|
|
public string Name { get; set; }
|
|
[EncryptedString]
|
|
[EncryptedStringLength(10000)]
|
|
public string Notes { get; set; }
|
|
public IEnumerable<CipherFieldModel> Fields { get; set; }
|
|
public IEnumerable<CipherPasswordHistoryModel> PasswordHistory { get; set; }
|
|
[Obsolete]
|
|
public Dictionary<string, string> Attachments { get; set; }
|
|
// TODO: Rename to Attachments whenever the above is finally removed.
|
|
public Dictionary<string, CipherAttachmentModel> Attachments2 { get; set; }
|
|
|
|
public CipherLoginModel Login { get; set; }
|
|
public CipherCardModel Card { get; set; }
|
|
public CipherIdentityModel Identity { get; set; }
|
|
public CipherSecureNoteModel SecureNote { get; set; }
|
|
public DateTime? LastKnownRevisionDate { get; set; } = null;
|
|
|
|
public CipherDetails ToCipherDetails(Guid userId, bool allowOrgIdSet = true)
|
|
{
|
|
var hasOrgId = !string.IsNullOrWhiteSpace(OrganizationId);
|
|
var cipher = new CipherDetails
|
|
{
|
|
Type = Type,
|
|
UserId = !hasOrgId ? (Guid?)userId : null,
|
|
OrganizationId = allowOrgIdSet && hasOrgId ? new Guid(OrganizationId) : (Guid?)null,
|
|
Edit = true,
|
|
ViewPassword = true,
|
|
};
|
|
ToCipherDetails(cipher);
|
|
return cipher;
|
|
}
|
|
|
|
public CipherDetails ToCipherDetails(CipherDetails existingCipher)
|
|
{
|
|
existingCipher.FolderId = string.IsNullOrWhiteSpace(FolderId) ? null : (Guid?)new Guid(FolderId);
|
|
existingCipher.Favorite = Favorite;
|
|
ToCipher(existingCipher);
|
|
return existingCipher;
|
|
}
|
|
|
|
public Cipher ToCipher(Cipher existingCipher)
|
|
{
|
|
switch (existingCipher.Type)
|
|
{
|
|
case CipherType.Login:
|
|
var loginObj = JObject.FromObject(new CipherLoginData(this),
|
|
new JsonSerializer { NullValueHandling = NullValueHandling.Ignore });
|
|
loginObj[nameof(CipherLoginData.Uri)]?.Parent?.Remove();
|
|
existingCipher.Data = loginObj.ToString(Formatting.None);
|
|
break;
|
|
case CipherType.Card:
|
|
existingCipher.Data = JsonConvert.SerializeObject(new CipherCardData(this),
|
|
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
|
|
break;
|
|
case CipherType.Identity:
|
|
existingCipher.Data = JsonConvert.SerializeObject(new CipherIdentityData(this),
|
|
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
|
|
break;
|
|
case CipherType.SecureNote:
|
|
existingCipher.Data = JsonConvert.SerializeObject(new CipherSecureNoteData(this),
|
|
new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore });
|
|
break;
|
|
default:
|
|
throw new ArgumentException("Unsupported type: " + nameof(Type) + ".");
|
|
}
|
|
|
|
var hasAttachments2 = (Attachments2?.Count ?? 0) > 0;
|
|
var hasAttachments = (Attachments?.Count ?? 0) > 0;
|
|
|
|
if (!hasAttachments2 && !hasAttachments)
|
|
{
|
|
return existingCipher;
|
|
}
|
|
|
|
var attachments = existingCipher.GetAttachments();
|
|
if ((attachments?.Count ?? 0) == 0)
|
|
{
|
|
return existingCipher;
|
|
}
|
|
|
|
if (hasAttachments2)
|
|
{
|
|
foreach (var attachment in attachments.Where(a => Attachments2.ContainsKey(a.Key)))
|
|
{
|
|
var attachment2 = Attachments2[attachment.Key];
|
|
attachment.Value.FileName = attachment2.FileName;
|
|
attachment.Value.Key = attachment2.Key;
|
|
}
|
|
}
|
|
else if (hasAttachments)
|
|
{
|
|
foreach (var attachment in attachments.Where(a => Attachments.ContainsKey(a.Key)))
|
|
{
|
|
attachment.Value.FileName = Attachments[attachment.Key];
|
|
attachment.Value.Key = null;
|
|
}
|
|
}
|
|
|
|
existingCipher.SetAttachments(attachments);
|
|
return existingCipher;
|
|
}
|
|
|
|
public Cipher ToOrganizationCipher()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(OrganizationId))
|
|
{
|
|
throw new ArgumentNullException(nameof(OrganizationId));
|
|
}
|
|
|
|
return ToCipher(new Cipher
|
|
{
|
|
Type = Type,
|
|
OrganizationId = new Guid(OrganizationId)
|
|
});
|
|
}
|
|
|
|
public CipherDetails ToOrganizationCipherDetails(Guid orgId)
|
|
{
|
|
return ToCipherDetails(new CipherDetails
|
|
{
|
|
Type = Type,
|
|
OrganizationId = orgId,
|
|
Edit = true
|
|
});
|
|
}
|
|
}
|
|
|
|
public class CipherWithIdRequestModel : CipherRequestModel
|
|
{
|
|
[Required]
|
|
public Guid? Id { get; set; }
|
|
}
|
|
|
|
public class CipherCreateRequestModel : IValidatableObject
|
|
{
|
|
public IEnumerable<Guid> CollectionIds { get; set; }
|
|
[Required]
|
|
public CipherRequestModel Cipher { get; set; }
|
|
|
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(Cipher.OrganizationId) && (!CollectionIds?.Any() ?? true))
|
|
{
|
|
yield return new ValidationResult("You must select at least one collection.",
|
|
new string[] { nameof(CollectionIds) });
|
|
}
|
|
}
|
|
}
|
|
|
|
public class CipherShareRequestModel : IValidatableObject
|
|
{
|
|
[Required]
|
|
public IEnumerable<string> CollectionIds { get; set; }
|
|
[Required]
|
|
public CipherRequestModel Cipher { get; set; }
|
|
|
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(Cipher.OrganizationId))
|
|
{
|
|
yield return new ValidationResult("Cipher OrganizationId is required.",
|
|
new string[] { nameof(Cipher.OrganizationId) });
|
|
}
|
|
|
|
if (!CollectionIds?.Any() ?? true)
|
|
{
|
|
yield return new ValidationResult("You must select at least one collection.",
|
|
new string[] { nameof(CollectionIds) });
|
|
}
|
|
}
|
|
}
|
|
|
|
public class CipherCollectionsRequestModel
|
|
{
|
|
[Required]
|
|
public IEnumerable<string> CollectionIds { get; set; }
|
|
}
|
|
|
|
public class CipherBulkDeleteRequestModel
|
|
{
|
|
[Required]
|
|
public IEnumerable<string> Ids { get; set; }
|
|
public string OrganizationId { get; set; }
|
|
}
|
|
|
|
public class CipherBulkRestoreRequestModel
|
|
{
|
|
[Required]
|
|
public IEnumerable<string> Ids { get; set; }
|
|
}
|
|
|
|
public class CipherBulkMoveRequestModel
|
|
{
|
|
[Required]
|
|
public IEnumerable<string> Ids { get; set; }
|
|
public string FolderId { get; set; }
|
|
}
|
|
|
|
public class CipherBulkShareRequestModel : IValidatableObject
|
|
{
|
|
[Required]
|
|
public IEnumerable<string> CollectionIds { get; set; }
|
|
[Required]
|
|
public IEnumerable<CipherWithIdRequestModel> Ciphers { get; set; }
|
|
|
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
|
{
|
|
if (!Ciphers?.Any() ?? true)
|
|
{
|
|
yield return new ValidationResult("You must select at least one cipher.",
|
|
new string[] { nameof(Ciphers) });
|
|
}
|
|
else
|
|
{
|
|
var allHaveIds = true;
|
|
var organizationIds = new HashSet<string>();
|
|
foreach (var c in Ciphers)
|
|
{
|
|
organizationIds.Add(c.OrganizationId);
|
|
if (allHaveIds)
|
|
{
|
|
allHaveIds = !(!c.Id.HasValue || string.IsNullOrWhiteSpace(c.OrganizationId));
|
|
}
|
|
}
|
|
|
|
if (!allHaveIds)
|
|
{
|
|
yield return new ValidationResult("All Ciphers must have an Id and OrganizationId.",
|
|
new string[] { nameof(Ciphers) });
|
|
}
|
|
else if (organizationIds.Count != 1)
|
|
{
|
|
yield return new ValidationResult("All ciphers must be for the same organization.");
|
|
}
|
|
}
|
|
|
|
if (!CollectionIds?.Any() ?? true)
|
|
{
|
|
yield return new ValidationResult("You must select at least one collection.",
|
|
new string[] { nameof(CollectionIds) });
|
|
}
|
|
}
|
|
}
|
|
}
|