1
0
mirror of https://github.com/bitwarden/server synced 2025-12-06 00:03:34 +00:00

Add template properties for Datadog (#6528)

* Add template properites for Datadog

* Add test and implementation for including User and ActingUser when only the Type is referenced

* Refactored database calls to fetch the user details in a single DB call

* Refactor to use a dedicated stored procedure for Dapper

* Remove TOP 1 from stored procedure

* Accept Claude's optimization of SingleOrDefaultAsync to unify Dapper/EF

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>

* Revert earlier change and add TOP 1 back into stored procedure

* Change go to GO

* Revert back to version that assumes uniqueness, remove TOP 1

---------

Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
This commit is contained in:
Brant DeBow
2025-11-24 10:30:45 -05:00
committed by GitHub
parent fcc879bd23
commit 9573cab37e
12 changed files with 259 additions and 40 deletions

View File

@@ -2,8 +2,8 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Entities;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
@@ -35,7 +35,7 @@ public class IntegrationTemplateContextTests
}
[Theory, BitAutoData]
public void UserName_WhenUserIsSet_ReturnsName(EventMessage eventMessage, User user)
public void UserName_WhenUserIsSet_ReturnsName(EventMessage eventMessage, OrganizationUserUserDetails user)
{
var sut = new IntegrationTemplateContext(eventMessage) { User = user };
@@ -51,7 +51,7 @@ public class IntegrationTemplateContextTests
}
[Theory, BitAutoData]
public void UserEmail_WhenUserIsSet_ReturnsEmail(EventMessage eventMessage, User user)
public void UserEmail_WhenUserIsSet_ReturnsEmail(EventMessage eventMessage, OrganizationUserUserDetails user)
{
var sut = new IntegrationTemplateContext(eventMessage) { User = user };
@@ -67,7 +67,23 @@ public class IntegrationTemplateContextTests
}
[Theory, BitAutoData]
public void ActingUserName_WhenActingUserIsSet_ReturnsName(EventMessage eventMessage, User actingUser)
public void UserType_WhenUserIsSet_ReturnsType(EventMessage eventMessage, OrganizationUserUserDetails user)
{
var sut = new IntegrationTemplateContext(eventMessage) { User = user };
Assert.Equal(user.Type, sut.UserType);
}
[Theory, BitAutoData]
public void UserType_WhenUserIsNull_ReturnsNull(EventMessage eventMessage)
{
var sut = new IntegrationTemplateContext(eventMessage) { User = null };
Assert.Null(sut.UserType);
}
[Theory, BitAutoData]
public void ActingUserName_WhenActingUserIsSet_ReturnsName(EventMessage eventMessage, OrganizationUserUserDetails actingUser)
{
var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = actingUser };
@@ -83,7 +99,7 @@ public class IntegrationTemplateContextTests
}
[Theory, BitAutoData]
public void ActingUserEmail_WhenActingUserIsSet_ReturnsEmail(EventMessage eventMessage, User actingUser)
public void ActingUserEmail_WhenActingUserIsSet_ReturnsEmail(EventMessage eventMessage, OrganizationUserUserDetails actingUser)
{
var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = actingUser };
@@ -98,6 +114,22 @@ public class IntegrationTemplateContextTests
Assert.Null(sut.ActingUserEmail);
}
[Theory, BitAutoData]
public void ActingUserType_WhenActingUserIsSet_ReturnsType(EventMessage eventMessage, OrganizationUserUserDetails actingUser)
{
var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = actingUser };
Assert.Equal(actingUser.Type, sut.ActingUserType);
}
[Theory, BitAutoData]
public void ActingUserType_WhenActingUserIsNull_ReturnsNull(EventMessage eventMessage)
{
var sut = new IntegrationTemplateContext(eventMessage) { ActingUser = null };
Assert.Null(sut.ActingUserType);
}
[Theory, BitAutoData]
public void OrganizationName_WhenOrganizationIsSet_ReturnsDisplayName(EventMessage eventMessage, Organization organization)
{
@@ -113,4 +145,20 @@ public class IntegrationTemplateContextTests
Assert.Null(sut.OrganizationName);
}
[Theory, BitAutoData]
public void GroupName_WhenGroupIsSet_ReturnsName(EventMessage eventMessage, Group group)
{
var sut = new IntegrationTemplateContext(eventMessage) { Group = group };
Assert.Equal(group.Name, sut.GroupName);
}
[Theory, BitAutoData]
public void GroupName_WhenGroupIsNull_ReturnsNull(EventMessage eventMessage)
{
var sut = new IntegrationTemplateContext(eventMessage) { Group = null };
Assert.Null(sut.GroupName);
}
}

