1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-20 02:48:03 -05:00

Add sync response

This commit is contained in:
Bernd Schoolmann 2025-06-05 14:13:59 +02:00
parent 83c84a7cc0
commit 90ef67b05c
No known key found for this signature in database
11 changed files with 102 additions and 40 deletions

View File

@ -3,6 +3,7 @@ using Bit.Api.AdminConsole.Models.Response;
using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Auth.Models.Request.WebAuthn;
using Bit.Api.KeyManagement.Queries;
using Bit.Api.KeyManagement.Validators;
using Bit.Api.Models.Request.Accounts;
using Bit.Api.Models.Response;
@ -59,6 +60,7 @@ public class AccountsController : Controller
_organizationUserValidator;
private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>
_webauthnKeyValidator;
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
public AccountsController(
@ -79,7 +81,8 @@ public class AccountsController : Controller
emergencyAccessValidator,
IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>, IReadOnlyList<OrganizationUser>>
organizationUserValidator,
IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> webAuthnKeyValidator
IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>> webAuthnKeyValidator,
IUserAccountKeysQuery userAccountKeysQuery
)
{
_organizationService = organizationService;
@ -98,6 +101,7 @@ public class AccountsController : Controller
_emergencyAccessValidator = emergencyAccessValidator;
_organizationUserValidator = organizationUserValidator;
_webauthnKeyValidator = webAuthnKeyValidator;
_userAccountKeysQuery = userAccountKeysQuery;
}
@ -397,7 +401,9 @@ public class AccountsController : Controller
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
var response = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
var userAccountKeysData = await _userAccountKeysQuery.Run(user);
var response = new ProfileResponseModel(user, userAccountKeysData, organizationUserDetails, providerUserDetails,
providerUserOrganizationDetails, twoFactorEnabled,
hasPremiumFromOrg, organizationIdsClaimingActiveUser);
return response;
@ -430,8 +436,9 @@ public class AccountsController : Controller
var twoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
var userAccountKeysData = await _userAccountKeysQuery.Run(user);
var response = new ProfileResponseModel(user, null, null, null, twoFactorEnabled, hasPremiumFromOrg, organizationIdsClaimingActiveUser);
var response = new ProfileResponseModel(user, userAccountKeysData, null, null, null, twoFactorEnabled, hasPremiumFromOrg, organizationIdsClaimingActiveUser);
return response;
}
@ -449,8 +456,9 @@ public class AccountsController : Controller
var userTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
var userAccountKeysData = await _userAccountKeysQuery.Run(user);
var response = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingActiveUser);
var response = new ProfileResponseModel(user, userAccountKeysData, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingActiveUser);
return response;
}

View File

