Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions src/Api/Vault/Controllers/SyncController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public class SyncController : Controller
private readonly GlobalSettings _globalSettings;
private readonly ICurrentContext _currentContext;
private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion);
private readonly Version _bankAccountCipherMinimumVersion = new(Constants.BankAccountCipherMinimumVersion);
private readonly IFeatureService _featureService;
private readonly IApplicationCacheService _applicationCacheService;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
Expand Down Expand Up @@ -104,7 +105,7 @@ await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id,

var folders = await _folderRepository.GetManyByUserIdAsync(user.Id);
var allCiphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, withOrganizations: hasEnabledOrgs);
var ciphers = FilterSSHKeys(allCiphers);
var ciphers = FilterUnsupportedCipherTypes(allCiphers);
var sends = await _sendRepository.GetManyByUserIdAsync(user.Id);

IEnumerable<CollectionDetails> collections = null;
Expand Down Expand Up @@ -141,15 +142,22 @@ await _providerUserRepository.GetManyOrganizationDetailsByUserAsync(user.Id,
return response;
}

private ICollection<CipherDetails> FilterSSHKeys(ICollection<CipherDetails> ciphers)
private ICollection<CipherDetails> FilterUnsupportedCipherTypes(ICollection<CipherDetails> ciphers)
{
if (_currentContext.ClientVersion >= _sshKeyCipherMinimumVersion || _featureService.IsEnabled(FeatureFlagKeys.SSHVersionCheckQAOverride))
var unsupportedTypes = new List<Core.Vault.Enums.CipherType>();

if (_currentContext.ClientVersion < _sshKeyCipherMinimumVersion && !_featureService.IsEnabled(FeatureFlagKeys.SSHVersionCheckQAOverride))
{
return ciphers;
unsupportedTypes.Add(Core.Vault.Enums.CipherType.SSHKey);
}
else

if (!_featureService.IsEnabled(FeatureFlagKeys.PM32009_NewItemTypes) || _currentContext.ClientVersion < _bankAccountCipherMinimumVersion)
{
return ciphers.Where(c => c.Type != Core.Vault.Enums.CipherType.SSHKey).ToList();
unsupportedTypes.Add(Core.Vault.Enums.CipherType.BankAccount);
}

return unsupportedTypes.Count == 0
? ciphers
: ciphers.Where(c => !unsupportedTypes.Contains(c.Type)).ToList();
}
}
63 changes: 63 additions & 0 deletions src/Api/Vault/Models/CipherBankAccountModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
ο»Ώusing Bit.Core.Utilities;
using Bit.Core.Vault.Models.Data;

namespace Bit.Api.Vault.Models;

public class CipherBankAccountModel
{
public CipherBankAccountModel() { }

public CipherBankAccountModel(CipherBankAccountData data)
{
BankName = data.BankName;
NameOnAccount = data.NameOnAccount;
AccountType = data.AccountType;
AccountNumber = data.AccountNumber;
RoutingNumber = data.RoutingNumber;
BranchNumber = data.BranchNumber;
Pin = data.Pin;
SwiftCode = data.SwiftCode;
Iban = data.Iban;
BankContactPhone = data.BankContactPhone;
}

[EncryptedString]
[EncryptedStringLength(1000)]
public string? BankName { get; set; }

[EncryptedString]
[EncryptedStringLength(1000)]
public string? NameOnAccount { get; set; }

[EncryptedString]
[EncryptedStringLength(1000)]
public string? AccountType { get; set; }

[EncryptedString]
[EncryptedStringLength(1000)]
public string? AccountNumber { get; set; }

[EncryptedString]
[EncryptedStringLength(1000)]
public string? RoutingNumber { get; set; }

[EncryptedString]
[EncryptedStringLength(1000)]
public string? BranchNumber { get; set; }

[EncryptedString]
[EncryptedStringLength(1000)]
public string? Pin { get; set; }

[EncryptedString]
[EncryptedStringLength(1000)]
public string? SwiftCode { get; set; }

[EncryptedString]
[EncryptedStringLength(1000)]
public string? Iban { get; set; }

[EncryptedString]
[EncryptedStringLength(1000)]
public string? BankContactPhone { get; set; }
}
27 changes: 27 additions & 0 deletions src/Api/Vault/Models/Request/CipherRequestModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ public class CipherRequestModel
[Obsolete("Use Data instead.")]
public CipherSSHKeyModel SSHKey { get; set; }

[Obsolete("Use Data instead.")] public CipherBankAccountModel BankAccount { get; set; }

/// <summary>
/// JSON string containing cipher-specific data
/// </summary>
Expand Down Expand Up @@ -120,6 +122,10 @@ public Cipher ToCipher(Cipher existingCipher, Guid? userId = null)
case CipherType.SSHKey:
existingCipher.Data = JsonSerializer.Serialize(ToCipherSSHKeyData(), JsonHelpers.IgnoreWritingNull);
break;
case CipherType.BankAccount:
existingCipher.Data =
JsonSerializer.Serialize(ToCipherBankAccountData(), JsonHelpers.IgnoreWritingNull);
break;
default:
throw new ArgumentException("Unsupported type: " + nameof(Type) + ".");
}
Expand Down Expand Up @@ -296,6 +302,27 @@ private CipherSSHKeyData ToCipherSSHKeyData()
};
}

