1
0
mirror of https://github.com/bitwarden/server.git synced 2025-02-08 00:31:27 +01:00

Merge branch 'main' into km/userkey-rotation-v2

This commit is contained in:
Bernd Schoolmann 2025-01-30 16:01:14 +01:00 committed by GitHub
commit 8f9252decb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 140 additions and 20 deletions

View File

@ -309,7 +309,7 @@ public class OrganizationsController : Controller
[HttpPost]
[ValidateAntiForgeryToken]
[RequirePermission(Permission.Org_Delete)]
[RequirePermission(Permission.Org_RequestDelete)]
public async Task<IActionResult> DeleteInitiation(Guid id, OrganizationInitiateDeleteModel model)
{
if (!ModelState.IsValid)

View File

@ -311,10 +311,8 @@ public class OrganizationUsersController : Controller
throw new UnauthorizedAccessException();
}
var masterPasswordPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
var useMasterPasswordPolicy = masterPasswordPolicy != null &&
masterPasswordPolicy.Enabled &&
masterPasswordPolicy.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled;
var useMasterPasswordPolicy = await ShouldHandleResetPasswordAsync(orgId);
if (useMasterPasswordPolicy && string.IsNullOrWhiteSpace(model.ResetPasswordKey))
{
throw new BadRequestException(string.Empty, "Master Password reset is required, but not provided.");
@ -328,6 +326,23 @@ public class OrganizationUsersController : Controller
}
}
private async Task<bool> ShouldHandleResetPasswordAsync(Guid orgId)
{
var organizationAbility = await _applicationCacheService.GetOrganizationAbilityAsync(orgId);
if (organizationAbility is not { UsePolicies: true })
{
return false;
}
var masterPasswordPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
var useMasterPasswordPolicy = masterPasswordPolicy != null &&
masterPasswordPolicy.Enabled &&
masterPasswordPolicy.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled;
return useMasterPasswordPolicy;
}
[HttpPost("{id}/confirm")]
public async Task Confirm(string orgId, string id, [FromBody] OrganizationUserConfirmRequestModel model)
{

View File

@ -57,6 +57,7 @@ public class OrganizationResponseModel : ResponseModel
MaxAutoscaleSmServiceAccounts = organization.MaxAutoscaleSmServiceAccounts;
LimitCollectionCreation = organization.LimitCollectionCreation;
LimitCollectionDeletion = organization.LimitCollectionDeletion;
LimitItemDeletion = organization.LimitItemDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
UseRiskInsights = organization.UseRiskInsights;
}
@ -102,6 +103,7 @@ public class OrganizationResponseModel : ResponseModel
public int? MaxAutoscaleSmServiceAccounts { get; set; }
public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; }
public bool LimitItemDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; }
public bool UseRiskInsights { get; set; }
}

View File

@ -67,6 +67,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
AccessSecretsManager = organization.AccessSecretsManager;
LimitCollectionCreation = organization.LimitCollectionCreation;
LimitCollectionDeletion = organization.LimitCollectionDeletion;
LimitItemDeletion = organization.LimitItemDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
UserIsManagedByOrganization = organizationIdsManagingUser.Contains(organization.OrganizationId);
UseRiskInsights = organization.UseRiskInsights;
@ -128,6 +129,7 @@ public class ProfileOrganizationResponseModel : ResponseModel
public bool AccessSecretsManager { get; set; }
public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; }
public bool LimitItemDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; }
/// <summary>
/// Indicates if the organization manages the user.

View File

@ -47,6 +47,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier;
LimitCollectionCreation = organization.LimitCollectionCreation;
LimitCollectionDeletion = organization.LimitCollectionDeletion;
LimitItemDeletion = organization.LimitItemDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
UseRiskInsights = organization.UseRiskInsights;
}

View File

@ -1,6 +1,7 @@
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using Bit.Core.Auth.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Api;
namespace Bit.Api.Auth.Models.Response;
@ -17,6 +18,7 @@ public class AuthRequestResponseModel : ResponseModel
Id = authRequest.Id;
PublicKey = authRequest.PublicKey;
RequestDeviceTypeValue = authRequest.RequestDeviceType;
RequestDeviceType = authRequest.RequestDeviceType.GetType().GetMember(authRequest.RequestDeviceType.ToString())
.FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName();
RequestIpAddress = authRequest.RequestIpAddress;
@ -30,6 +32,7 @@ public class AuthRequestResponseModel : ResponseModel
public Guid Id { get; set; }
public string PublicKey { get; set; }
public DeviceType RequestDeviceTypeValue { get; set; }
public string RequestDeviceType { get; set; }
public string RequestIpAddress { get; set; }
public string Key { get; set; }

View File