View File

@@ -1,10 +1,11 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Entities;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
@@ -20,9 +21,11 @@ namespace Bit.Core.Test.Services;
public class EventIntegrationHandlerTests
{
private const string _templateBase = "Date: #Date#, Type: #Type#, UserId: #UserId#";
private const string _templateWithGroup = "Group: #GroupName#";
private const string _templateWithOrganization = "Org: #OrganizationName#";
private const string _templateWithUser = "#UserName#, #UserEmail#";
private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#";
private const string _templateWithUser = "#UserName#, #UserEmail#, #UserType#";
private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#, #ActingUserType#";
private static readonly Guid _groupId = Guid.NewGuid();
private static readonly Guid _organizationId = Guid.NewGuid();
private static readonly Uri _uri = new Uri("https://localhost");
private static readonly Uri _uri2 = new Uri("https://example.com");
@@ -45,7 +48,7 @@ public class EventIntegrationHandlerTests
.Create();
}
private static IntegrationMessage<WebhookIntegrationConfigurationDetails> expectedMessage(string template)
private static IntegrationMessage<WebhookIntegrationConfigurationDetails> ExpectedMessage(string template)
{
return new IntegrationMessage<WebhookIntegrationConfigurationDetails>()
{
@@ -105,7 +108,7 @@ public class EventIntegrationHandlerTests
config.Configuration = null;
config.IntegrationConfiguration = JsonSerializer.Serialize(new { Uri = _uri });
config.Template = _templateBase;
config.Filters = JsonSerializer.Serialize(new IntegrationFilterGroup() { });
config.Filters = JsonSerializer.Serialize(new IntegrationFilterGroup());
return [config];
}
@@ -138,15 +141,16 @@ public class EventIntegrationHandlerTests
await sutProvider.Sut.HandleEventAsync(eventMessage);
var expectedMessage = EventIntegrationHandlerTests.expectedMessage(
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
);
Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
}
[Theory, BitAutoData]
@@ -157,7 +161,7 @@ public class EventIntegrationHandlerTests
await sutProvider.Sut.HandleEventAsync(eventMessage);
var expectedMessage = EventIntegrationHandlerTests.expectedMessage(
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
);
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
@@ -167,29 +171,56 @@ public class EventIntegrationHandlerTests
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
}
[Theory, BitAutoData]
public async Task HandleEventAsync_ActingUserTemplate_LoadsUserFromRepository(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));
var user = Substitute.For<User>();
var user = Substitute.For<OrganizationUserUserDetails>();
user.Email = "test@example.com";
user.Name = "Test";
eventMessage.OrganizationId = _organizationId;
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(user);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>()).Returns(user);
await sutProvider.Sut.HandleEventAsync(eventMessage);
var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"{user.Name}, {user.Email}");
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"{user.Name}, {user.Email}, {user.Type}");
Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IUserRepository>().Received(1).GetByIdAsync(eventMessage.ActingUserId ?? Guid.Empty);
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), eventMessage.ActingUserId ?? Guid.Empty);
}
[Theory, BitAutoData]
public async Task HandleEventAsync_GroupTemplate_LoadsGroupFromRepository(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithGroup));
var group = Substitute.For<Group>();
group.Name = "Test";
eventMessage.GroupId = _groupId;
eventMessage.OrganizationId = _organizationId;
sutProvider.GetDependency<IGroupRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(group);
await sutProvider.Sut.HandleEventAsync(eventMessage);
Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"Group: {group.Name}");
Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
await sutProvider.GetDependency<IGroupRepository>().Received(1).GetByIdAsync(eventMessage.GroupId ?? Guid.Empty);
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
}
[Theory, BitAutoData]
@@ -205,34 +236,37 @@ public class EventIntegrationHandlerTests
Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"Org: {organization.Name}");
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"Org: {organization.Name}");
Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationRepository>().Received(1).GetByIdAsync(eventMessage.OrganizationId ?? Guid.Empty);
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationUserRepository>().DidNotReceiveWithAnyArgs().GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>());
}
[Theory, BitAutoData]
public async Task HandleEventAsync_UserTemplate_LoadsUserFromRepository(EventMessage eventMessage)
{
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
var user = Substitute.For<User>();
var user = Substitute.For<OrganizationUserUserDetails>();
user.Email = "test@example.com";
user.Name = "Test";
eventMessage.OrganizationId = _organizationId;
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(Arg.Any<Guid>()).Returns(user);
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), Arg.Any<Guid>()).Returns(user);
await sutProvider.Sut.HandleEventAsync(eventMessage);
var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"{user.Name}, {user.Email}");
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage($"{user.Name}, {user.Email}, {user.Type}");
Assert.Single(_eventIntegrationPublisher.ReceivedCalls());
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
AssertHelper.AssertPropertyEqual(expectedMessage, new[] { "MessageId" })));
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
await sutProvider.GetDependency<IUserRepository>().Received(1).GetByIdAsync(eventMessage.UserId ?? Guid.Empty);
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetDetailsByOrganizationIdUserIdAsync(Arg.Any<Guid>(), eventMessage.UserId ?? Guid.Empty);
}
[Theory, BitAutoData]
@@ -256,7 +290,7 @@ public class EventIntegrationHandlerTests
await sutProvider.Sut.HandleEventAsync(eventMessage);
var expectedMessage = EventIntegrationHandlerTests.expectedMessage(
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
);
@@ -298,7 +332,7 @@ public class EventIntegrationHandlerTests
foreach (var eventMessage in eventMessages)
{
var expectedMessage = EventIntegrationHandlerTests.expectedMessage(
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
);
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(
@@ -316,7 +350,7 @@ public class EventIntegrationHandlerTests
foreach (var eventMessage in eventMessages)
{
var expectedMessage = EventIntegrationHandlerTests.expectedMessage(
var expectedMessage = EventIntegrationHandlerTests.ExpectedMessage(
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
);
await _eventIntegrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(

View File

@@ -83,6 +83,7 @@ public class IntegrationTemplateProcessorTests
[Theory]
[InlineData("User name is #UserName#")]
[InlineData("Email: #UserEmail#")]
[InlineData("User type = #UserType#")]
public void TemplateRequiresUser_ContainingKeys_ReturnsTrue(string template)
{
var result = IntegrationTemplateProcessor.TemplateRequiresUser(template);
@@ -102,6 +103,7 @@ public class IntegrationTemplateProcessorTests
[Theory]
[InlineData("Acting user is #ActingUserName#")]
[InlineData("Acting user's email is #ActingUserEmail#")]
[InlineData("Acting user's type is #ActingUserType#")]
public void TemplateRequiresActingUser_ContainingKeys_ReturnsTrue(string template)
{
var result = IntegrationTemplateProcessor.TemplateRequiresActingUser(template);
@@ -118,6 +120,25 @@ public class IntegrationTemplateProcessorTests
Assert.False(result);
}
[Theory]
[InlineData("Group name is #GroupName#!")]
[InlineData("Group: #GroupName#")]
public void TemplateRequiresGroup_ContainingKeys_ReturnsTrue(string template)
{
var result = IntegrationTemplateProcessor.TemplateRequiresGroup(template);
Assert.True(result);
}
[Theory]
[InlineData("#GroupId#")] // This is on the base class, not fetched, so should be false
[InlineData("No Group Tokens")]
[InlineData("")]
public void TemplateRequiresGroup_EmptyInputOrNoMatchingKeys_ReturnsFalse(string template)
{
var result = IntegrationTemplateProcessor.TemplateRequiresGroup(template);
Assert.False(result);
}
[Theory]
[InlineData("Organization: #OrganizationName#")]
[InlineData("Welcome to #OrganizationName#")]