Merge branch 'renovate/linq2db-6.x' of https://github.com/bitwarden/server into renovate/linq2db-6.x

This commit is contained in:
Robert Y
2026-04-09 08:42:00 -06:00
8 changed files with 280 additions and 15 deletions

View File

@@ -31,18 +31,39 @@ public class OrganizationIntegrationController(
.ToList();
}
/// <summary>
/// Creates a new organization integration.
/// Validates that only one integration of each type can exist per organization.
/// </summary>
/// <param name="organizationId"></param>
/// <param name="model"></param>
/// <returns></returns>
/// <exception cref="NotFoundException">Not enough permissions to access the organization.</exception>
/// <exception cref="ConflictResult">When an integration of the same type already exists for the organization.</exception>
[HttpPost("")]
public async Task<OrganizationIntegrationResponseModel> CreateAsync(Guid organizationId, [FromBody] OrganizationIntegrationRequestModel model)
public async Task<ActionResult<OrganizationIntegrationResponseModel>> CreateAsync(Guid organizationId, [FromBody] OrganizationIntegrationRequestModel model)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
if (!await HasPermission(organizationId))
{
throw new NotFoundException();
}
var integration = model.ToOrganizationIntegration(organizationId);
var canCreate = await createCommand.CanCreateAsync(integration);
if (!canCreate)
{
return Conflict();
}
var created = await createCommand.CreateAsync(integration);
return new OrganizationIntegrationResponseModel(created);
return Ok(new OrganizationIntegrationResponseModel(created));
}
[HttpPut("{integrationId:guid}")]

View File

@@ -45,7 +45,7 @@
<PackageReference Include="Microsoft.Bot.Builder" Version="4.23.0" />
<PackageReference Include="Microsoft.Bot.Builder.Integration.AspNet.Core" Version="4.23.0" />
<PackageReference Include="Microsoft.Bot.Connector" Version="4.23.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.8.0" />
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />

View File

