mirror of
https://github.com/bitwarden/server
synced 2026-02-20 03:13:35 +00:00
[PM-26378] Auto confirm events (#7017)
* implement auto confirm push notification * fix test * fix test * simplify LINQ * add event logging for auto confirm * fix test
This commit is contained in:
@@ -61,6 +61,7 @@ public class OrganizationsController : Controller
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IResendOrganizationInviteCommand _resendOrganizationInviteCommand;
|
||||
private readonly IOrganizationBillingService _organizationBillingService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IAutomaticUserConfirmationOrganizationPolicyComplianceValidator _automaticUserConfirmationOrganizationPolicyComplianceValidator;
|
||||
|
||||
public OrganizationsController(
|
||||
@@ -88,6 +89,7 @@ public class OrganizationsController : Controller
|
||||
IPricingClient pricingClient,
|
||||
IResendOrganizationInviteCommand resendOrganizationInviteCommand,
|
||||
IOrganizationBillingService organizationBillingService,
|
||||
IEventService eventService,
|
||||
IAutomaticUserConfirmationOrganizationPolicyComplianceValidator automaticUserConfirmationOrganizationPolicyComplianceValidator)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
@@ -114,6 +116,7 @@ public class OrganizationsController : Controller
|
||||
_pricingClient = pricingClient;
|
||||
_resendOrganizationInviteCommand = resendOrganizationInviteCommand;
|
||||
_organizationBillingService = organizationBillingService;
|
||||
_eventService = eventService;
|
||||
_automaticUserConfirmationOrganizationPolicyComplianceValidator = automaticUserConfirmationOrganizationPolicyComplianceValidator;
|
||||
}
|
||||
|
||||
@@ -283,6 +286,8 @@ public class OrganizationsController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
var previousUseAutomaticUserConfirmation = organization.UseAutomaticUserConfirmation;
|
||||
|
||||
UpdateOrganization(organization, model);
|
||||
var plan = await _pricingClient.GetPlanOrThrow(organization.PlanType);
|
||||
if (organization.UseSecretsManager && !plan.SupportsSecretsManager)
|
||||
@@ -304,6 +309,14 @@ public class OrganizationsController : Controller
|
||||
|
||||
await _organizationRepository.ReplaceAsync(organization);
|
||||
|
||||
if (previousUseAutomaticUserConfirmation != organization.UseAutomaticUserConfirmation)
|
||||
{
|
||||
var eventType = organization.UseAutomaticUserConfirmation
|
||||
? EventType.Organization_AutoConfirmEnabled_Portal
|
||||
: EventType.Organization_AutoConfirmDisabled_Portal;
|
||||
await _eventService.LogOrganizationEventAsync(organization, eventType, EventSystemUser.BitwardenPortal);
|
||||
}
|
||||
|
||||
await _applicationCacheService.UpsertOrganizationAbilityAsync(organization);
|
||||
|
||||
// Sync name/email changes to Stripe
|
||||
|
||||
@@ -28,6 +28,7 @@ public interface IEventService
|
||||
Task LogOrganizationUserEventsAsync<T>(IEnumerable<(T, EventType, DateTime?)> events) where T : IOrganizationUser;
|
||||
Task LogOrganizationUserEventsAsync<T>(IEnumerable<(T, EventType, EventSystemUser, DateTime?)> events) where T : IOrganizationUser;
|
||||
Task LogOrganizationEventAsync(Organization organization, EventType type, DateTime? date = null);
|
||||
Task LogOrganizationEventAsync(Organization organization, EventType type, EventSystemUser systemUser, DateTime? date = null);
|
||||
Task LogProviderUserEventAsync(ProviderUser providerUser, EventType type, DateTime? date = null);
|
||||
Task LogProviderUsersEventAsync(IEnumerable<(ProviderUser, EventType, DateTime?)> events);
|
||||
Task LogProviderOrganizationEventAsync(ProviderOrganization providerOrganization, EventType type, DateTime? date = null);
|
||||
|
||||
@@ -7,4 +7,5 @@ public enum EventSystemUser : byte
|
||||
DomainVerification = 2,
|
||||
PublicApi = 3,
|
||||
TwoFactorDisabled = 4,
|
||||
BitwardenPortal = 5,
|
||||
}
|
||||
|
||||
@@ -84,6 +84,10 @@ public enum EventType : int
|
||||
Organization_CollectionManagement_AllowAdminAccessToAllCollectionItemsDisabled = 1617,
|
||||
Organization_ItemOrganization_Accepted = 1618,
|
||||
Organization_ItemOrganization_Declined = 1619,
|
||||
Organization_AutoConfirmEnabled_Admin = 1620,
|
||||
Organization_AutoConfirmDisabled_Admin = 1621,
|
||||
Organization_AutoConfirmEnabled_Portal = 1622,
|
||||
Organization_AutoConfirmDisabled_Portal = 1623,
|
||||
|
||||
Policy_Updated = 1700,
|
||||
|
||||
|
||||
@@ -309,6 +309,25 @@ public class EventService : IEventService
|
||||
await _eventWriteService.CreateAsync(e);
|
||||
}
|
||||
|
||||
public async Task LogOrganizationEventAsync(Organization organization, EventType type, EventSystemUser systemUser, DateTime? date = null)
|
||||
{
|
||||
if (!organization.Enabled || !organization.UseEvents)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var EventMessage = new EventMessage
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
ProviderId = await GetProviderIdAsync(organization.Id),
|
||||
Type = type,
|
||||
SystemUser = systemUser,
|
||||
Date = date.GetValueOrDefault(DateTime.UtcNow),
|
||||
DeviceType = DeviceType.Server
|
||||
};
|
||||
await _eventWriteService.CreateAsync(EventMessage);
|
||||
}
|
||||
|
||||
public async Task LogProviderUserEventAsync(ProviderUser providerUser, EventType type, DateTime? date = null)
|
||||
{
|
||||
await LogProviderUsersEventAsync(new[] { (providerUser, type, date) });
|
||||
|
||||
@@ -57,6 +57,11 @@ public class NoopEventService : IEventService
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task LogOrganizationEventAsync(Organization organization, EventType type, EventSystemUser systemUser, DateTime? date = null)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task LogProviderUserEventAsync(ProviderUser providerUser, EventType type, DateTime? date = null)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
|
||||
@@ -131,6 +131,8 @@ public class CollectController : Controller
|
||||
break;
|
||||
|
||||
case EventType.Organization_ClientExportedVault:
|
||||
case EventType.Organization_AutoConfirmEnabled_Admin:
|
||||
case EventType.Organization_AutoConfirmDisabled_Admin:
|
||||
if (!eventModel.OrganizationId.HasValue)
|
||||
{
|
||||
continue;
|
||||
|
||||
@@ -11,6 +11,7 @@ using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -464,5 +465,109 @@ public class OrganizationsControllerTests
|
||||
.IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>());
|
||||
}
|
||||
|
||||
[BitAutoData]
|
||||
[SutProviderCustomize]
|
||||
[Theory]
|
||||
public async Task Edit_UseAutomaticUserConfirmation_EnabledByPortal_LogsEvent(
|
||||
Organization organization,
|
||||
SutProvider<OrganizationsController> sutProvider)
|
||||
{
|
||||
var update = new OrganizationEditModel
|
||||
{
|
||||
PlanType = PlanType.TeamsMonthly,
|
||||
UseAutomaticUserConfirmation = true
|
||||
};
|
||||
|
||||
organization.UseAutomaticUserConfirmation = false;
|
||||
organization.Enabled = true;
|
||||
organization.UseEvents = true;
|
||||
|
||||
sutProvider.GetDependency<IAccessControlService>()
|
||||
.UserHasPermission(Permission.Org_Plan_Edit)
|
||||
.Returns(true);
|
||||
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
|
||||
|
||||
var request = new AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest(organization.Id);
|
||||
sutProvider.GetDependency<IAutomaticUserConfirmationOrganizationPolicyComplianceValidator>()
|
||||
.IsOrganizationCompliantAsync(Arg.Any<AutomaticUserConfirmationOrganizationPolicyComplianceValidatorRequest>())
|
||||
.Returns(Valid(request));
|
||||
|
||||
_ = await sutProvider.Sut.Edit(organization.Id, update);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>().Received(1)
|
||||
.LogOrganizationEventAsync(
|
||||
Arg.Is<Organization>(o => o.Id == organization.Id),
|
||||
EventType.Organization_AutoConfirmEnabled_Portal,
|
||||
EventSystemUser.BitwardenPortal);
|
||||
}
|
||||
|
||||
[BitAutoData]
|
||||
[SutProviderCustomize]
|
||||
[Theory]
|
||||
public async Task Edit_UseAutomaticUserConfirmation_DisabledByPortal_LogsEvent(
|
||||
Organization organization,
|
||||
SutProvider<OrganizationsController> sutProvider)
|
||||
{
|
||||
var update = new OrganizationEditModel
|
||||
{
|
||||
PlanType = PlanType.TeamsMonthly,
|
||||
UseAutomaticUserConfirmation = false
|
||||
};
|
||||
|
||||
organization.UseAutomaticUserConfirmation = true;
|
||||
organization.Enabled = true;
|
||||
organization.UseEvents = true;
|
||||
|
||||
sutProvider.GetDependency<IAccessControlService>()
|
||||
.UserHasPermission(Permission.Org_Plan_Edit)
|
||||
.Returns(true);
|
||||
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
|
||||
|
||||
_ = await sutProvider.Sut.Edit(organization.Id, update);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>().Received(1)
|
||||
.LogOrganizationEventAsync(
|
||||
Arg.Is<Organization>(o => o.Id == organization.Id),
|
||||
EventType.Organization_AutoConfirmDisabled_Portal,
|
||||
EventSystemUser.BitwardenPortal);
|
||||
}
|
||||
|
||||
[BitAutoData]
|
||||
[SutProviderCustomize]
|
||||
[Theory]
|
||||
public async Task Edit_UseAutomaticUserConfirmation_NoChange_DoesNotLogEvent(
|
||||
Organization organization,
|
||||
SutProvider<OrganizationsController> sutProvider)
|
||||
{
|
||||
var update = new OrganizationEditModel
|
||||
{
|
||||
PlanType = PlanType.TeamsMonthly,
|
||||
UseAutomaticUserConfirmation = true
|
||||
};
|
||||
|
||||
organization.UseAutomaticUserConfirmation = true;
|
||||
organization.Enabled = true;
|
||||
organization.UseEvents = true;
|
||||
|
||||
sutProvider.GetDependency<IAccessControlService>()
|
||||
.UserHasPermission(Permission.Org_Plan_Edit)
|
||||
.Returns(true);
|
||||
|
||||
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
|
||||
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
|
||||
|
||||
_ = await sutProvider.Sut.Edit(organization.Id, update);
|
||||
|
||||
await sutProvider.GetDependency<IEventService>().DidNotReceive()
|
||||
.LogOrganizationEventAsync(
|
||||
Arg.Any<Organization>(),
|
||||
Arg.Any<EventType>(),
|
||||
Arg.Any<EventSystemUser>());
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -116,6 +116,26 @@ public class EventServiceTests
|
||||
e.InstallationId == installationId));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task LogOrganizationEvent_WithEventSystemUser_LogsRequiredInfo(Organization organization, EventType eventType,
|
||||
EventSystemUser eventSystemUser, DateTime date, Guid providerId, SutProvider<EventService> sutProvider)
|
||||
{
|
||||
organization.Enabled = true;
|
||||
organization.UseEvents = true;
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().ProviderIdForOrg(Arg.Any<Guid>()).Returns(providerId);
|
||||
|
||||
await sutProvider.Sut.LogOrganizationEventAsync(organization, eventType, eventSystemUser, date);
|
||||
|
||||
await sutProvider.GetDependency<IEventWriteService>().Received(1).CreateAsync(Arg.Is<IEvent>(e =>
|
||||
e.OrganizationId == organization.Id &&
|
||||
e.Type == eventType &&
|
||||
e.SystemUser == eventSystemUser &&
|
||||
e.DeviceType == DeviceType.Server &&
|
||||
e.Date == date &&
|
||||
e.ProviderId == providerId));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task LogOrganizationUserEvent_LogsRequiredInfo(OrganizationUser orgUser, EventType eventType, DateTime date,
|
||||
Guid actingUserId, Guid providerId, string ipAddress, DeviceType deviceType, SutProvider<EventService> sutProvider)
|
||||
|
||||
@@ -743,4 +743,80 @@ public class CollectControllerTests
|
||||
Arg.Is<IEnumerable<Tuple<Cipher, EventType, DateTime?>>>(tuples => tuples.Count() == 50)
|
||||
);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(EventType.Organization_AutoConfirmEnabled_Admin)]
|
||||
[BitAutoData(EventType.Organization_AutoConfirmDisabled_Admin)]
|
||||
public async Task Post_OrganizationAutoConfirmAdmin_WithValidOrg_LogsOrgEvent(
|
||||
EventType eventType, Guid userId, Guid orgId, Organization organization)
|
||||
{
|
||||
_currentContext.UserId.Returns(userId);
|
||||
organization.Id = orgId;
|
||||
_organizationRepository.GetByIdAsync(orgId).Returns(organization);
|
||||
var eventDate = DateTime.UtcNow;
|
||||
var events = new List<EventModel>
|
||||
{
|
||||
new EventModel
|
||||
{
|
||||
Type = eventType,
|
||||
OrganizationId = orgId,
|
||||
Date = eventDate
|
||||
}
|
||||
};
|
||||
|
||||
var result = await _sut.Post(events);
|
||||
|
||||
Assert.IsType<OkResult>(result);
|
||||
await _organizationRepository.Received(1).GetByIdAsync(orgId);
|
||||
await _eventService.Received(1).LogOrganizationEventAsync(organization, eventType, eventDate);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(EventType.Organization_AutoConfirmEnabled_Admin)]
|
||||
[BitAutoData(EventType.Organization_AutoConfirmDisabled_Admin)]
|
||||
public async Task Post_OrganizationAutoConfirmAdmin_WithoutOrgId_SkipsEvent(
|
||||
EventType eventType, Guid userId)
|
||||
{
|
||||
_currentContext.UserId.Returns(userId);
|
||||
var events = new List<EventModel>
|
||||
{
|
||||
new EventModel
|
||||
{
|
||||
Type = eventType,
|
||||
OrganizationId = null,
|
||||
Date = DateTime.UtcNow
|
||||
}
|
||||
};
|
||||
|
||||
var result = await _sut.Post(events);
|
||||
|
||||
Assert.IsType<OkResult>(result);
|
||||
await _organizationRepository.DidNotReceiveWithAnyArgs().GetByIdAsync(default);
|
||||
await _eventService.DidNotReceiveWithAnyArgs().LogOrganizationEventAsync(default, default, default);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(EventType.Organization_AutoConfirmEnabled_Admin)]
|
||||
[BitAutoData(EventType.Organization_AutoConfirmDisabled_Admin)]
|
||||
public async Task Post_OrganizationAutoConfirmAdmin_WithNullOrg_SkipsEvent(
|
||||
EventType eventType, Guid userId, Guid orgId)
|
||||
{
|
||||
_currentContext.UserId.Returns(userId);
|
||||
_organizationRepository.GetByIdAsync(orgId).Returns((Organization)null);
|
||||
var events = new List<EventModel>
|
||||
{
|
||||
new EventModel
|
||||
{
|
||||
Type = eventType,
|
||||
OrganizationId = orgId,
|
||||
Date = DateTime.UtcNow
|
||||
}
|
||||
};
|
||||
|
||||
var result = await _sut.Post(events);
|
||||
|
||||
Assert.IsType<OkResult>(result);
|
||||
await _organizationRepository.Received(1).GetByIdAsync(orgId);
|
||||
await _eventService.DidNotReceiveWithAnyArgs().LogOrganizationEventAsync(default, default, default);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user