From 08e9d329e21e382e91d688431dac58be877a1c98 Mon Sep 17 00:00:00 2001 From: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Date: Tue, 24 Mar 2026 10:09:01 -0400 Subject: [PATCH 1/4] Ensuring the creation date and update date are used correctly, and that editorName gets returns to clients side properly --- .../Controllers/SecretVersionsController.cs | 44 ++++++++++++++++--- .../Controllers/SecretsController.cs | 16 ++++--- .../Response/SecretVersionResponseModel.cs | 1 + 3 files changed, 49 insertions(+), 12 deletions(-) diff --git a/src/Api/SecretsManager/Controllers/SecretVersionsController.cs b/src/Api/SecretsManager/Controllers/SecretVersionsController.cs index 86e2d1f7e9c1..c08495e81c3b 100644 --- a/src/Api/SecretsManager/Controllers/SecretVersionsController.cs +++ b/src/Api/SecretsManager/Controllers/SecretVersionsController.cs @@ -21,19 +21,25 @@ public class SecretVersionsController : Controller private readonly ISecretRepository _secretRepository; private readonly IUserService _userService; private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IServiceAccountRepository _serviceAccountRepository; + private readonly IUserRepository _userRepository; public SecretVersionsController( ICurrentContext currentContext, ISecretVersionRepository secretVersionRepository, ISecretRepository secretRepository, IUserService userService, - IOrganizationUserRepository organizationUserRepository) + IOrganizationUserRepository organizationUserRepository, + IServiceAccountRepository serviceAccountRepository, + IUserRepository userRepository) { _currentContext = currentContext; _secretVersionRepository = secretVersionRepository; _secretRepository = secretRepository; _userService = userService; _organizationUserRepository = organizationUserRepository; + _serviceAccountRepository = serviceAccountRepository; + _userRepository = userRepository; } [HttpGet("secrets/{secretId}/versions")] @@ -51,7 +57,7 @@ public async Task> GetVersionsBySe { // Already verified Secrets Manager access above var versionList = await _secretVersionRepository.GetManyBySecretIdAsync(secretId); - var responseList = versionList.Select(v => new SecretVersionResponseModel(v)); + var responseList = await Task.WhenAll(versionList.Select(v => ResolveVersionWithEditorName(v, secret.OrganizationId))); return new ListResponseModel(responseList); } @@ -71,7 +77,7 @@ public async Task> GetVersionsBySe } var versions = await _secretVersionRepository.GetManyBySecretIdAsync(secretId); - var responses = versions.Select(v => new SecretVersionResponseModel(v)); + var responses = await Task.WhenAll(versions.Select(v => ResolveVersionWithEditorName(v, secret.OrganizationId))); return new ListResponseModel(responses); } @@ -96,7 +102,7 @@ public async Task GetByIdAsync([FromRoute] Guid id) _currentContext.IdentityClientType == IdentityClientType.Organization) { // Already verified Secrets Manager access above - return new SecretVersionResponseModel(secretVersion); + return await ResolveVersionWithEditorName(secretVersion, secret.OrganizationId); } var userId = _userService.GetProperUserId(User); @@ -114,7 +120,7 @@ public async Task GetByIdAsync([FromRoute] Guid id) throw new NotFoundException(); } - return new SecretVersionResponseModel(secretVersion); + return await ResolveVersionWithEditorName(secretVersion, secret.OrganizationId); } [HttpPost("secret-versions/get-by-ids")] @@ -334,4 +340,32 @@ public async Task BulkDeleteAsync([FromBody] List ids) return Ok(); } + + private async Task ResolveVersionWithEditorName(Bit.Core.SecretsManager.Entities.SecretVersion secretVersion, Guid organizationId) + { + var response = new SecretVersionResponseModel(secretVersion); + + if (secretVersion.EditorServiceAccountId.HasValue) + { + var serviceAccount = await _serviceAccountRepository.GetByIdAsync(secretVersion.EditorServiceAccountId.Value); + if (serviceAccount != null) + { + response.EditorName = serviceAccount.Name; + } + } + else if (secretVersion.EditorOrganizationUserId.HasValue) + { + var orgUser = await _organizationUserRepository.GetByIdAsync(secretVersion.EditorOrganizationUserId.Value); + if (orgUser?.UserId.HasValue == true) + { + var user = await _userRepository.GetByIdAsync(orgUser.UserId.Value); + if (user != null) + { + response.EditorName = user.Name; + } + } + } + + return response; + } } diff --git a/src/Api/SecretsManager/Controllers/SecretsController.cs b/src/Api/SecretsManager/Controllers/SecretsController.cs index dcfe1be11188..fa248095dcbd 100644 --- a/src/Api/SecretsManager/Controllers/SecretsController.cs +++ b/src/Api/SecretsManager/Controllers/SecretsController.cs @@ -177,6 +177,7 @@ public async Task UpdateSecretAsync([FromRoute] Guid id, [F throw new NotFoundException(); } + var originalSecret = secret; var updatedSecret = updateRequest.ToSecret(secret); var authorizationResult = await _authorizationService.AuthorizeAsync(User, updatedSecret, SecretOperations.Update); if (!authorizationResult.Succeeded) @@ -200,8 +201,6 @@ public async Task UpdateSecretAsync([FromRoute] Guid id, [F // Create a version record if the value changed if (updateRequest.ValueChanged) { - // Store the old value before updating - var oldValue = secret.Value; var userId = _userService.GetProperUserId(User)!.Value; Guid? editorServiceAccountId = null; Guid? editorOrganizationUserId = null; @@ -217,21 +216,24 @@ public async Task UpdateSecretAsync([FromRoute] Guid id, [F { editorOrganizationUserId = orgUser.Id; } - else + } + else if (_currentContext.IdentityClientType == IdentityClientType.Organization) + { + var orgUser = await _organizationUserRepository.GetByOrganizationAsync(secret.OrganizationId, userId); + if (orgUser != null) { - throw new NotFoundException(); + editorOrganizationUserId = orgUser.Id; } } var secretVersion = new SecretVersion { SecretId = id, - Value = oldValue, - VersionDate = DateTime.UtcNow, + Value = originalSecret.Value, + VersionDate = originalSecret.CreationDate, EditorServiceAccountId = editorServiceAccountId, EditorOrganizationUserId = editorOrganizationUserId }; - await _secretVersionRepository.CreateAsync(secretVersion); } diff --git a/src/Api/SecretsManager/Models/Response/SecretVersionResponseModel.cs b/src/Api/SecretsManager/Models/Response/SecretVersionResponseModel.cs index 07b8e88f7e51..468fe6457794 100644 --- a/src/Api/SecretsManager/Models/Response/SecretVersionResponseModel.cs +++ b/src/Api/SecretsManager/Models/Response/SecretVersionResponseModel.cs @@ -13,6 +13,7 @@ public class SecretVersionResponseModel : ResponseModel public DateTime VersionDate { get; set; } public Guid? EditorServiceAccountId { get; set; } public Guid? EditorOrganizationUserId { get; set; } + public string? EditorName { get; set; } public SecretVersionResponseModel() : base(_objectName) { } From 5bbc4e97fdfb5f8e611365b11883ddb0b40f88f0 Mon Sep 17 00:00:00 2001 From: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Date: Wed, 22 Apr 2026 15:18:22 -0400 Subject: [PATCH 2/4] Logging an update event when the user restores a secret version, fix date issues with version dates --- .../Controllers/SecretVersionsController.cs | 31 +++++++++++++++++-- .../Controllers/SecretsController.cs | 8 +++-- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/Api/SecretsManager/Controllers/SecretVersionsController.cs b/src/Api/SecretsManager/Controllers/SecretVersionsController.cs index c08495e81c3b..1b1cd4d7924c 100644 --- a/src/Api/SecretsManager/Controllers/SecretVersionsController.cs +++ b/src/Api/SecretsManager/Controllers/SecretVersionsController.cs @@ -6,6 +6,7 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Repositories; +using Bit.Core.SecretsManager.Entities; using Bit.Core.SecretsManager.Repositories; using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; @@ -23,6 +24,7 @@ public class SecretVersionsController : Controller private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IServiceAccountRepository _serviceAccountRepository; private readonly IUserRepository _userRepository; + private readonly IEventService _eventService; public SecretVersionsController( ICurrentContext currentContext, @@ -31,7 +33,8 @@ public SecretVersionsController( IUserService userService, IOrganizationUserRepository organizationUserRepository, IServiceAccountRepository serviceAccountRepository, - IUserRepository userRepository) + IUserRepository userRepository, + IEventService eventService) { _currentContext = currentContext; _secretVersionRepository = secretVersionRepository; @@ -40,6 +43,7 @@ public SecretVersionsController( _organizationUserRepository = organizationUserRepository; _serviceAccountRepository = serviceAccountRepository; _userRepository = userRepository; + _eventService = eventService; } [HttpGet("secrets/{secretId}/versions")] @@ -207,6 +211,7 @@ public async Task RestoreVersionAsync([FromRoute] Guid secr // Store the current value before restoration var currentValue = secret.Value; + var currentValueRevisionDate = secret.RevisionDate; // For service accounts and organization API, skip user-level access checks if (_currentContext.IdentityClientType == IdentityClientType.ServiceAccount) @@ -221,7 +226,7 @@ public async Task RestoreVersionAsync([FromRoute] Guid secr { SecretId = secretId, Value = currentValue!, - VersionDate = DateTime.UtcNow, + VersionDate = currentValueRevisionDate, EditorServiceAccountId = editorUserId.Value }; @@ -233,6 +238,7 @@ public async Task RestoreVersionAsync([FromRoute] Guid secr secret.Value = version.Value; secret.RevisionDate = DateTime.UtcNow; var updatedSec = await _secretRepository.UpdateAsync(secret); + await LogSecretEventAsync(updatedSec, EventType.Secret_Edited); return new SecretResponseModel(updatedSec, true, true); } @@ -264,7 +270,7 @@ public async Task RestoreVersionAsync([FromRoute] Guid secr { SecretId = secretId, Value = currentValue!, - VersionDate = DateTime.UtcNow, + VersionDate = currentValueRevisionDate, EditorOrganizationUserId = orgUser.Id }; @@ -276,6 +282,7 @@ public async Task RestoreVersionAsync([FromRoute] Guid secr secret.RevisionDate = DateTime.UtcNow; var updatedSecret = await _secretRepository.UpdateAsync(secret); + await LogSecretEventAsync(updatedSecret, EventType.Secret_Edited); return new SecretResponseModel(updatedSecret, true, true); } @@ -368,4 +375,22 @@ private async Task ResolveVersionWithEditorName(Bit. return response; } + + private async Task LogSecretsEventAsync(IEnumerable secrets, EventType eventType) + { + var userId = _userService.GetProperUserId(User)!.Value; + + switch (_currentContext.IdentityClientType) + { + case IdentityClientType.ServiceAccount: + await _eventService.LogServiceAccountSecretsEventAsync(userId, secrets, eventType); + break; + case IdentityClientType.User: + await _eventService.LogUserSecretsEventAsync(userId, secrets, eventType); + break; + } + } + + private Task LogSecretEventAsync(Secret secret, EventType eventType) => + LogSecretsEventAsync(new[] { secret }, eventType); } diff --git a/src/Api/SecretsManager/Controllers/SecretsController.cs b/src/Api/SecretsManager/Controllers/SecretsController.cs index fa248095dcbd..e016cdf0bd09 100644 --- a/src/Api/SecretsManager/Controllers/SecretsController.cs +++ b/src/Api/SecretsManager/Controllers/SecretsController.cs @@ -177,7 +177,9 @@ public async Task UpdateSecretAsync([FromRoute] Guid id, [F throw new NotFoundException(); } - var originalSecret = secret; + var originalValue = secret.Value; + var valueRevisionDate = secret.RevisionDate; + var updatedSecret = updateRequest.ToSecret(secret); var authorizationResult = await _authorizationService.AuthorizeAsync(User, updatedSecret, SecretOperations.Update); if (!authorizationResult.Succeeded) @@ -229,8 +231,8 @@ public async Task UpdateSecretAsync([FromRoute] Guid id, [F var secretVersion = new SecretVersion { SecretId = id, - Value = originalSecret.Value, - VersionDate = originalSecret.CreationDate, + Value = originalValue, + VersionDate = valueRevisionDate, EditorServiceAccountId = editorServiceAccountId, EditorOrganizationUserId = editorOrganizationUserId }; From b253d02f77ac763bbefd36374d03a362f8d1fa15 Mon Sep 17 00:00:00 2001 From: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Date: Thu, 23 Apr 2026 11:13:40 -0400 Subject: [PATCH 3/4] removing future code improvement --- .../Controllers/SecretVersionsController.cs | 42 ++----------------- 1 file changed, 4 insertions(+), 38 deletions(-) diff --git a/src/Api/SecretsManager/Controllers/SecretVersionsController.cs b/src/Api/SecretsManager/Controllers/SecretVersionsController.cs index 1b1cd4d7924c..9d8a2d8a6484 100644 --- a/src/Api/SecretsManager/Controllers/SecretVersionsController.cs +++ b/src/Api/SecretsManager/Controllers/SecretVersionsController.cs @@ -22,8 +22,6 @@ public class SecretVersionsController : Controller private readonly ISecretRepository _secretRepository; private readonly IUserService _userService; private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IServiceAccountRepository _serviceAccountRepository; - private readonly IUserRepository _userRepository; private readonly IEventService _eventService; public SecretVersionsController( @@ -32,8 +30,6 @@ public SecretVersionsController( ISecretRepository secretRepository, IUserService userService, IOrganizationUserRepository organizationUserRepository, - IServiceAccountRepository serviceAccountRepository, - IUserRepository userRepository, IEventService eventService) { _currentContext = currentContext; @@ -41,8 +37,6 @@ public SecretVersionsController( _secretRepository = secretRepository; _userService = userService; _organizationUserRepository = organizationUserRepository; - _serviceAccountRepository = serviceAccountRepository; - _userRepository = userRepository; _eventService = eventService; } @@ -61,7 +55,7 @@ public async Task> GetVersionsBySe { // Already verified Secrets Manager access above var versionList = await _secretVersionRepository.GetManyBySecretIdAsync(secretId); - var responseList = await Task.WhenAll(versionList.Select(v => ResolveVersionWithEditorName(v, secret.OrganizationId))); + var responseList = versionList.Select(v => new SecretVersionResponseModel(v)).ToList(); return new ListResponseModel(responseList); } @@ -81,7 +75,7 @@ public async Task> GetVersionsBySe } var versions = await _secretVersionRepository.GetManyBySecretIdAsync(secretId); - var responses = await Task.WhenAll(versions.Select(v => ResolveVersionWithEditorName(v, secret.OrganizationId))); + var responses = versions.Select(v => new SecretVersionResponseModel(v)).ToList(); return new ListResponseModel(responses); } @@ -106,7 +100,7 @@ public async Task GetByIdAsync([FromRoute] Guid id) _currentContext.IdentityClientType == IdentityClientType.Organization) { // Already verified Secrets Manager access above - return await ResolveVersionWithEditorName(secretVersion, secret.OrganizationId); + return new SecretVersionResponseModel(secretVersion); } var userId = _userService.GetProperUserId(User); @@ -124,7 +118,7 @@ public async Task GetByIdAsync([FromRoute] Guid id) throw new NotFoundException(); } - return await ResolveVersionWithEditorName(secretVersion, secret.OrganizationId); + return new SecretVersionResponseModel(secretVersion); } [HttpPost("secret-versions/get-by-ids")] @@ -348,34 +342,6 @@ public async Task BulkDeleteAsync([FromBody] List ids) return Ok(); } - private async Task ResolveVersionWithEditorName(Bit.Core.SecretsManager.Entities.SecretVersion secretVersion, Guid organizationId) - { - var response = new SecretVersionResponseModel(secretVersion); - - if (secretVersion.EditorServiceAccountId.HasValue) - { - var serviceAccount = await _serviceAccountRepository.GetByIdAsync(secretVersion.EditorServiceAccountId.Value); - if (serviceAccount != null) - { - response.EditorName = serviceAccount.Name; - } - } - else if (secretVersion.EditorOrganizationUserId.HasValue) - { - var orgUser = await _organizationUserRepository.GetByIdAsync(secretVersion.EditorOrganizationUserId.Value); - if (orgUser?.UserId.HasValue == true) - { - var user = await _userRepository.GetByIdAsync(orgUser.UserId.Value); - if (user != null) - { - response.EditorName = user.Name; - } - } - } - - return response; - } - private async Task LogSecretsEventAsync(IEnumerable secrets, EventType eventType) { var userId = _userService.GetProperUserId(User)!.Value; From 5550385dfd1145f8f18a8730110ac448cb3151da Mon Sep 17 00:00:00 2001 From: cd-bitwarden <106776772+cd-bitwarden@users.noreply.github.com> Date: Thu, 23 Apr 2026 11:33:57 -0400 Subject: [PATCH 4/4] removing future improvement --- .../SecretsManager/Models/Response/SecretVersionResponseModel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Api/SecretsManager/Models/Response/SecretVersionResponseModel.cs b/src/Api/SecretsManager/Models/Response/SecretVersionResponseModel.cs index 468fe6457794..07b8e88f7e51 100644 --- a/src/Api/SecretsManager/Models/Response/SecretVersionResponseModel.cs +++ b/src/Api/SecretsManager/Models/Response/SecretVersionResponseModel.cs @@ -13,7 +13,6 @@ public class SecretVersionResponseModel : ResponseModel public DateTime VersionDate { get; set; } public Guid? EditorServiceAccountId { get; set; } public Guid? EditorOrganizationUserId { get; set; } - public string? EditorName { get; set; } public SecretVersionResponseModel() : base(_objectName) { }