@@ -17,11 +17,21 @@ public class CreateOrganizationIntegrationCommand(
IFusionCache cache)
: ICreateOrganizationIntegrationCommand
{
public async Task<OrganizationIntegration> CreateAsync(OrganizationIntegration integration)
public async Task<bool> CanCreateAsync(OrganizationIntegration integration)
{
var existingIntegrations = await integrationRepository
.GetManyByOrganizationAsync(integration.OrganizationId);
if (existingIntegrations.Any(i => i.Type == integration.Type))
{
return false;
}
return true;
}
public async Task<OrganizationIntegration> CreateAsync(OrganizationIntegration integration)
{
if (await CanCreateAsync(integration) == false)
{
throw new BadRequestException("An integration of this type already exists for this organization.");
}

View File

@@ -15,4 +15,12 @@ public interface ICreateOrganizationIntegrationCommand
/// <exception cref="Exceptions.BadRequestException">Thrown when an integration
/// of the same type already exists for the organization.</exception>
Task<OrganizationIntegration> CreateAsync(OrganizationIntegration integration);
/// <summary>
/// Checks if a new organization integration can be created based on existing integrations.
/// Enforces a validation to ensure that only one integration of each type can exist per organization.
/// </summary>
/// <param name="integration"></param>
/// <returns></returns>
Task<bool> CanCreateAsync(OrganizationIntegration integration);
}

View File

@@ -91,8 +91,8 @@ public class EventService : IEventService
});
}
var providerAbilities = await _applicationCacheService.GetProviderAbilitiesAsync();
var providers = await _currentContext.ProviderMembershipAsync(_providerUserRepository, userId);
var providerAbilities = await _applicationCacheService.GetProviderAbilitiesAsync(providers.Select(provider => provider.Id));
var providerEvents = providers.Where(o => CanUseProviderEvents(providerAbilities, o.Id))
.Select(p => new EventMessage(_currentContext)
{
@@ -382,9 +382,11 @@ public class EventService : IEventService
public async Task LogProviderUsersEventAsync(IEnumerable<(ProviderUser, EventType, DateTime?)> events)
{
var providerAbilities = await _applicationCacheService.GetProviderAbilitiesAsync();
var materializedEvents = events.ToList();
var providerAbilities = await _applicationCacheService.GetProviderAbilitiesAsync(
materializedEvents.Select(materializedEvent => materializedEvent.Item1.ProviderId));
var eventMessages = new List<IEvent>();
foreach (var (providerUser, type, date) in events)
foreach (var (providerUser, type, date) in materializedEvents)
{
if (!CanUseProviderEvents(providerAbilities, providerUser.ProviderId))
{
@@ -412,9 +414,11 @@ public class EventService : IEventService
public async Task LogProviderOrganizationEventsAsync(IEnumerable<(ProviderOrganization, EventType, DateTime?)> events)
{
var providerAbilities = await _applicationCacheService.GetProviderAbilitiesAsync();
var materializedEvents = events.ToList();
var providerAbilities = await _applicationCacheService.GetProviderAbilitiesAsync(
materializedEvents.Select(materializedEvent => materializedEvent.Item1.ProviderId));
var eventMessages = new List<IEvent>();
foreach (var (providerOrganization, type, date) in events)
foreach (var (providerOrganization, type, date) in materializedEvents)
{
if (!CanUseProviderEvents(providerAbilities, providerOrganization.ProviderId))
{

View File

@@ -88,6 +88,9 @@ public class OrganizationIntegrationControllerTests
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<ICreateOrganizationIntegrationCommand>()
.CanCreateAsync(Arg.Any<OrganizationIntegration>())
.Returns(true);
sutProvider.GetDependency<ICreateOrganizationIntegrationCommand>()
.CreateAsync(Arg.Any<OrganizationIntegration>())
.Returns(integration);
@@ -98,7 +101,31 @@ public class OrganizationIntegrationControllerTests
.CreateAsync(Arg.Is<OrganizationIntegration>(i =>
i.OrganizationId == organizationId &&
i.Type == IntegrationType.Webhook));
Assert.IsType<OrganizationIntegrationResponseModel>(response);
Assert.IsType<ActionResult<OrganizationIntegrationResponseModel>>(response);
Assert.IsType<OkObjectResult>(response.Result);
}
[Theory, BitAutoData]
public async Task CreateAsync_TheTypeAlreadyExists_ThrowsConflict(
SutProvider<OrganizationIntegrationController> sutProvider,
Guid organizationId,
OrganizationIntegration integration)
{
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
sutProvider.GetDependency<ICurrentContext>()
.OrganizationOwner(organizationId)
.Returns(true);
sutProvider.GetDependency<ICreateOrganizationIntegrationCommand>()
.CanCreateAsync(Arg.Any<OrganizationIntegration>())
.Returns(false);
sutProvider.GetDependency<ICreateOrganizationIntegrationCommand>()
.CreateAsync(Arg.Any<OrganizationIntegration>())
.Returns(integration);
var response = await sutProvider.Sut.CreateAsync(organizationId, _webhookRequestModel);
Assert.IsType<ActionResult<OrganizationIntegrationResponseModel>>(response);
Assert.IsType<ConflictResult>(response.Result);
}
[Theory, BitAutoData]

View File

@@ -214,7 +214,7 @@ public class EventServiceTests
{
{providerUser.ProviderId, new ProviderAbility() { UseEvents = true, Enabled = true } }
};
sutProvider.GetDependency<IApplicationCacheService>().GetProviderAbilitiesAsync().Returns(providerAbilities);
sutProvider.GetDependency<IApplicationCacheService>().GetProviderAbilitiesAsync(Arg.Any<IEnumerable<Guid>>()).Returns(providerAbilities);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().IpAddress.Returns(ipAddress);
sutProvider.GetDependency<ICurrentContext>().DeviceType.Returns(deviceType);
@@ -347,7 +347,7 @@ public class EventServiceTests
{
{ provider.Id, new ProviderAbility() { UseEvents = true, Enabled = true } }
};
sutProvider.GetDependency<IApplicationCacheService>().GetProviderAbilitiesAsync().Returns(providerAbilities);
sutProvider.GetDependency<IApplicationCacheService>().GetProviderAbilitiesAsync(Arg.Any<IEnumerable<Guid>>()).Returns(providerAbilities);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().IpAddress.Returns(ipAddress);
sutProvider.GetDependency<ICurrentContext>().DeviceType.Returns(deviceType);
@@ -386,7 +386,7 @@ public class EventServiceTests
.GetOrganizationAbilitiesAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(orgAbilities);
sutProvider.GetDependency<IApplicationCacheService>()
.GetProviderAbilitiesAsync()
.GetProviderAbilitiesAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new Dictionary<Guid, ProviderAbility>());
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByUserAsync(userId)
@@ -421,7 +421,7 @@ public class EventServiceTests
.GetOrganizationAbilitiesAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(orgAbilities);
sutProvider.GetDependency<IApplicationCacheService>()
.GetProviderAbilitiesAsync()
.GetProviderAbilitiesAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new Dictionary<Guid, ProviderAbility>());
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByUserAsync(userId)
@@ -441,4 +441,199 @@ public class EventServiceTests
.DidNotReceiveWithAnyArgs()
.CreateManyAsync(default);
}
[Theory, BitAutoData]
public async Task LogProviderUsersEventAsync_LogsRequiredInfo(Provider provider, ICollection<ProviderUser> providerUsers,
EventType eventType, DateTime date, Guid actingUserId, string ipAddress, DeviceType deviceType,
SutProvider<EventService> sutProvider)
{
// Arrange
foreach (var providerUser in providerUsers)
{
providerUser.ProviderId = provider.Id;
}
var providerAbilities = new Dictionary<Guid, ProviderAbility>()
{
{ provider.Id, new ProviderAbility() { UseEvents = true, Enabled = true } }
};
sutProvider.GetDependency<IApplicationCacheService>()
.GetProviderAbilitiesAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(providerAbilities);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().IpAddress.Returns(ipAddress);
sutProvider.GetDependency<ICurrentContext>().DeviceType.Returns(deviceType);
// Act
await sutProvider.Sut.LogProviderUsersEventAsync(providerUsers.Select(providerUser => (providerUser, eventType, (DateTime?)date)));
// Assert
var expected = providerUsers.Select(pu => new EventMessage()
{
IpAddress = ipAddress,
DeviceType = deviceType,
ProviderId = provider.Id,
UserId = pu.UserId,
ProviderUserId = pu.Id,
Type = eventType,
ActingUserId = actingUserId,
Date = date
}).ToList();
await sutProvider.GetDependency<IEventWriteService>().Received(1)
.CreateManyAsync(Arg.Is(AssertHelper.AssertPropertyEqual<IEvent>(expected, new[] { "IdempotencyId" })));
}
[Theory, BitAutoData]
public async Task LogProviderUsersEventAsync_WhenEventsDisabled_DoesNotLog(Provider provider,
ICollection<ProviderUser> providerUsers, EventType eventType, DateTime date,
SutProvider<EventService> sutProvider)
{
// Arrange
foreach (var providerUser in providerUsers)
{
providerUser.ProviderId = provider.Id;
}
sutProvider.GetDependency<IApplicationCacheService>()
.GetProviderAbilitiesAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new Dictionary<Guid, ProviderAbility>
{
{ provider.Id, new ProviderAbility() { UseEvents = false, Enabled = true } }
});
// Act
await sutProvider.Sut.LogProviderUsersEventAsync(providerUsers.Select(providerUser => (providerUser, eventType, (DateTime?)date)));
// Assert
await sutProvider.GetDependency<IEventWriteService>().Received(1)
.CreateManyAsync(Arg.Is<IEnumerable<IEvent>>(events => !events.Any()));
}
[Theory, BitAutoData]
public async Task LogProviderUsersEventAsync_QueriesOnlyRelevantProviderIds(
ICollection<ProviderUser> providerUsers, EventType eventType, DateTime date,
SutProvider<EventService> sutProvider)
{
// Arrange
sutProvider.GetDependency<IApplicationCacheService>()
.GetProviderAbilitiesAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new Dictionary<Guid, ProviderAbility>());
// Act
await sutProvider.Sut.LogProviderUsersEventAsync(providerUsers.Select(pu => (pu, eventType, (DateTime?)date)));
// Assert
var expectedIds = providerUsers.Select(pu => pu.ProviderId).Distinct();
await sutProvider.GetDependency<IApplicationCacheService>().Received(1)
.GetProviderAbilitiesAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.OrderBy(x => x).SequenceEqual(expectedIds.OrderBy(x => x))));
}
[Theory, BitAutoData]
public async Task LogProviderOrganizationEventsAsync_WhenEventsDisabled_DoesNotLog(Provider provider,
ICollection<ProviderOrganization> providerOrganizations, EventType eventType, DateTime date,
SutProvider<EventService> sutProvider)
{
// Arrange
foreach (var providerOrganization in providerOrganizations)
{
providerOrganization.ProviderId = provider.Id;
}
sutProvider.GetDependency<IApplicationCacheService>()
.GetProviderAbilitiesAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new Dictionary<Guid, ProviderAbility>
{
{ provider.Id, new ProviderAbility() { UseEvents = false, Enabled = true } }
});
// Act
await sutProvider.Sut.LogProviderOrganizationEventsAsync(
providerOrganizations.Select(po => (po, eventType, (DateTime?)date)));
// Assert
await sutProvider.GetDependency<IEventWriteService>().Received(1)
.CreateManyAsync(Arg.Is<IEnumerable<IEvent>>(events => !events.Any()));
}
[Theory, BitAutoData]
public async Task LogProviderOrganizationEventsAsync_QueriesOnlyRelevantProviderIds(
ICollection<ProviderOrganization> providerOrganizations, EventType eventType, DateTime date,
SutProvider<EventService> sutProvider)
{
// Arrange
sutProvider.GetDependency<IApplicationCacheService>()
.GetProviderAbilitiesAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new Dictionary<Guid, ProviderAbility>());
// Act
await sutProvider.Sut.LogProviderOrganizationEventsAsync(
providerOrganizations.Select(providerOrganization => (providerOrganization, eventType, (DateTime?)date)));
// Assert
var expectedIds = providerOrganizations.Select(po => po.ProviderId).Distinct();
await sutProvider.GetDependency<IApplicationCacheService>().Received(1)
.GetProviderAbilitiesAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.OrderBy(x => x).SequenceEqual(expectedIds.OrderBy(x => x))));
}
[Theory, BitAutoData]
public async Task LogUserEventAsync_WithProviderMembership_LogsProviderEvent(
Guid userId, EventType eventType, CurrentContextProvider provider,
SutProvider<EventService> sutProvider)
{
// Arrange
var providerAbilities = new Dictionary<Guid, ProviderAbility>
{
{ provider.Id, new ProviderAbility() { UseEvents = true, Enabled = true } }
};
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilitiesAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new Dictionary<Guid, OrganizationAbility>());
sutProvider.GetDependency<IApplicationCacheService>()
.GetProviderAbilitiesAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(providerAbilities);
sutProvider.GetDependency<ICurrentContext>()
.OrganizationMembershipAsync(Arg.Any<IOrganizationUserRepository>(), userId)
.Returns(new List<CurrentContextOrganization>());
sutProvider.GetDependency<ICurrentContext>()
.ProviderMembershipAsync(Arg.Any<IProviderUserRepository>(), userId)
.Returns(new List<CurrentContextProvider> { provider });
// Act
await sutProvider.Sut.LogUserEventAsync(userId, eventType);
// Assert
await sutProvider.GetDependency<IEventWriteService>().Received(1)
.CreateManyAsync(Arg.Is<IEnumerable<IEvent>>(events =>
events.Any(e => e.ProviderId == provider.Id && e.UserId == userId && e.Type == eventType)));
}
[Theory, BitAutoData]
public async Task LogUserEventAsync_QueriesOnlyMemberProviderIds(
Guid userId, EventType eventType, ICollection<CurrentContextProvider> providers,
SutProvider<EventService> sutProvider)
{
// Arrange
sutProvider.GetDependency<IApplicationCacheService>()
.GetOrganizationAbilitiesAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new Dictionary<Guid, OrganizationAbility>());
sutProvider.GetDependency<IApplicationCacheService>()
.GetProviderAbilitiesAsync(Arg.Any<IEnumerable<Guid>>())
.Returns(new Dictionary<Guid, ProviderAbility>());
sutProvider.GetDependency<ICurrentContext>()
.OrganizationMembershipAsync(Arg.Any<IOrganizationUserRepository>(), userId)
.Returns(new List<CurrentContextOrganization>());
sutProvider.GetDependency<ICurrentContext>()
.ProviderMembershipAsync(Arg.Any<IProviderUserRepository>(), userId)
.Returns(providers.ToList());
// Act
await sutProvider.Sut.LogUserEventAsync(userId, eventType);
// Assert
var expectedIds = providers.Select(provider => provider.Id);
await sutProvider.GetDependency<IApplicationCacheService>().Received(1)
.GetProviderAbilitiesAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.OrderBy(x => x).SequenceEqual(expectedIds.OrderBy(x => x))));
}
}

View File

@@ -12,7 +12,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="dbup-sqlserver" Version="6.0.0" />
<PackageReference Include="dbup-sqlserver" Version="7.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
</ItemGroup>