private CipherBankAccountData ToCipherBankAccountData()
{
return new CipherBankAccountData
{
Name = Name,
Notes = Notes,
Fields = Fields?.Select(f => f.ToCipherFieldData()),
PasswordHistory = PasswordHistory?.Select(ph => ph.ToCipherPasswordHistoryData()),
BankName = BankAccount.BankName,
NameOnAccount = BankAccount.NameOnAccount,
AccountType = BankAccount.AccountType,
AccountNumber = BankAccount.AccountNumber,
RoutingNumber = BankAccount.RoutingNumber,
BranchNumber = BankAccount.BranchNumber,
Pin = BankAccount.Pin,
SwiftCode = BankAccount.SwiftCode,
Iban = BankAccount.Iban,
BankContactPhone = BankAccount.BankContactPhone,
};
}

/// <summary>
/// Updates a JSON string representing a dictionary by adding, updating, or removing a key-value pair
/// based on the provided userIdKey and newValue.
Expand Down
8 changes: 8 additions & 0 deletions src/Api/Vault/Models/Response/CipherResponseModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ public CipherMiniResponseModel(Cipher cipher, IGlobalSettings globalSettings, bo
cipherData = sshKeyData;
SSHKey = new CipherSSHKeyModel(sshKeyData);
break;
case CipherType.BankAccount:
var bankAccountData = JsonSerializer.Deserialize<CipherBankAccountData>(cipher.Data);
cipherData = bankAccountData;
BankAccount = new CipherBankAccountModel(bankAccountData);
break;
default:
throw new ArgumentException("Unsupported " + nameof(Type) + ".");
}
Expand Down Expand Up @@ -98,6 +103,9 @@ public CipherMiniResponseModel(Cipher cipher, IGlobalSettings globalSettings, bo
[Obsolete("Use Data instead.")]
public CipherSSHKeyModel SSHKey { get; set; }

[Obsolete("Use Data instead.")]
public CipherBankAccountModel BankAccount { get; set; }

[Obsolete("Use Data instead.")]
public IEnumerable<CipherFieldModel> Fields { get; set; }

Expand Down
4 changes: 4 additions & 0 deletions src/Core/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ public static class Constants

public const string Fido2KeyCipherMinimumVersion = "2023.10.0";
public const string SSHKeyCipherMinimumVersion = "2024.12.0";

// TODO: Update with actual version once the feature is implemented
public const string BankAccountCipherMinimumVersion = "2026.2.0";
public const string DenyLegacyUserMinimumVersion = "2025.6.0";

/// <summary>
Expand Down Expand Up @@ -262,6 +265,7 @@ public static class FeatureFlagKeys
public const string PM29438_WelcomeDialogWithExtensionPrompt = "pm-29438-welcome-dialog-with-extension-prompt";
public const string PM29438_DialogWithExtensionPromptAccountAge = "pm-29438-dialog-with-extension-prompt-account-age";
public const string PM31039_ItemActionInExtension = "pm-31039-item-action-in-extension";
public const string PM32009_NewItemTypes = "pm-32009-new-item-types";

/* Innovation Team */
public const string ArchiveVaultItems = "pm-19148-innovation-archive";
Expand Down
1 change: 1 addition & 0 deletions src/Core/Vault/Enums/CipherType.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ public enum CipherType : byte
Card = 3,
Identity = 4,
SSHKey = 5,
BankAccount = 6,
}
17 changes: 17 additions & 0 deletions src/Core/Vault/Models/Data/CipherBankAccountData.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
ο»Ώnamespace Bit.Core.Vault.Models.Data;

public class CipherBankAccountData : CipherData
{
public CipherBankAccountData() { }

public string? BankName { get; set; }
public string? NameOnAccount { get; set; }
public string? AccountType { get; set; }
public string? AccountNumber { get; set; }
public string? RoutingNumber { get; set; }
public string? BranchNumber { get; set; }
public string? Pin { get; set; }
public string? SwiftCode { get; set; }
public string? Iban { get; set; }
public string? BankContactPhone { get; set; }
}
2 changes: 2 additions & 0 deletions src/Core/Vault/Services/Implementations/CipherService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1126,6 +1126,7 @@ private string SerializeCipherData(CipherData data)
CipherCardData cardData => JsonSerializer.Serialize(cardData),
CipherSecureNoteData noteData => JsonSerializer.Serialize(noteData),
CipherSSHKeyData sshKeyData => JsonSerializer.Serialize(sshKeyData),
CipherBankAccountData bankAccountData => JsonSerializer.Serialize(bankAccountData),
_ => throw new ArgumentException("Unsupported cipher data type.", nameof(data))
};
}
Expand All @@ -1139,6 +1140,7 @@ private CipherData DeserializeCipherData(Cipher cipher)
CipherType.Card => JsonSerializer.Deserialize<CipherCardData>(cipher.Data),
CipherType.SecureNote => JsonSerializer.Deserialize<CipherSecureNoteData>(cipher.Data),
CipherType.SSHKey => JsonSerializer.Deserialize<CipherSSHKeyData>(cipher.Data),
CipherType.BankAccount => JsonSerializer.Deserialize<CipherBankAccountData>(cipher.Data),
_ => throw new ArgumentException("Unsupported cipher type.", nameof(cipher))
};
}
Expand Down
125 changes: 125 additions & 0 deletions test/Api.Test/Vault/Controllers/SyncControllerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
using AutoFixture;
using Bit.Api.Vault.Controllers;
using Bit.Api.Vault.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Models;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
Expand All @@ -22,6 +24,7 @@
using Bit.Core.Tools.Entities;
using Bit.Core.Tools.Repositories;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Enums;
using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Repositories;
using Bit.Test.Common.AutoFixture;
Expand Down Expand Up @@ -405,6 +408,128 @@ public async Task Get_HaveMasterPassword_UserDecryptionMasterPasswordUnlockNotNu
Assert.Equal(user.Email.ToLower(), result.UserDecryption.MasterPasswordUnlock.Salt);
}