@ -7,12 +7,14 @@ public class OrganizationCollectionManagementUpdateRequestModel
{
public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; }
public bool LimitItemDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; }
public virtual Organization ToOrganization(Organization existingOrganization, IFeatureService featureService)
{
existingOrganization.LimitCollectionCreation = LimitCollectionCreation;
existingOrganization.LimitCollectionDeletion = LimitCollectionDeletion;
existingOrganization.LimitItemDeletion = LimitItemDeletion;
existingOrganization.AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems;
return existingOrganization;
}

View File

@ -1097,7 +1097,7 @@ public class CiphersController : Controller
[HttpDelete("{id}/attachment/{attachmentId}")]
[HttpPost("{id}/attachment/{attachmentId}/delete")]
public async Task DeleteAttachment(Guid id, string attachmentId)
public async Task<DeleteAttachmentResponseData> DeleteAttachment(Guid id, string attachmentId)
{
var userId = _userService.GetProperUserId(User).Value;
var cipher = await GetByIdAsync(id, userId);
@ -1106,7 +1106,7 @@ public class CiphersController : Controller
throw new NotFoundException();
}
await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, false);
return await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, false);
}
[HttpDelete("{id}/attachment/{attachmentId}/admin")]

View File

@ -23,6 +23,7 @@ public class OrganizationAbility
UsePolicies = organization.UsePolicies;
LimitCollectionCreation = organization.LimitCollectionCreation;
LimitCollectionDeletion = organization.LimitCollectionDeletion;
LimitItemDeletion = organization.LimitItemDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
UseRiskInsights = organization.UseRiskInsights;
}
@ -41,6 +42,7 @@ public class OrganizationAbility
public bool UsePolicies { get; set; }
public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; }
public bool LimitItemDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; }
public bool UseRiskInsights { get; set; }
}

View File

@ -56,6 +56,7 @@ public class OrganizationUserOrganizationDetails
public int? SmServiceAccounts { get; set; }
public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; }
public bool LimitItemDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; }
public bool UseRiskInsights { get; set; }
}

View File

@ -146,6 +146,7 @@ public class SelfHostedOrganizationDetails : Organization
OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling,
LimitCollectionCreation = LimitCollectionCreation,
LimitCollectionDeletion = LimitCollectionDeletion,
LimitItemDeletion = LimitItemDeletion,
AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems,
Status = Status
};

View File

@ -42,6 +42,7 @@ public class ProviderUserOrganizationDetails
public PlanType PlanType { get; set; }
public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; }
public bool LimitItemDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; }
public bool UseRiskInsights { get; set; }
public ProviderType ProviderType { get; set; }

View File

@ -168,6 +168,9 @@ public static class FeatureFlagKeys
public const string EnablePasswordManagerSyncAndroid = "enable-password-manager-sync-android";
public const string EnablePasswordManagerSynciOS = "enable-password-manager-sync-ios";
public const string AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner";
public const string SingleTapPasskeyCreation = "single-tap-passkey-creation";
public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication";
public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications";
public static List<string> GetAllKeys()
{

View File

@ -62,4 +62,5 @@ public class OrganizationCollectionManagementPushNotification
public Guid OrganizationId { get; init; }
public bool LimitCollectionCreation { get; init; }
public bool LimitCollectionDeletion { get; init; }
public bool LimitItemDeletion { get; init; }
}

View File

@ -246,7 +246,8 @@ public class NotificationHubPushNotificationService : IPushNotificationService
{
OrganizationId = organization.Id,
LimitCollectionCreation = organization.LimitCollectionCreation,
LimitCollectionDeletion = organization.LimitCollectionDeletion
LimitCollectionDeletion = organization.LimitCollectionDeletion,
LimitItemDeletion = organization.LimitItemDeletion
},
false
);

View File

@ -239,6 +239,7 @@ public class AzureQueuePushNotificationService : IPushNotificationService
{
OrganizationId = organization.Id,
LimitCollectionCreation = organization.LimitCollectionCreation,
LimitCollectionDeletion = organization.LimitCollectionDeletion
LimitCollectionDeletion = organization.LimitCollectionDeletion,
LimitItemDeletion = organization.LimitItemDeletion
}, false);
}

View File

@ -248,6 +248,7 @@ public class NotificationsApiPushNotificationService : BaseIdentityClientService
{
OrganizationId = organization.Id,
LimitCollectionCreation = organization.LimitCollectionCreation,
LimitCollectionDeletion = organization.LimitCollectionDeletion
LimitCollectionDeletion = organization.LimitCollectionDeletion,
LimitItemDeletion = organization.LimitItemDeletion
}, false);
}

View File