@ -1,4 +1,5 @@
#nullable enable
using Bit.Api.KeyManagement.Queries;
using Bit.Api.Models.Request;
using Bit.Api.Models.Request.Accounts;
using Bit.Api.Models.Response;
@ -20,7 +21,8 @@ namespace Bit.Api.Billing.Controllers;
[Authorize("Application")]
public class AccountsController(
IUserService userService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) : Controller
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IUserAccountKeysQuery userAccountKeysQuery) : Controller
{
[HttpPost("premium")]
public async Task<PaymentResponseModel> PostPremiumAsync(
@ -57,8 +59,9 @@ public class AccountsController(
var userTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
var userHasPremiumFromOrganization = await userService.HasPremiumFromOrganization(user);
var organizationIdsClaimingActiveUser = await GetOrganizationIdsClaimingUserAsync(user.Id);
var userAccountKeysData = await userAccountKeysQuery.Run(user);
var profile = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled,
var profile = new ProfileResponseModel(user, userAccountKeysData, null, null, null, userTwoFactorEnabled,
userHasPremiumFromOrganization, organizationIdsClaimingActiveUser);
return new PaymentResponseModel
{

View File

@ -3,28 +3,36 @@ using Bit.Core.Models.Api;
namespace Bit.Api.Models.Response;
#nullable enable
/// <summary>
/// This response model is used to return keys of a user - downstream of the user key - to the client.
/// This includes the private keys (signature/encryption), and proof tying one to another. This could
/// also be used to contain further user-owned keys in the future (per-vault keys, etc). This should
/// not be used to contain keys not just owned by the user (e.g. organization keys).
/// This response model is used to return the the asymmetric encryption keys,
/// and signature keys of an entity. This includes the private keys of the key pairs,
/// (private key, signing key), and the public keys of the key pairs (unsigned public key,
/// signed public key, verification key).
/// </summary>
public class PrivateAccountKeysResponseModel : ResponseModel
public class PrivateKeysResponseModel : ResponseModel
{
public PrivateAccountKeysResponseModel(UserAccountKeysData accountKeys) : base("accountKeys")
public PrivateKeysResponseModel(UserAccountKeysData accountKeys) : base("accountKeys")
{
if (accountKeys != null)
if (accountKeys == null)
{
SignatureKeyPair = accountKeys.signatureKeyPairData;
throw new ArgumentNullException(nameof(accountKeys));
}
if (accountKeys.SignatureKeyPairData != null)
{
SignatureKeyPair = accountKeys.SignatureKeyPairData;
}
PublicKeyEncryptionKeyPair = accountKeys.PublicKeyEncryptionKeyPairData;
}
public PrivateAccountKeysResponseModel() : base("accountKeys")
public PrivateKeysResponseModel() : base("privateKeys")
{
}
public SignatureKeyPairData SignatureKeyPair { get; set; }
public PublicKeyEncryptionKeyPairData PublicKeyEncryptionKeyPair { get; set; }
// Not all accounts have signature keys, but all accounts have public encryption keys.
public SignatureKeyPairData? SignatureKeyPair { get; set; }
public required PublicKeyEncryptionKeyPairData PublicKeyEncryptionKeyPair { get; set; }
}

View File

@ -1,16 +0,0 @@
using Bit.Core.Models.Api;
namespace Bit.Api.KeyManagement.Models.Response;
public class UserKeyResponseModel : ResponseModel
{
public UserKeyResponseModel(Guid id, string key)
: base("userKey")
{
UserId = id;
PublicKey = key;
}
public Guid UserId { get; set; }
public string PublicKey { get; set; }
}

View File

@ -0,0 +1,25 @@
#nullable enable
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Repositories;
namespace Bit.Api.KeyManagement.Queries;
public interface IUserAccountKeysQuery
{
Task<UserAccountKeysData> Run(User user);
}
public class UserAccountKeysQuery(IUserSignatureKeyPairRepository signatureKeyPairRepository) : IUserAccountKeysQuery
{
public async Task<UserAccountKeysData> Run(User user)
{
var userAccountKeysData = new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
SignatureKeyPairData = await signatureKeyPairRepository.GetByUserIdAsync(user.Id)
};
return userAccountKeysData;
}
}

View File

@ -2,6 +2,7 @@
using Bit.Api.AdminConsole.Models.Response.Providers;
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Models.Api;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
@ -10,6 +11,7 @@ namespace Bit.Api.Models.Response;
public class ProfileResponseModel : ResponseModel
{
public ProfileResponseModel(User user,
UserAccountKeysData userAccountKeysData,
IEnumerable<OrganizationUserOrganizationDetails> organizationsUserDetails,
IEnumerable<ProviderUserProviderDetails> providerUserDetails,
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
@ -32,6 +34,7 @@ public class ProfileResponseModel : ResponseModel
TwoFactorEnabled = twoFactorEnabled;
Key = user.Key;
PrivateKey = user.PrivateKey;
AccountKeys = userAccountKeysData;
SecurityStamp = user.SecurityStamp;
ForcePasswordReset = user.ForcePasswordReset;
UsesKeyConnector = user.UsesKeyConnector;
@ -57,7 +60,9 @@ public class ProfileResponseModel : ResponseModel
public string Culture { get; set; }
public bool TwoFactorEnabled { get; set; }
public string Key { get; set; }
[Obsolete("Use AccountKeys instead.")]
public string PrivateKey { get; set; }
public UserAccountKeysData AccountKeys { get; set; }
public string SecurityStamp { get; set; }
public bool ForcePasswordReset { get; set; }
public bool UsesKeyConnector { get; set; }

View File

@ -8,6 +8,8 @@ using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Repositories;
using Bit.Core.Models.Data;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -39,6 +41,7 @@ public class SyncController : Controller
private readonly IFeatureService _featureService;
private readonly IApplicationCacheService _applicationCacheService;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IUserSignatureKeyPairRepository _userSignatureKeyPairRepository;
public SyncController(
IUserService userService,
@ -54,7 +57,8 @@ public class SyncController : Controller
ICurrentContext currentContext,
IFeatureService featureService,
IApplicationCacheService applicationCacheService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IUserSignatureKeyPairRepository userSignatureKeyPairRepository)
{
_userService = userService;
_folderRepository = folderRepository;
@ -70,6 +74,7 @@ public class SyncController : Controller
_featureService = featureService;
_applicationCacheService = applicationCacheService;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_userSignatureKeyPairRepository = userSignatureKeyPairRepository;
}
[HttpGet("")]
@ -112,8 +117,14 @@ public class SyncController : Controller
var organizationIdsClaimingActiveUser = organizationClaimingActiveUser.Select(o => o.Id);
var organizationAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
var signingKeys = await _userSignatureKeyPairRepository.GetByUserIdAsync(user.Id);
var userAccountKeysData = new UserAccountKeysData
{
PublicKeyEncryptionKeyPairData = user.GetPublicKeyEncryptionKeyPair(),
SignatureKeyPairData = signingKeys,
};
var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities,
var response = new SyncResponseModel(_globalSettings, user, userAccountKeysData, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationAbilities,
organizationIdsClaimingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails,
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends);
return response;

View File

@ -4,6 +4,7 @@ using Bit.Api.Tools.Models.Response;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Models.Api;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations;
@ -20,6 +21,7 @@ public class SyncResponseModel : ResponseModel
public SyncResponseModel(
GlobalSettings globalSettings,
User user,
UserAccountKeysData userAccountKeysData,
bool userTwoFactorEnabled,
bool userHasPremiumFromOrganization,
IDictionary<Guid, OrganizationAbility> organizationAbilities,
@ -36,7 +38,7 @@ public class SyncResponseModel : ResponseModel
IEnumerable<Send> sends)
: base("sync")
{
Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
Profile = new ProfileResponseModel(user, userAccountKeysData, organizationUserDetails, providerUserDetails,
providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsClaimingingUser);
Folders = folders.Select(f => new FolderResponseModel(f));
Ciphers = ciphers.Select(cipher =>

View File

@ -3,6 +3,7 @@ using System.Text.Json;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Identity;
@ -254,4 +255,14 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
{
return MasterPassword != null;
}
public PublicKeyEncryptionKeyPairData GetPublicKeyEncryptionKeyPair()
{
return new PublicKeyEncryptionKeyPairData
{
WrappedPrivateKey = PrivateKey,
SignedPublicKey = SignedPublicKey,
PublicKey = PublicKey
};
}
}

View File

@ -1,7 +1,9 @@
namespace Bit.Core.KeyManagement.Models.Data;
#nullable enable
public class UserAccountKeysData
{
public PublicKeyEncryptionKeyPairData PublicKeyEncryptionKeyPairData { get; set; }
public SignatureKeyPairData signatureKeyPairData { get; set; }
public required PublicKeyEncryptionKeyPairData PublicKeyEncryptionKeyPairData { get; set; }
public SignatureKeyPairData? SignatureKeyPairData { get; set; }
}

View File

@ -4,6 +4,7 @@ using Bit.Api.Auth.Controllers;
using Bit.Api.Auth.Models.Request;
using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Auth.Models.Request.WebAuthn;
using Bit.Api.KeyManagement.Queries;
using Bit.Api.KeyManagement.Validators;
using Bit.Api.Tools.Models.Request;
using Bit.Api.Vault.Models.Request;
@ -54,7 +55,7 @@ public class AccountsControllerTests : IDisposable
_resetPasswordValidator;
private readonly IRotationValidator<IEnumerable<WebAuthnLoginRotateKeyRequestModel>, IEnumerable<WebAuthnLoginRotateKeyData>>
_webauthnKeyRotationValidator;
private readonly IUserAccountKeysQuery _userAccountKeysQuery;
public AccountsControllerTests()
{
@ -79,6 +80,7 @@ public class AccountsControllerTests : IDisposable
_resetPasswordValidator = Substitute
.For<IRotationValidator<IEnumerable<ResetPasswordWithOrgIdRequestModel>,
IReadOnlyList<OrganizationUser>>>();
_userAccountKeysQuery = Substitute.For<IUserAccountKeysQuery>();
_sut = new AccountsController(
_organizationService,
@ -96,7 +98,8 @@ public class AccountsControllerTests : IDisposable
_sendValidator,
_emergencyAccessValidator,
_resetPasswordValidator,
_webauthnKeyRotationValidator
_webauthnKeyRotationValidator,
_userAccountKeysQuery
);
}