[Theory]
[BitAutoData]
public async Task Get_BankAccountCiphers_ReturnedWhenFlagEnabledAndClientVersionSupported(
User user, SutProvider<SyncController> sutProvider)
{
user.EquivalentDomains = null;
user.ExcludedGlobalEquivalentDomains = null;

var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);

var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
SignatureKeyPairData = null,
});

var bankAccountCipher = new CipherDetails { Type = CipherType.BankAccount, Data = "{}", UserId = user.Id };
var loginCipher = new CipherDetails { Type = CipherType.Login, Data = "{}", UserId = user.Id };
var ciphers = new List<CipherDetails> { bankAccountCipher, loginCipher };

sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(user.Id, Arg.Any<bool>()).Returns(ciphers);

sutProvider.GetDependency<ICurrentContext>()
.ClientVersion.Returns(new Version(Constants.BankAccountCipherMinimumVersion));

sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM32009_NewItemTypes).Returns(true);

sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(user).Returns(false);
userService.HasPremiumFromOrganization(user).Returns(false);

var result = await sutProvider.Sut.Get();

Assert.Contains(result.Ciphers, c => c.Type == CipherType.BankAccount);
}

[Theory]
[BitAutoData]
public async Task Get_BankAccountCiphers_FilteredWhenFlagDisabled(
User user, SutProvider<SyncController> sutProvider)
{
user.EquivalentDomains = null;
user.ExcludedGlobalEquivalentDomains = null;

var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);

var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
SignatureKeyPairData = null,
});

var bankAccountCipher = new CipherDetails { Type = CipherType.BankAccount, Data = "{}", UserId = user.Id };
var loginCipher = new CipherDetails { Type = CipherType.Login, Data = "{}", UserId = user.Id };
var ciphers = new List<CipherDetails> { bankAccountCipher, loginCipher };

sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(user.Id, Arg.Any<bool>()).Returns(ciphers);

// New client version but flag disabled
sutProvider.GetDependency<ICurrentContext>()
.ClientVersion.Returns(new Version(Constants.BankAccountCipherMinimumVersion));

sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM32009_NewItemTypes).Returns(false);

sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(user).Returns(false);
userService.HasPremiumFromOrganization(user).Returns(false);

var result = await sutProvider.Sut.Get();

Assert.DoesNotContain(result.Ciphers, c => c.Type == CipherType.BankAccount);
Assert.Contains(result.Ciphers, c => c.Type == CipherType.Login);
}

[Theory]
[BitAutoData]
public async Task Get_BankAccountCiphers_FilteredWhenFlagEnabledButClientVersionTooOld(
User user, SutProvider<SyncController> sutProvider)
{
user.EquivalentDomains = null;
user.ExcludedGlobalEquivalentDomains = null;

var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).ReturnsForAnyArgs(user);

var userAccountKeysQuery = sutProvider.GetDependency<IUserAccountKeysQuery>();
userAccountKeysQuery.Run(user).Returns(new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
SignatureKeyPairData = null,
});

var bankAccountCipher = new CipherDetails { Type = CipherType.BankAccount, Data = "{}", UserId = user.Id };
var ciphers = new List<CipherDetails> { bankAccountCipher };

sutProvider.GetDependency<ICipherRepository>()
.GetManyByUserIdAsync(user.Id, Arg.Any<bool>()).Returns(ciphers);

// Flag enabled but old client version
sutProvider.GetDependency<ICurrentContext>()
.ClientVersion.Returns(new Version("2025.1.0"));

sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.PM32009_NewItemTypes).Returns(true);

sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(user).Returns(false);
userService.HasPremiumFromOrganization(user).Returns(false);

var result = await sutProvider.Sut.Get();

Assert.DoesNotContain(result.Ciphers, c => c.Type == CipherType.BankAccount);
}

private async Task AssertMethodsCalledAsync(IUserService userService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IOrganizationUserRepository organizationUserRepository,
Expand Down
Loading
Loading