@ -273,7 +273,8 @@ public class RelayPushNotificationService : BaseIdentityClientService, IPushNoti
{
OrganizationId = organization.Id,
LimitCollectionCreation = organization.LimitCollectionCreation,
LimitCollectionDeletion = organization.LimitCollectionDeletion
LimitCollectionDeletion = organization.LimitCollectionDeletion,
LimitItemDeletion = organization.LimitItemDeletion
},
false
);

View File

@ -0,0 +1,13 @@
using Bit.Core.Vault.Entities;
namespace Bit.Core.Vault.Models.Data;
public class DeleteAttachmentResponseData
{
public Cipher Cipher { get; set; }
public DeleteAttachmentResponseData(Cipher cipher)
{
Cipher = cipher;
}
}

View File

@ -17,7 +17,7 @@ public interface ICipherService
string attachmentId, Guid organizationShareId);
Task DeleteAsync(Cipher cipher, Guid deletingUserId, bool orgAdmin = false);
Task DeleteManyAsync(IEnumerable<Guid> cipherIds, Guid deletingUserId, Guid? organizationId = null, bool orgAdmin = false);
Task DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false);
Task<DeleteAttachmentResponseData> DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId, bool orgAdmin = false);
Task PurgeAsync(Guid organizationId);
Task MoveManyAsync(IEnumerable<Guid> cipherIds, Guid? destinationFolderId, Guid movingUserId);
Task SaveFolderAsync(Folder folder);

View File

@ -210,6 +210,11 @@ public class CipherService : ICipherService
AttachmentData = JsonSerializer.Serialize(data)
});
cipher.AddAttachment(attachmentId, data);
// Update the revision date when an attachment is added
cipher.RevisionDate = DateTime.UtcNow;
await _cipherRepository.ReplaceAsync((CipherDetails)cipher);
await _pushService.PushSyncCipherUpdateAsync(cipher, null);
return (attachmentId, uploadUrl);
@ -259,6 +264,10 @@ public class CipherService : ICipherService
throw;
}
// Update the revision date when an attachment is added
cipher.RevisionDate = DateTime.UtcNow;
await _cipherRepository.ReplaceAsync((CipherDetails)cipher);
// push
await _pushService.PushSyncCipherUpdateAsync(cipher, null);
}
@ -441,7 +450,7 @@ public class CipherService : ICipherService
await _pushService.PushSyncCiphersAsync(deletingUserId);
}
public async Task DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId,
public async Task<DeleteAttachmentResponseData> DeleteAttachmentAsync(Cipher cipher, string attachmentId, Guid deletingUserId,
bool orgAdmin = false)
{
if (!orgAdmin && !(await UserCanEditAsync(cipher, deletingUserId)))
@ -454,7 +463,7 @@ public class CipherService : ICipherService
throw new NotFoundException();
}
await DeleteAttachmentAsync(cipher, cipher.GetAttachments()[attachmentId]);
return await DeleteAttachmentAsync(cipher, cipher.GetAttachments()[attachmentId]);
}
public async Task PurgeAsync(Guid organizationId)
@ -834,11 +843,11 @@ public class CipherService : ICipherService
}
}
private async Task DeleteAttachmentAsync(Cipher cipher, CipherAttachment.MetaData attachmentData)
private async Task<DeleteAttachmentResponseData> DeleteAttachmentAsync(Cipher cipher, CipherAttachment.MetaData attachmentData)
{
if (attachmentData == null || string.IsNullOrWhiteSpace(attachmentData.AttachmentId))
{
return;
return null;
}
await _cipherRepository.DeleteAttachmentAsync(cipher.Id, attachmentData.AttachmentId);
@ -846,8 +855,14 @@ public class CipherService : ICipherService
await _attachmentStorageService.DeleteAttachmentAsync(cipher.Id, attachmentData);
await _eventService.LogCipherEventAsync(cipher, Bit.Core.Enums.EventType.Cipher_AttachmentDeleted);
// Update the revision date when an attachment is deleted
cipher.RevisionDate = DateTime.UtcNow;
await _cipherRepository.ReplaceAsync((CipherDetails)cipher);
// push
await _pushService.PushSyncCipherUpdateAsync(cipher, null);
return new DeleteAttachmentResponseData(cipher);
}
private async Task ValidateCipherEditForAttachmentAsync(Cipher cipher, Guid savingUserId, bool orgAdmin,

View File

@ -20,5 +20,6 @@ public static class VaultServiceCollectionExtensions
services.AddScoped<IGetTaskDetailsForUserQuery, GetTaskDetailsForUserQuery>();
services.AddScoped<IMarkTaskAsCompleteCommand, MarkTaskAsCompletedCommand>();
services.AddScoped<IGetCipherPermissionsForUserQuery, GetCipherPermissionsForUserQuery>();
services.AddScoped<IGetTasksForOrganizationQuery, GetTasksForOrganizationQuery>();
}
}

View File

@ -101,6 +101,7 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
UsePolicies = e.UsePolicies,
LimitCollectionCreation = e.LimitCollectionCreation,
LimitCollectionDeletion = e.LimitCollectionDeletion,
LimitItemDeletion = e.LimitItemDeletion,
AllowAdminAccessToAllCollectionItems = e.AllowAdminAccessToAllCollectionItems,
UseRiskInsights = e.UseRiskInsights
}).ToListAsync();

View File

@ -68,6 +68,7 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery<OrganizationU
SmServiceAccounts = o.SmServiceAccounts,
LimitCollectionCreation = o.LimitCollectionCreation,
LimitCollectionDeletion = o.LimitCollectionDeletion,
LimitItemDeletion = o.LimitItemDeletion,
AllowAdminAccessToAllCollectionItems = o.AllowAdminAccessToAllCollectionItems,
UseRiskInsights = o.UseRiskInsights,
};

View File

@ -46,6 +46,7 @@ public class ProviderUserOrganizationDetailsViewQuery : IQuery<ProviderUserOrgan
PlanType = x.o.PlanType,
LimitCollectionCreation = x.o.LimitCollectionCreation,
LimitCollectionDeletion = x.o.LimitCollectionDeletion,
LimitItemDeletion = x.o.LimitItemDeletion,
AllowAdminAccessToAllCollectionItems = x.o.AllowAdminAccessToAllCollectionItems,
UseRiskInsights = x.o.UseRiskInsights,
ProviderType = x.p.Type

View File

@ -123,24 +123,74 @@ public class OrganizationUsersControllerTests
[Theory]
[BitAutoData]
public async Task Accept_RequireMasterPasswordReset(Guid orgId, Guid orgUserId,
public async Task Accept_WhenOrganizationUsePoliciesIsEnabledAndResetPolicyIsEnabled_ShouldHandleResetPassword(Guid orgId, Guid orgUserId,
OrganizationUserAcceptRequestModel model, User user, SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
applicationCacheService.GetOrganizationAbilityAsync(orgId).Returns(new OrganizationAbility { UsePolicies = true });
var policy = new Policy
{
Enabled = true,
Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, }),
};
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
sutProvider.GetDependency<IPolicyRepository>().GetByOrganizationIdTypeAsync(orgId,
var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
policyRepository.GetByOrganizationIdTypeAsync(orgId,
PolicyType.ResetPassword).Returns(policy);
// Act
await sutProvider.Sut.Accept(orgId, orgUserId, model);
// Assert
await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(1)
.AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, sutProvider.GetDependency<IUserService>());
.AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, userService);
await sutProvider.GetDependency<IOrganizationService>().Received(1)
.UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id);
await userService.Received(1).GetUserByPrincipalAsync(default);
await applicationCacheService.Received(1).GetOrganizationAbilityAsync(orgId);
await policyRepository.Received(1).GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
}
[Theory]
[BitAutoData]
public async Task Accept_WhenOrganizationUsePoliciesIsDisabled_ShouldNotHandleResetPassword(Guid orgId, Guid orgUserId,
OrganizationUserAcceptRequestModel model, User user, SutProvider<OrganizationUsersController> sutProvider)
{
// Arrange
var applicationCacheService = sutProvider.GetDependency<IApplicationCacheService>();
applicationCacheService.GetOrganizationAbilityAsync(orgId).Returns(new OrganizationAbility { UsePolicies = false });
var policy = new Policy
{
Enabled = true,
Data = CoreHelpers.ClassToJsonData(new ResetPasswordDataModel { AutoEnrollEnabled = true, }),
};
var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
var policyRepository = sutProvider.GetDependency<IPolicyRepository>();
policyRepository.GetByOrganizationIdTypeAsync(orgId,
PolicyType.ResetPassword).Returns(policy);
// Act
await sutProvider.Sut.Accept(orgId, orgUserId, model);
// Assert
await userService.Received(1).GetUserByPrincipalAsync(default);
await sutProvider.GetDependency<IAcceptOrgUserCommand>().Received(1)
.AcceptOrgUserByEmailTokenAsync(orgUserId, user, model.Token, userService);
await sutProvider.GetDependency<IOrganizationService>().Received(0)
.UpdateUserResetPasswordEnrollmentAsync(orgId, user.Id, model.ResetPasswordKey, user.Id);
await policyRepository.Received(0).GetByOrganizationIdTypeAsync(orgId, PolicyType.ResetPassword);
await applicationCacheService.Received(1).GetOrganizationAbilityAsync(orgId);
}
[Theory]