1
0
mirror of https://github.com/bitwarden/server.git synced 2025-04-05 05:00:19 -05:00

Merge branch 'main' into jmccannon/ac/pm-16811-scim-invite-optimization

# Conflicts:
#	src/Core/Constants.cs
This commit is contained in:
jrmccannon 2025-03-28 14:52:31 -05:00
commit 419fbdbced
No known key found for this signature in database
GPG Key ID: CF03F3DB01CE96A6
36 changed files with 10556 additions and 1024 deletions

View File

@ -1,10 +1,8 @@
using Bit.Core; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Scim.Groups.Interfaces; using Bit.Scim.Groups.Interfaces;
using Bit.Scim.Models; using Bit.Scim.Models;
using Bit.Scim.Utilities; using Bit.Scim.Utilities;
@ -24,10 +22,8 @@ public class GroupsController : Controller
private readonly IGetGroupsListQuery _getGroupsListQuery; private readonly IGetGroupsListQuery _getGroupsListQuery;
private readonly IDeleteGroupCommand _deleteGroupCommand; private readonly IDeleteGroupCommand _deleteGroupCommand;
private readonly IPatchGroupCommand _patchGroupCommand; private readonly IPatchGroupCommand _patchGroupCommand;
private readonly IPatchGroupCommandvNext _patchGroupCommandvNext;
private readonly IPostGroupCommand _postGroupCommand; private readonly IPostGroupCommand _postGroupCommand;
private readonly IPutGroupCommand _putGroupCommand; private readonly IPutGroupCommand _putGroupCommand;
private readonly IFeatureService _featureService;
public GroupsController( public GroupsController(
IGroupRepository groupRepository, IGroupRepository groupRepository,
@ -35,10 +31,8 @@ public class GroupsController : Controller
IGetGroupsListQuery getGroupsListQuery, IGetGroupsListQuery getGroupsListQuery,
IDeleteGroupCommand deleteGroupCommand, IDeleteGroupCommand deleteGroupCommand,
IPatchGroupCommand patchGroupCommand, IPatchGroupCommand patchGroupCommand,
IPatchGroupCommandvNext patchGroupCommandvNext,
IPostGroupCommand postGroupCommand, IPostGroupCommand postGroupCommand,
IPutGroupCommand putGroupCommand, IPutGroupCommand putGroupCommand
IFeatureService featureService
) )
{ {
_groupRepository = groupRepository; _groupRepository = groupRepository;
@ -46,10 +40,8 @@ public class GroupsController : Controller
_getGroupsListQuery = getGroupsListQuery; _getGroupsListQuery = getGroupsListQuery;
_deleteGroupCommand = deleteGroupCommand; _deleteGroupCommand = deleteGroupCommand;
_patchGroupCommand = patchGroupCommand; _patchGroupCommand = patchGroupCommand;
_patchGroupCommandvNext = patchGroupCommandvNext;
_postGroupCommand = postGroupCommand; _postGroupCommand = postGroupCommand;
_putGroupCommand = putGroupCommand; _putGroupCommand = putGroupCommand;
_featureService = featureService;
} }
[HttpGet("{id}")] [HttpGet("{id}")]
@ -103,21 +95,13 @@ public class GroupsController : Controller
[HttpPatch("{id}")] [HttpPatch("{id}")]
public async Task<IActionResult> Patch(Guid organizationId, Guid id, [FromBody] ScimPatchModel model) public async Task<IActionResult> Patch(Guid organizationId, Guid id, [FromBody] ScimPatchModel model)
{ {
if (_featureService.IsEnabled(FeatureFlagKeys.ShortcutDuplicatePatchRequests)) var group = await _groupRepository.GetByIdAsync(id);
if (group == null || group.OrganizationId != organizationId)
{ {
var group = await _groupRepository.GetByIdAsync(id); throw new NotFoundException("Group not found.");
if (group == null || group.OrganizationId != organizationId)
{
throw new NotFoundException("Group not found.");
}
await _patchGroupCommandvNext.PatchGroupAsync(group, model);
return new NoContentResult();
} }
var organization = await _organizationRepository.GetByIdAsync(organizationId); await _patchGroupCommand.PatchGroupAsync(group, model);
await _patchGroupCommand.PatchGroupAsync(organization, id, model);
return new NoContentResult(); return new NoContentResult();
} }

View File

@ -5,5 +5,5 @@ namespace Bit.Scim.Groups.Interfaces;
public interface IPatchGroupCommand public interface IPatchGroupCommand
{ {
Task PatchGroupAsync(Organization organization, Guid id, ScimPatchModel model); Task PatchGroupAsync(Group group, ScimPatchModel model);
} }

View File

@ -1,9 +0,0 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Scim.Models;
namespace Bit.Scim.Groups.Interfaces;
public interface IPatchGroupCommandvNext
{
Task PatchGroupAsync(Group group, ScimPatchModel model);
}

View File

@ -5,8 +5,10 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Scim.Groups.Interfaces; using Bit.Scim.Groups.Interfaces;
using Bit.Scim.Models; using Bit.Scim.Models;
using Bit.Scim.Utilities;
namespace Bit.Scim.Groups; namespace Bit.Scim.Groups;
@ -16,118 +18,137 @@ public class PatchGroupCommand : IPatchGroupCommand
private readonly IGroupService _groupService; private readonly IGroupService _groupService;
private readonly IUpdateGroupCommand _updateGroupCommand; private readonly IUpdateGroupCommand _updateGroupCommand;
private readonly ILogger<PatchGroupCommand> _logger; private readonly ILogger<PatchGroupCommand> _logger;
private readonly IOrganizationRepository _organizationRepository;
public PatchGroupCommand( public PatchGroupCommand(
IGroupRepository groupRepository, IGroupRepository groupRepository,
IGroupService groupService, IGroupService groupService,
IUpdateGroupCommand updateGroupCommand, IUpdateGroupCommand updateGroupCommand,
ILogger<PatchGroupCommand> logger) ILogger<PatchGroupCommand> logger,
IOrganizationRepository organizationRepository)
{ {
_groupRepository = groupRepository; _groupRepository = groupRepository;
_groupService = groupService; _groupService = groupService;
_updateGroupCommand = updateGroupCommand; _updateGroupCommand = updateGroupCommand;
_logger = logger; _logger = logger;
_organizationRepository = organizationRepository;
} }
public async Task PatchGroupAsync(Organization organization, Guid id, ScimPatchModel model) public async Task PatchGroupAsync(Group group, ScimPatchModel model)
{ {
var group = await _groupRepository.GetByIdAsync(id);
if (group == null || group.OrganizationId != organization.Id)
{
throw new NotFoundException("Group not found.");
}
var operationHandled = false;
foreach (var operation in model.Operations) foreach (var operation in model.Operations)
{ {
// Replace operations await HandleOperationAsync(group, operation);
if (operation.Op?.ToLowerInvariant() == "replace") }
{ }
// Replace a list of members
if (operation.Path?.ToLowerInvariant() == "members") private async Task HandleOperationAsync(Group group, ScimPatchModel.OperationModel operation)
{
switch (operation.Op?.ToLowerInvariant())
{
// Replace a list of members
case PatchOps.Replace when operation.Path?.ToLowerInvariant() == PatchPaths.Members:
{ {
var ids = GetOperationValueIds(operation.Value); var ids = GetOperationValueIds(operation.Value);
await _groupRepository.UpdateUsersAsync(group.Id, ids); await _groupRepository.UpdateUsersAsync(group.Id, ids);
operationHandled = true; break;
} }
// Replace group name from path
else if (operation.Path?.ToLowerInvariant() == "displayname") // Replace group name from path
case PatchOps.Replace when operation.Path?.ToLowerInvariant() == PatchPaths.DisplayName:
{ {
group.Name = operation.Value.GetString(); group.Name = operation.Value.GetString();
var organization = await _organizationRepository.GetByIdAsync(group.OrganizationId);
if (organization == null)
{
throw new NotFoundException();
}
await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM); await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
operationHandled = true; break;
} }
// Replace group name from value object
else if (string.IsNullOrWhiteSpace(operation.Path) && // Replace group name from value object
operation.Value.TryGetProperty("displayName", out var displayNameProperty)) case PatchOps.Replace when
string.IsNullOrWhiteSpace(operation.Path) &&
operation.Value.TryGetProperty("displayName", out var displayNameProperty):
{ {
group.Name = displayNameProperty.GetString(); group.Name = displayNameProperty.GetString();
var organization = await _organizationRepository.GetByIdAsync(group.OrganizationId);
if (organization == null)
{
throw new NotFoundException();
}
await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM); await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
operationHandled = true; break;
} }
}
// Add a single member // Add a single member
else if (operation.Op?.ToLowerInvariant() == "add" && case PatchOps.Add when
!string.IsNullOrWhiteSpace(operation.Path) && !string.IsNullOrWhiteSpace(operation.Path) &&
operation.Path.ToLowerInvariant().StartsWith("members[value eq ")) operation.Path.StartsWith("members[value eq ", StringComparison.OrdinalIgnoreCase) &&
{ TryGetOperationPathId(operation.Path, out var addId):
var addId = GetOperationPathId(operation.Path); {
if (addId.HasValue) await AddMembersAsync(group, [addId]);
break;
}
// Add a list of members
case PatchOps.Add when
operation.Path?.ToLowerInvariant() == PatchPaths.Members:
{
await AddMembersAsync(group, GetOperationValueIds(operation.Value));
break;
}
// Remove a single member
case PatchOps.Remove when
!string.IsNullOrWhiteSpace(operation.Path) &&
operation.Path.StartsWith("members[value eq ", StringComparison.OrdinalIgnoreCase) &&
TryGetOperationPathId(operation.Path, out var removeId):
{
await _groupService.DeleteUserAsync(group, removeId, EventSystemUser.SCIM);
break;
}
// Remove a list of members
case PatchOps.Remove when
operation.Path?.ToLowerInvariant() == PatchPaths.Members:
{ {
var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet(); var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();
orgUserIds.Add(addId.Value); foreach (var v in GetOperationValueIds(operation.Value))
{
orgUserIds.Remove(v);
}
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds); await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
operationHandled = true; break;
} }
}
// Add a list of members
else if (operation.Op?.ToLowerInvariant() == "add" &&
operation.Path?.ToLowerInvariant() == "members")
{
var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();
foreach (var v in GetOperationValueIds(operation.Value))
{
orgUserIds.Add(v);
}
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
operationHandled = true;
}
// Remove a single member
else if (operation.Op?.ToLowerInvariant() == "remove" &&
!string.IsNullOrWhiteSpace(operation.Path) &&
operation.Path.ToLowerInvariant().StartsWith("members[value eq "))
{
var removeId = GetOperationPathId(operation.Path);
if (removeId.HasValue)
{
await _groupService.DeleteUserAsync(group, removeId.Value, EventSystemUser.SCIM);
operationHandled = true;
}
}
// Remove a list of members
else if (operation.Op?.ToLowerInvariant() == "remove" &&
operation.Path?.ToLowerInvariant() == "members")
{
var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();
foreach (var v in GetOperationValueIds(operation.Value))
{
orgUserIds.Remove(v);
}
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
operationHandled = true;
}
}
if (!operationHandled) default:
{ {
_logger.LogWarning("Group patch operation not handled: {0} : ", _logger.LogWarning("Group patch operation not handled: {OperationOp}:{OperationPath}", operation.Op, operation.Path);
string.Join(", ", model.Operations.Select(o => $"{o.Op}:{o.Path}"))); break;
}
} }
} }
private List<Guid> GetOperationValueIds(JsonElement objArray) private async Task AddMembersAsync(Group group, HashSet<Guid> usersToAdd)
{ {
var ids = new List<Guid>(); // Azure Entra ID is known to send redundant "add" requests for each existing member every time any member
// is removed. To avoid excessive load on the database, we check against the high availability replica and
// return early if they already exist.
var groupMembers = await _groupRepository.GetManyUserIdsByIdAsync(group.Id, useReadOnlyReplica: true);
if (usersToAdd.IsSubsetOf(groupMembers))
{
_logger.LogDebug("Ignoring duplicate SCIM request to add members {Members} to group {Group}", usersToAdd, group.Id);
return;
}
await _groupRepository.AddGroupUsersByIdAsync(group.Id, usersToAdd);
}
private static HashSet<Guid> GetOperationValueIds(JsonElement objArray)
{
var ids = new HashSet<Guid>();
foreach (var obj in objArray.EnumerateArray()) foreach (var obj in objArray.EnumerateArray())
{ {
if (obj.TryGetProperty("value", out var valueProperty)) if (obj.TryGetProperty("value", out var valueProperty))
@ -141,13 +162,9 @@ public class PatchGroupCommand : IPatchGroupCommand
return ids; return ids;
} }
private Guid? GetOperationPathId(string path) private static bool TryGetOperationPathId(string path, out Guid pathId)
{ {
// Parse Guid from string like: members[value eq "{GUID}"}] // Parse Guid from string like: members[value eq "{GUID}"}]
if (Guid.TryParse(path.Substring(18).Replace("\"]", string.Empty), out var id)) return Guid.TryParse(path.Substring(18).Replace("\"]", string.Empty), out pathId);
{
return id;
}
return null;
} }
} }

View File

@ -1,170 +0,0 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Scim.Groups.Interfaces;
using Bit.Scim.Models;
using Bit.Scim.Utilities;
namespace Bit.Scim.Groups;
public class PatchGroupCommandvNext : IPatchGroupCommandvNext
{
private readonly IGroupRepository _groupRepository;
private readonly IGroupService _groupService;
private readonly IUpdateGroupCommand _updateGroupCommand;
private readonly ILogger<PatchGroupCommandvNext> _logger;
private readonly IOrganizationRepository _organizationRepository;
public PatchGroupCommandvNext(
IGroupRepository groupRepository,
IGroupService groupService,
IUpdateGroupCommand updateGroupCommand,
ILogger<PatchGroupCommandvNext> logger,
IOrganizationRepository organizationRepository)
{
_groupRepository = groupRepository;
_groupService = groupService;
_updateGroupCommand = updateGroupCommand;
_logger = logger;
_organizationRepository = organizationRepository;
}
public async Task PatchGroupAsync(Group group, ScimPatchModel model)
{
foreach (var operation in model.Operations)
{
await HandleOperationAsync(group, operation);
}
}
private async Task HandleOperationAsync(Group group, ScimPatchModel.OperationModel operation)
{
switch (operation.Op?.ToLowerInvariant())
{
// Replace a list of members
case PatchOps.Replace when operation.Path?.ToLowerInvariant() == PatchPaths.Members:
{
var ids = GetOperationValueIds(operation.Value);
await _groupRepository.UpdateUsersAsync(group.Id, ids);
break;
}
// Replace group name from path
case PatchOps.Replace when operation.Path?.ToLowerInvariant() == PatchPaths.DisplayName:
{
group.Name = operation.Value.GetString();
var organization = await _organizationRepository.GetByIdAsync(group.OrganizationId);
if (organization == null)
{
throw new NotFoundException();
}
await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
break;
}
// Replace group name from value object
case PatchOps.Replace when
string.IsNullOrWhiteSpace(operation.Path) &&
operation.Value.TryGetProperty("displayName", out var displayNameProperty):
{
group.Name = displayNameProperty.GetString();
var organization = await _organizationRepository.GetByIdAsync(group.OrganizationId);
if (organization == null)
{
throw new NotFoundException();
}
await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
break;
}
// Add a single member
case PatchOps.Add when
!string.IsNullOrWhiteSpace(operation.Path) &&
operation.Path.StartsWith("members[value eq ", StringComparison.OrdinalIgnoreCase) &&
TryGetOperationPathId(operation.Path, out var addId):
{
await AddMembersAsync(group, [addId]);
break;
}
// Add a list of members
case PatchOps.Add when
operation.Path?.ToLowerInvariant() == PatchPaths.Members:
{
await AddMembersAsync(group, GetOperationValueIds(operation.Value));
break;
}
// Remove a single member
case PatchOps.Remove when
!string.IsNullOrWhiteSpace(operation.Path) &&
operation.Path.StartsWith("members[value eq ", StringComparison.OrdinalIgnoreCase) &&
TryGetOperationPathId(operation.Path, out var removeId):
{
await _groupService.DeleteUserAsync(group, removeId, EventSystemUser.SCIM);
break;
}
// Remove a list of members
case PatchOps.Remove when
operation.Path?.ToLowerInvariant() == PatchPaths.Members:
{
var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();
foreach (var v in GetOperationValueIds(operation.Value))
{
orgUserIds.Remove(v);
}
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
break;
}
default:
{
_logger.LogWarning("Group patch operation not handled: {OperationOp}:{OperationPath}", operation.Op, operation.Path);
break;
}
}
}
private async Task AddMembersAsync(Group group, HashSet<Guid> usersToAdd)
{
// Azure Entra ID is known to send redundant "add" requests for each existing member every time any member
// is removed. To avoid excessive load on the database, we check against the high availability replica and
// return early if they already exist.
var groupMembers = await _groupRepository.GetManyUserIdsByIdAsync(group.Id, useReadOnlyReplica: true);
if (usersToAdd.IsSubsetOf(groupMembers))
{
_logger.LogDebug("Ignoring duplicate SCIM request to add members {Members} to group {Group}", usersToAdd, group.Id);
return;
}
await _groupRepository.AddGroupUsersByIdAsync(group.Id, usersToAdd);
}
private static HashSet<Guid> GetOperationValueIds(JsonElement objArray)
{
var ids = new HashSet<Guid>();
foreach (var obj in objArray.EnumerateArray())
{
if (obj.TryGetProperty("value", out var valueProperty))
{
if (valueProperty.TryGetGuid(out var guid))
{
ids.Add(guid);
}
}
}
return ids;
}
private static bool TryGetOperationPathId(string path, out Guid pathId)
{
// Parse Guid from string like: members[value eq "{GUID}"}]
return Guid.TryParse(path.Substring(18).Replace("\"]", string.Empty), out pathId);
}
}

View File

@ -10,7 +10,6 @@ public static class ScimServiceCollectionExtensions
public static void AddScimGroupCommands(this IServiceCollection services) public static void AddScimGroupCommands(this IServiceCollection services)
{ {
services.AddScoped<IPatchGroupCommand, PatchGroupCommand>(); services.AddScoped<IPatchGroupCommand, PatchGroupCommand>();
services.AddScoped<IPatchGroupCommandvNext, PatchGroupCommandvNext>();
services.AddScoped<IPostGroupCommand, PostGroupCommand>(); services.AddScoped<IPostGroupCommand, PostGroupCommand>();
services.AddScoped<IPutGroupCommand, PutGroupCommand>(); services.AddScoped<IPutGroupCommand, PutGroupCommand>();
} }

View File

@ -20,6 +20,7 @@ public class GroupsControllerPatchTests : IClassFixture<ScimApplicationFactory>,
{ {
var databaseContext = _factory.GetDatabaseContext(); var databaseContext = _factory.GetDatabaseContext();
_factory.ReinitializeDbForTests(databaseContext); _factory.ReinitializeDbForTests(databaseContext);
return Task.CompletedTask; return Task.CompletedTask;
} }

View File

@ -1,251 +0,0 @@
using System.Text.Json;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Services;
using Bit.Scim.Groups.Interfaces;
using Bit.Scim.IntegrationTest.Factories;
using Bit.Scim.Models;
using Bit.Scim.Utilities;
using Bit.Test.Common.Helpers;
using NSubstitute;
using NSubstitute.ExceptionExtensions;
using Xunit;
namespace Bit.Scim.IntegrationTest.Controllers.v2;
public class GroupsControllerPatchTestsvNext : IClassFixture<ScimApplicationFactory>, IAsyncLifetime
{
private readonly ScimApplicationFactory _factory;
public GroupsControllerPatchTestsvNext(ScimApplicationFactory factory)
{
_factory = factory;
// Enable the feature flag for new PatchGroupsCommand and stub out the old command to be safe
_factory.SubstituteService((IFeatureService featureService)
=> featureService.IsEnabled(FeatureFlagKeys.ShortcutDuplicatePatchRequests).Returns(true));
_factory.SubstituteService((IPatchGroupCommand patchGroupCommand)
=> patchGroupCommand.PatchGroupAsync(Arg.Any<Organization>(), Arg.Any<Guid>(), Arg.Any<ScimPatchModel>())
.ThrowsAsync(new Exception("This test suite should be testing the vNext command, but the existing command was called.")));
}
public Task InitializeAsync()
{
var databaseContext = _factory.GetDatabaseContext();
_factory.ReinitializeDbForTests(databaseContext);
return Task.CompletedTask;
}
Task IAsyncLifetime.DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task Patch_ReplaceDisplayName_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId1;
var newDisplayName = "Patch Display Name";
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "replace",
Value = JsonDocument.Parse($"{{\"displayName\":\"{newDisplayName}\"}}").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId);
Assert.Equal(newDisplayName, group.Name);
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount, databaseContext.GroupUsers.Count());
Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1));
Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4));
}
[Fact]
public async Task Patch_ReplaceMembers_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId1;
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "replace",
Path = "members",
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId2}\"}}]").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
Assert.Single(databaseContext.GroupUsers);
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount - 1, databaseContext.GroupUsers.Count());
var groupUser = databaseContext.GroupUsers.FirstOrDefault();
Assert.Equal(ScimApplicationFactory.TestOrganizationUserId2, groupUser.OrganizationUserId);
}
[Fact]
public async Task Patch_AddSingleMember_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId1;
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "add",
Path = $"members[value eq \"{ScimApplicationFactory.TestOrganizationUserId2}\"]",
Value = JsonDocument.Parse("{}").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount + 1, databaseContext.GroupUsers.Count());
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1));
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2));
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4));
}
[Fact]
public async Task Patch_AddListMembers_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId2;
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "add",
Path = "members",
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId2}\"}},{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId3}\"}}]").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2));
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId3));
}
[Fact]
public async Task Patch_RemoveSingleMember_ReplaceDisplayName_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId1;
var newDisplayName = "Patch Display Name";
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "remove",
Path = $"members[value eq \"{ScimApplicationFactory.TestOrganizationUserId1}\"]",
Value = JsonDocument.Parse("{}").RootElement
},
new ScimPatchModel.OperationModel
{
Op = "replace",
Value = JsonDocument.Parse($"{{\"displayName\":\"{newDisplayName}\"}}").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount - 1, databaseContext.GroupUsers.Count());
Assert.Equal(ScimApplicationFactory.InitialGroupCount, databaseContext.Groups.Count());
var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId);
Assert.Equal(newDisplayName, group.Name);
}
[Fact]
public async Task Patch_RemoveListMembers_Success()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = ScimApplicationFactory.TestGroupId1;
var inputModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>()
{
new ScimPatchModel.OperationModel
{
Op = "remove",
Path = "members",
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId1}\"}}, {{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId4}\"}}]").RootElement
}
},
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
var databaseContext = _factory.GetDatabaseContext();
Assert.Empty(databaseContext.GroupUsers);
}
[Fact]
public async Task Patch_NotFound()
{
var organizationId = ScimApplicationFactory.TestOrganizationId1;
var groupId = Guid.NewGuid();
var inputModel = new Models.ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>(),
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
};
var expectedResponse = new ScimErrorResponseModel
{
Status = StatusCodes.Status404NotFound,
Detail = "Group not found.",
Schemas = new List<string> { ScimConstants.Scim2SchemaError }
};
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode);
var responseModel = JsonSerializer.Deserialize<ScimErrorResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);
}
}

View File

@ -1,15 +1,18 @@
using System.Text.Json; using System.Text.Json;
using AutoFixture;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Scim.Groups; using Bit.Scim.Groups;
using Bit.Scim.Models; using Bit.Scim.Models;
using Bit.Scim.Utilities; using Bit.Scim.Utilities;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Microsoft.Extensions.Logging;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
@ -20,19 +23,16 @@ public class PatchGroupCommandTests
{ {
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task PatchGroup_ReplaceListMembers_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, IEnumerable<Guid> userIds) public async Task PatchGroup_ReplaceListMembers_Success(SutProvider<PatchGroupCommand> sutProvider,
Organization organization, Group group, IEnumerable<Guid> userIds)
{ {
group.OrganizationId = organization.Id; group.OrganizationId = organization.Id;
sutProvider.GetDependency<IGroupRepository>() var scimPatchModel = new ScimPatchModel
.GetByIdAsync(group.Id)
.Returns(group);
var scimPatchModel = new Models.ScimPatchModel
{ {
Operations = new List<ScimPatchModel.OperationModel> Operations = new List<ScimPatchModel.OperationModel>
{ {
new ScimPatchModel.OperationModel new()
{ {
Op = "replace", Op = "replace",
Path = "members", Path = "members",
@ -42,26 +42,31 @@ public class PatchGroupCommandTests
Schemas = new List<string> { ScimConstants.Scim2SchemaUser } Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
}; };
await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel); await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(group.Id, Arg.Is<IEnumerable<Guid>>(arg => arg.All(id => userIds.Contains(id)))); await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(
group.Id,
Arg.Is<IEnumerable<Guid>>(arg =>
arg.Count() == userIds.Count() &&
arg.ToHashSet().SetEquals(userIds)));
} }
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task PatchGroup_ReplaceDisplayNameFromPath_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, string displayName) public async Task PatchGroup_ReplaceDisplayNameFromPath_Success(
SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, string displayName)
{ {
group.OrganizationId = organization.Id; group.OrganizationId = organization.Id;
sutProvider.GetDependency<IGroupRepository>() sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(group.Id) .GetByIdAsync(organization.Id)
.Returns(group); .Returns(organization);
var scimPatchModel = new Models.ScimPatchModel var scimPatchModel = new ScimPatchModel
{ {
Operations = new List<ScimPatchModel.OperationModel> Operations = new List<ScimPatchModel.OperationModel>
{ {
new ScimPatchModel.OperationModel new()
{ {
Op = "replace", Op = "replace",
Path = "displayname", Path = "displayname",
@ -71,27 +76,55 @@ public class PatchGroupCommandTests
Schemas = new List<string> { ScimConstants.Scim2SchemaUser } Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
}; };
await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel); await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM); await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
Assert.Equal(displayName, group.Name); Assert.Equal(displayName, group.Name);
} }
[Theory]
[BitAutoData]
public async Task PatchGroup_ReplaceDisplayNameFromPath_MissingOrganization_Throws(
SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, string displayName)
{
group.OrganizationId = organization.Id;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns((Organization)null);
var scimPatchModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new()
{
Op = "replace",
Path = "displayname",
Value = JsonDocument.Parse($"\"{displayName}\"").RootElement
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PatchGroupAsync(group, scimPatchModel));
}
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task PatchGroup_ReplaceDisplayNameFromValueObject_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, string displayName) public async Task PatchGroup_ReplaceDisplayNameFromValueObject_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, string displayName)
{ {
group.OrganizationId = organization.Id; group.OrganizationId = organization.Id;
sutProvider.GetDependency<IGroupRepository>() sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(group.Id) .GetByIdAsync(organization.Id)
.Returns(group); .Returns(organization);
var scimPatchModel = new Models.ScimPatchModel var scimPatchModel = new ScimPatchModel
{ {
Operations = new List<ScimPatchModel.OperationModel> Operations = new List<ScimPatchModel.OperationModel>
{ {
new ScimPatchModel.OperationModel new()
{ {
Op = "replace", Op = "replace",
Value = JsonDocument.Parse($"{{\"displayName\":\"{displayName}\"}}").RootElement Value = JsonDocument.Parse($"{{\"displayName\":\"{displayName}\"}}").RootElement
@ -100,12 +133,39 @@ public class PatchGroupCommandTests
Schemas = new List<string> { ScimConstants.Scim2SchemaUser } Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
}; };
await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel); await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM); await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
Assert.Equal(displayName, group.Name); Assert.Equal(displayName, group.Name);
} }
[Theory]
[BitAutoData]
public async Task PatchGroup_ReplaceDisplayNameFromValueObject_MissingOrganization_Throws(
SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, string displayName)
{
group.OrganizationId = organization.Id;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns((Organization)null);
var scimPatchModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new()
{
Op = "replace",
Value = JsonDocument.Parse($"{{\"displayName\":\"{displayName}\"}}").RootElement
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PatchGroupAsync(group, scimPatchModel));
}
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task PatchGroup_AddSingleMember_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, ICollection<Guid> existingMembers, Guid userId) public async Task PatchGroup_AddSingleMember_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, ICollection<Guid> existingMembers, Guid userId)
@ -113,18 +173,14 @@ public class PatchGroupCommandTests
group.OrganizationId = organization.Id; group.OrganizationId = organization.Id;
sutProvider.GetDependency<IGroupRepository>() sutProvider.GetDependency<IGroupRepository>()
.GetByIdAsync(group.Id) .GetManyUserIdsByIdAsync(group.Id, true)
.Returns(group);
sutProvider.GetDependency<IGroupRepository>()
.GetManyUserIdsByIdAsync(group.Id)
.Returns(existingMembers); .Returns(existingMembers);
var scimPatchModel = new Models.ScimPatchModel var scimPatchModel = new ScimPatchModel
{ {
Operations = new List<ScimPatchModel.OperationModel> Operations = new List<ScimPatchModel.OperationModel>
{ {
new ScimPatchModel.OperationModel new()
{ {
Op = "add", Op = "add",
Path = $"members[value eq \"{userId}\"]", Path = $"members[value eq \"{userId}\"]",
@ -133,9 +189,47 @@ public class PatchGroupCommandTests
Schemas = new List<string> { ScimConstants.Scim2SchemaUser } Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
}; };
await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel); await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(group.Id, Arg.Is<IEnumerable<Guid>>(arg => arg.All(id => existingMembers.Append(userId).Contains(id)))); await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByIdAsync(
group.Id,
Arg.Is<IEnumerable<Guid>>(arg => arg.Single() == userId));
}
[Theory]
[BitAutoData]
public async Task PatchGroup_AddSingleMember_ReturnsEarlyIfAlreadyInGroup(
SutProvider<PatchGroupCommand> sutProvider,
Organization organization,
Group group,
ICollection<Guid> existingMembers)
{
// User being added is already in group
var userId = existingMembers.First();
group.OrganizationId = organization.Id;
sutProvider.GetDependency<IGroupRepository>()
.GetManyUserIdsByIdAsync(group.Id, true)
.Returns(existingMembers);
var scimPatchModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new()
{
Op = "add",
Path = $"members[value eq \"{userId}\"]",
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupRepository>()
.DidNotReceiveWithAnyArgs()
.AddGroupUsersByIdAsync(default, default);
} }
[Theory] [Theory]
@ -145,18 +239,14 @@ public class PatchGroupCommandTests
group.OrganizationId = organization.Id; group.OrganizationId = organization.Id;
sutProvider.GetDependency<IGroupRepository>() sutProvider.GetDependency<IGroupRepository>()
.GetByIdAsync(group.Id) .GetManyUserIdsByIdAsync(group.Id, true)
.Returns(group);
sutProvider.GetDependency<IGroupRepository>()
.GetManyUserIdsByIdAsync(group.Id)
.Returns(existingMembers); .Returns(existingMembers);
var scimPatchModel = new Models.ScimPatchModel var scimPatchModel = new ScimPatchModel
{ {
Operations = new List<ScimPatchModel.OperationModel> Operations = new List<ScimPatchModel.OperationModel>
{ {
new ScimPatchModel.OperationModel new()
{ {
Op = "add", Op = "add",
Path = $"members", Path = $"members",
@ -166,9 +256,101 @@ public class PatchGroupCommandTests
Schemas = new List<string> { ScimConstants.Scim2SchemaUser } Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
}; };
await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel); await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(group.Id, Arg.Is<IEnumerable<Guid>>(arg => arg.All(id => existingMembers.Concat(userIds).Contains(id)))); await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByIdAsync(
group.Id,
Arg.Is<IEnumerable<Guid>>(arg =>
arg.Count() == userIds.Count &&
arg.ToHashSet().SetEquals(userIds)));
}
[Theory]
[BitAutoData]
public async Task PatchGroup_AddListMembers_IgnoresDuplicatesInRequest(
SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group,
ICollection<Guid> existingMembers)
{
// Create 3 userIds
var fixture = new Fixture { RepeatCount = 3 };
var userIds = fixture.CreateMany<Guid>().ToList();
// Copy the list and add a duplicate
var userIdsWithDuplicate = userIds.Append(userIds.First()).ToList();
Assert.Equal(4, userIdsWithDuplicate.Count);
group.OrganizationId = organization.Id;
sutProvider.GetDependency<IGroupRepository>()
.GetManyUserIdsByIdAsync(group.Id, true)
.Returns(existingMembers);
var scimPatchModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new()
{
Op = "add",
Path = $"members",
Value = JsonDocument.Parse(JsonSerializer
.Serialize(userIdsWithDuplicate
.Select(uid => new { value = uid })
.ToArray())).RootElement
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByIdAsync(
group.Id,
Arg.Is<IEnumerable<Guid>>(arg =>
arg.Count() == 3 &&
arg.ToHashSet().SetEquals(userIds)));
}
[Theory]
[BitAutoData]
public async Task PatchGroup_AddListMembers_SuccessIfOnlySomeUsersAreInGroup(
SutProvider<PatchGroupCommand> sutProvider,
Organization organization, Group group,
ICollection<Guid> existingMembers,
ICollection<Guid> userIds)
{
// A user is already in the group, but some still need to be added
userIds.Add(existingMembers.First());
group.OrganizationId = organization.Id;
sutProvider.GetDependency<IGroupRepository>()
.GetManyUserIdsByIdAsync(group.Id, true)
.Returns(existingMembers);
var scimPatchModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new()
{
Op = "add",
Path = $"members",
Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupRepository>()
.Received(1)
.AddGroupUsersByIdAsync(
group.Id,
Arg.Is<IEnumerable<Guid>>(arg =>
arg.Count() == userIds.Count &&
arg.ToHashSet().SetEquals(userIds)));
} }
[Theory] [Theory]
@ -177,10 +359,6 @@ public class PatchGroupCommandTests
{ {
group.OrganizationId = organization.Id; group.OrganizationId = organization.Id;
sutProvider.GetDependency<IGroupRepository>()
.GetByIdAsync(group.Id)
.Returns(group);
var scimPatchModel = new Models.ScimPatchModel var scimPatchModel = new Models.ScimPatchModel
{ {
Operations = new List<ScimPatchModel.OperationModel> Operations = new List<ScimPatchModel.OperationModel>
@ -194,21 +372,19 @@ public class PatchGroupCommandTests
Schemas = new List<string> { ScimConstants.Scim2SchemaUser } Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
}; };
await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel); await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupService>().Received(1).DeleteUserAsync(group, userId, EventSystemUser.SCIM); await sutProvider.GetDependency<IGroupService>().Received(1).DeleteUserAsync(group, userId, EventSystemUser.SCIM);
} }
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task PatchGroup_RemoveListMembers_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, ICollection<Guid> existingMembers) public async Task PatchGroup_RemoveListMembers_Success(SutProvider<PatchGroupCommand> sutProvider,
Organization organization, Group group, ICollection<Guid> existingMembers)
{ {
List<Guid> usersToRemove = [existingMembers.First(), existingMembers.Skip(1).First()];
group.OrganizationId = organization.Id; group.OrganizationId = organization.Id;
sutProvider.GetDependency<IGroupRepository>()
.GetByIdAsync(group.Id)
.Returns(group);
sutProvider.GetDependency<IGroupRepository>() sutProvider.GetDependency<IGroupRepository>()
.GetManyUserIdsByIdAsync(group.Id) .GetManyUserIdsByIdAsync(group.Id)
.Returns(existingMembers); .Returns(existingMembers);
@ -217,30 +393,58 @@ public class PatchGroupCommandTests
{ {
Operations = new List<ScimPatchModel.OperationModel> Operations = new List<ScimPatchModel.OperationModel>
{ {
new ScimPatchModel.OperationModel new()
{ {
Op = "remove", Op = "remove",
Path = $"members", Path = $"members",
Value = JsonDocument.Parse(JsonSerializer.Serialize(existingMembers.Select(uid => new { value = uid }).ToArray())).RootElement Value = JsonDocument.Parse(JsonSerializer.Serialize(usersToRemove.Select(uid => new { value = uid }).ToArray())).RootElement
} }
}, },
Schemas = new List<string> { ScimConstants.Scim2SchemaUser } Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
}; };
await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel); await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(group.Id, Arg.Is<IEnumerable<Guid>>(arg => arg.All(id => existingMembers.Contains(id)))); var expectedRemainingUsers = existingMembers.Skip(2).ToList();
await sutProvider.GetDependency<IGroupRepository>()
.Received(1)
.UpdateUsersAsync(
group.Id,
Arg.Is<IEnumerable<Guid>>(arg =>
arg.Count() == expectedRemainingUsers.Count &&
arg.ToHashSet().SetEquals(expectedRemainingUsers)));
} }
[Theory] [Theory]
[BitAutoData] [BitAutoData]
public async Task PatchGroup_NoAction_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group) public async Task PatchGroup_InvalidOperation_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group)
{ {
group.OrganizationId = organization.Id; group.OrganizationId = organization.Id;
sutProvider.GetDependency<IGroupRepository>() var scimPatchModel = new Models.ScimPatchModel
.GetByIdAsync(group.Id) {
.Returns(group); Operations = [new ScimPatchModel.OperationModel { Op = "invalid operation" }],
Schemas = [ScimConstants.Scim2SchemaUser]
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
// Assert: no operation performed
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default);
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyUserIdsByIdAsync(default);
await sutProvider.GetDependency<IUpdateGroupCommand>().DidNotReceiveWithAnyArgs().UpdateGroupAsync(default, default);
await sutProvider.GetDependency<IGroupService>().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default);
// Assert: logging
sutProvider.GetDependency<ILogger<PatchGroupCommand>>().ReceivedWithAnyArgs().LogWarning(default);
}
[Theory]
[BitAutoData]
public async Task PatchGroup_NoOperation_Success(
SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group)
{
group.OrganizationId = organization.Id;
var scimPatchModel = new Models.ScimPatchModel var scimPatchModel = new Models.ScimPatchModel
{ {
@ -248,45 +452,11 @@ public class PatchGroupCommandTests
Schemas = new List<string> { ScimConstants.Scim2SchemaUser } Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
}; };
await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel); await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default); await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default);
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyUserIdsByIdAsync(default); await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyUserIdsByIdAsync(default);
await sutProvider.GetDependency<IUpdateGroupCommand>().DidNotReceiveWithAnyArgs().UpdateGroupAsync(default, default); await sutProvider.GetDependency<IUpdateGroupCommand>().DidNotReceiveWithAnyArgs().UpdateGroupAsync(default, default);
await sutProvider.GetDependency<IGroupService>().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default); await sutProvider.GetDependency<IGroupService>().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default);
} }
[Theory]
[BitAutoData]
public async Task PatchGroup_NotFound_Throws(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Guid groupId)
{
var scimPatchModel = new Models.ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>(),
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.PatchGroupAsync(organization, groupId, scimPatchModel));
}
[Theory]
[BitAutoData]
public async Task PatchGroup_MismatchingOrganizationId_Throws(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Guid groupId)
{
var scimPatchModel = new Models.ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>(),
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
sutProvider.GetDependency<IGroupRepository>()
.GetByIdAsync(groupId)
.Returns(new Group
{
Id = groupId,
OrganizationId = Guid.NewGuid()
});
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.PatchGroupAsync(organization, groupId, scimPatchModel));
}
} }

View File

@ -1,381 +0,0 @@
using System.Text.Json;
using AutoFixture;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Scim.Groups;
using Bit.Scim.Models;
using Bit.Scim.Utilities;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Scim.Test.Groups;
[SutProviderCustomize]
public class PatchGroupCommandvNextTests
{
[Theory]
[BitAutoData]
public async Task PatchGroup_ReplaceListMembers_Success(SutProvider<PatchGroupCommandvNext> sutProvider,
Organization organization, Group group, IEnumerable<Guid> userIds)
{
group.OrganizationId = organization.Id;
var scimPatchModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new()
{
Op = "replace",
Path = "members",
Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(
group.Id,
Arg.Is<IEnumerable<Guid>>(arg =>
arg.Count() == userIds.Count() &&
arg.ToHashSet().SetEquals(userIds)));
}
[Theory]
[BitAutoData]
public async Task PatchGroup_ReplaceDisplayNameFromPath_Success(
SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group, string displayName)
{
group.OrganizationId = organization.Id;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
var scimPatchModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new()
{
Op = "replace",
Path = "displayname",
Value = JsonDocument.Parse($"\"{displayName}\"").RootElement
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
Assert.Equal(displayName, group.Name);
}
[Theory]
[BitAutoData]
public async Task PatchGroup_ReplaceDisplayNameFromValueObject_Success(SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group, string displayName)
{
group.OrganizationId = organization.Id;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByIdAsync(organization.Id)
.Returns(organization);
var scimPatchModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new()
{
Op = "replace",
Value = JsonDocument.Parse($"{{\"displayName\":\"{displayName}\"}}").RootElement
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
Assert.Equal(displayName, group.Name);
}
[Theory]
[BitAutoData]
public async Task PatchGroup_AddSingleMember_Success(SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group, ICollection<Guid> existingMembers, Guid userId)
{
group.OrganizationId = organization.Id;
sutProvider.GetDependency<IGroupRepository>()
.GetManyUserIdsByIdAsync(group.Id, true)
.Returns(existingMembers);
var scimPatchModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new()
{
Op = "add",
Path = $"members[value eq \"{userId}\"]",
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByIdAsync(
group.Id,
Arg.Is<IEnumerable<Guid>>(arg => arg.Single() == userId));
}
[Theory]
[BitAutoData]
public async Task PatchGroup_AddSingleMember_ReturnsEarlyIfAlreadyInGroup(
SutProvider<PatchGroupCommandvNext> sutProvider,
Organization organization,
Group group,
ICollection<Guid> existingMembers)
{
// User being added is already in group
var userId = existingMembers.First();
group.OrganizationId = organization.Id;
sutProvider.GetDependency<IGroupRepository>()
.GetManyUserIdsByIdAsync(group.Id, true)
.Returns(existingMembers);
var scimPatchModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new()
{
Op = "add",
Path = $"members[value eq \"{userId}\"]",
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupRepository>()
.DidNotReceiveWithAnyArgs()
.AddGroupUsersByIdAsync(default, default);
}
[Theory]
[BitAutoData]
public async Task PatchGroup_AddListMembers_Success(SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group, ICollection<Guid> existingMembers, ICollection<Guid> userIds)
{
group.OrganizationId = organization.Id;
sutProvider.GetDependency<IGroupRepository>()
.GetManyUserIdsByIdAsync(group.Id, true)
.Returns(existingMembers);
var scimPatchModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new()
{
Op = "add",
Path = $"members",
Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByIdAsync(
group.Id,
Arg.Is<IEnumerable<Guid>>(arg =>
arg.Count() == userIds.Count &&
arg.ToHashSet().SetEquals(userIds)));
}
[Theory]
[BitAutoData]
public async Task PatchGroup_AddListMembers_IgnoresDuplicatesInRequest(
SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group,
ICollection<Guid> existingMembers)
{
// Create 3 userIds
var fixture = new Fixture { RepeatCount = 3 };
var userIds = fixture.CreateMany<Guid>().ToList();
// Copy the list and add a duplicate
var userIdsWithDuplicate = userIds.Append(userIds.First()).ToList();
Assert.Equal(4, userIdsWithDuplicate.Count);
group.OrganizationId = organization.Id;
sutProvider.GetDependency<IGroupRepository>()
.GetManyUserIdsByIdAsync(group.Id, true)
.Returns(existingMembers);
var scimPatchModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new()
{
Op = "add",
Path = $"members",
Value = JsonDocument.Parse(JsonSerializer
.Serialize(userIdsWithDuplicate
.Select(uid => new { value = uid })
.ToArray())).RootElement
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByIdAsync(
group.Id,
Arg.Is<IEnumerable<Guid>>(arg =>
arg.Count() == 3 &&
arg.ToHashSet().SetEquals(userIds)));
}
[Theory]
[BitAutoData]
public async Task PatchGroup_AddListMembers_SuccessIfOnlySomeUsersAreInGroup(
SutProvider<PatchGroupCommandvNext> sutProvider,
Organization organization, Group group,
ICollection<Guid> existingMembers,
ICollection<Guid> userIds)
{
// A user is already in the group, but some still need to be added
userIds.Add(existingMembers.First());
group.OrganizationId = organization.Id;
sutProvider.GetDependency<IGroupRepository>()
.GetManyUserIdsByIdAsync(group.Id, true)
.Returns(existingMembers);
var scimPatchModel = new ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new()
{
Op = "add",
Path = $"members",
Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupRepository>()
.Received(1)
.AddGroupUsersByIdAsync(
group.Id,
Arg.Is<IEnumerable<Guid>>(arg =>
arg.Count() == userIds.Count &&
arg.ToHashSet().SetEquals(userIds)));
}
[Theory]
[BitAutoData]
public async Task PatchGroup_RemoveSingleMember_Success(SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group, Guid userId)
{
group.OrganizationId = organization.Id;
var scimPatchModel = new Models.ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new ScimPatchModel.OperationModel
{
Op = "remove",
Path = $"members[value eq \"{userId}\"]",
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupService>().Received(1).DeleteUserAsync(group, userId, EventSystemUser.SCIM);
}
[Theory]
[BitAutoData]
public async Task PatchGroup_RemoveListMembers_Success(SutProvider<PatchGroupCommandvNext> sutProvider,
Organization organization, Group group, ICollection<Guid> existingMembers)
{
List<Guid> usersToRemove = [existingMembers.First(), existingMembers.Skip(1).First()];
group.OrganizationId = organization.Id;
sutProvider.GetDependency<IGroupRepository>()
.GetManyUserIdsByIdAsync(group.Id)
.Returns(existingMembers);
var scimPatchModel = new Models.ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>
{
new()
{
Op = "remove",
Path = $"members",
Value = JsonDocument.Parse(JsonSerializer.Serialize(usersToRemove.Select(uid => new { value = uid }).ToArray())).RootElement
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
var expectedRemainingUsers = existingMembers.Skip(2).ToList();
await sutProvider.GetDependency<IGroupRepository>()
.Received(1)
.UpdateUsersAsync(
group.Id,
Arg.Is<IEnumerable<Guid>>(arg =>
arg.Count() == expectedRemainingUsers.Count &&
arg.ToHashSet().SetEquals(expectedRemainingUsers)));
}
[Theory]
[BitAutoData]
public async Task PatchGroup_NoAction_Success(
SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group)
{
group.OrganizationId = organization.Id;
var scimPatchModel = new Models.ScimPatchModel
{
Operations = new List<ScimPatchModel.OperationModel>(),
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
};
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default);
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyUserIdsByIdAsync(default);
await sutProvider.GetDependency<IUpdateGroupCommand>().DidNotReceiveWithAnyArgs().UpdateGroupAsync(default, default);
await sutProvider.GetDependency<IGroupService>().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default);
}
}

View File

@ -5,6 +5,7 @@ using Bit.Core;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Core.Vault.Commands.Interfaces; using Bit.Core.Vault.Commands.Interfaces;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Enums; using Bit.Core.Vault.Enums;
using Bit.Core.Vault.Queries; using Bit.Core.Vault.Queries;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
@ -89,11 +90,28 @@ public class SecurityTaskController : Controller
public async Task<ListResponseModel<SecurityTasksResponseModel>> BulkCreateTasks(Guid orgId, public async Task<ListResponseModel<SecurityTasksResponseModel>> BulkCreateTasks(Guid orgId,
[FromBody] BulkCreateSecurityTasksRequestModel model) [FromBody] BulkCreateSecurityTasksRequestModel model)
{ {
var securityTasks = await _createManyTasksCommand.CreateAsync(orgId, model.Tasks); // Retrieve existing pending security tasks for the organization
var pendingSecurityTasks = await _getTasksForOrganizationQuery.GetTasksAsync(orgId, SecurityTaskStatus.Pending);
await _createManyTaskNotificationsCommand.CreateAsync(orgId, securityTasks); // Get the security tasks that are already associated with a cipher within the submitted model
var existingTasks = pendingSecurityTasks.Where(x => model.Tasks.Any(y => y.CipherId == x.CipherId)).ToList();
var response = securityTasks.Select(x => new SecurityTasksResponseModel(x)).ToList(); // Get tasks that need to be created
var tasksToCreateFromModel = model.Tasks.Where(x => !existingTasks.Any(y => y.CipherId == x.CipherId)).ToList();
ICollection<SecurityTask> newSecurityTasks = new List<SecurityTask>();
if (tasksToCreateFromModel.Count != 0)
{
newSecurityTasks = await _createManyTasksCommand.CreateAsync(orgId, tasksToCreateFromModel);
}
// Combine existing tasks and newly created tasks
var allTasks = existingTasks.Concat(newSecurityTasks);
await _createManyTaskNotificationsCommand.CreateAsync(orgId, allTasks);
var response = allTasks.Select(x => new SecurityTasksResponseModel(x)).ToList();
return new ListResponseModel<SecurityTasksResponseModel>(response); return new ListResponseModel<SecurityTasksResponseModel>(response);
} }
} }

View File

@ -0,0 +1,18 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Utilities;
#nullable enable
namespace Bit.Core.AdminConsole.Entities;
public class OrganizationIntegration : ITableObject<Guid>
{
public Guid Id { get; set; }
public Guid OrganizationId { get; set; }
public IntegrationType Type { get; set; }
public string? Configuration { get; set; }
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
public void SetNewId() => Id = CoreHelpers.GenerateComb();
}

View File

@ -0,0 +1,19 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Utilities;
#nullable enable
namespace Bit.Core.AdminConsole.Entities;
public class OrganizationIntegrationConfiguration : ITableObject<Guid>
{
public Guid Id { get; set; }
public Guid OrganizationIntegrationId { get; set; }
public EventType EventType { get; set; }
public string? Configuration { get; set; }
public string? Template { get; set; }
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
public void SetNewId() => Id = CoreHelpers.GenerateComb();
}

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Enums;
public enum IntegrationType : int
{
Slack = 1,
Webhook = 2,
}

View File

@ -106,9 +106,9 @@ public static class FeatureFlagKeys
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint"; public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
public const string DeviceApprovalRequestAdminNotifications = "pm-15637-device-approval-request-admin-notifications"; public const string DeviceApprovalRequestAdminNotifications = "pm-15637-device-approval-request-admin-notifications";
public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission"; public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission";
public const string ShortcutDuplicatePatchRequests = "pm-16812-shortcut-duplicate-patch-requests";
public const string PushSyncOrgKeysOnRevokeRestore = "pm-17168-push-sync-org-keys-on-revoke-restore"; public const string PushSyncOrgKeysOnRevokeRestore = "pm-17168-push-sync-org-keys-on-revoke-restore";
public const string PolicyRequirements = "pm-14439-policy-requirements"; public const string PolicyRequirements = "pm-14439-policy-requirements";
public const string SsoExternalIdVisibility = "pm-18630-sso-external-id-visibility";
public const string ScimInviteUserOptimization = "pm-16811-optimize-invite-user-flow-to-fail-fast"; public const string ScimInviteUserOptimization = "pm-16811-optimize-invite-user-flow-to-fail-fast";
/* Tools Team */ /* Tools Team */

View File

@ -0,0 +1,17 @@
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Bit.Infrastructure.EntityFramework.AdminConsole.Configurations;
public class OrganizationIntegrationConfigurationEntityTypeConfiguration : IEntityTypeConfiguration<OrganizationIntegrationConfiguration>
{
public void Configure(EntityTypeBuilder<OrganizationIntegrationConfiguration> builder)
{
builder
.Property(p => p.Id)
.ValueGeneratedNever();
builder.ToTable(nameof(OrganizationIntegrationConfiguration));
}
}

View File

@ -0,0 +1,26 @@
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace Bit.Infrastructure.EntityFramework.AdminConsole.Configurations;
public class OrganizationIntegrationEntityTypeConfiguration : IEntityTypeConfiguration<OrganizationIntegration>
{
public void Configure(EntityTypeBuilder<OrganizationIntegration> builder)
{
builder
.Property(p => p.Id)
.ValueGeneratedNever();
builder
.HasIndex(p => p.OrganizationId)
.IsClustered(false);
builder
.HasIndex(p => new { p.OrganizationId, p.Type })
.IsUnique()
.IsClustered(false);
builder.ToTable(nameof(OrganizationIntegration));
}
}

View File

@ -0,0 +1,16 @@
using AutoMapper;
namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models;
public class OrganizationIntegration : Core.AdminConsole.Entities.OrganizationIntegration
{
public virtual Organization Organization { get; set; }
}
public class OrganizationIntegrationMapperProfile : Profile
{
public OrganizationIntegrationMapperProfile()
{
CreateMap<Core.AdminConsole.Entities.OrganizationIntegration, OrganizationIntegration>().ReverseMap();
}
}

View File

@ -0,0 +1,16 @@
using AutoMapper;
namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models;
public class OrganizationIntegrationConfiguration : Core.AdminConsole.Entities.OrganizationIntegrationConfiguration
{
public virtual OrganizationIntegration OrganizationIntegration { get; set; }
}
public class OrganizationIntegrationConfigurationMapperProfile : Profile
{
public OrganizationIntegrationConfigurationMapperProfile()
{
CreateMap<Core.AdminConsole.Entities.OrganizationIntegrationConfiguration, OrganizationIntegrationConfiguration>().ReverseMap();
}
}

View File

@ -0,0 +1,20 @@
CREATE PROCEDURE [dbo].[OrganizationIntegrationConfigurationDetails_ReadManyByEventTypeOrganizationIdIntegrationType]
@EventType SMALLINT,
@OrganizationId UNIQUEIDENTIFIER,
@IntegrationType SMALLINT
AS
BEGIN
SET NOCOUNT ON
SELECT
oic.*
FROM
[dbo].[OrganizationIntegrationConfigurationDetailsView] oic
WHERE
oic.[EventType] = @EventType
AND
oic.[OrganizationId] = @OrganizationId
AND
oic.[IntegrationType] = @IntegrationType
END
GO

View File

@ -0,0 +1,20 @@
CREATE TABLE [dbo].[OrganizationIntegration]
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[OrganizationId] UNIQUEIDENTIFIER NOT NULL,
[Type] SMALLINT NOT NULL,
[Configuration] VARCHAR (MAX) NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
[RevisionDate] DATETIME2 (7) NOT NULL,
CONSTRAINT [PK_OrganizationIntegration] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_OrganizationIntegration_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id])
);
GO
CREATE NONCLUSTERED INDEX [IX_OrganizationIntegration_OrganizationId]
ON [dbo].[OrganizationIntegration]([OrganizationId] ASC);
GO
CREATE UNIQUE INDEX [IX_OrganizationIntegration_Organization_Type]
ON [dbo].[OrganizationIntegration]([OrganizationId], [Type]);
GO

View File

@ -0,0 +1,13 @@
CREATE TABLE [dbo].[OrganizationIntegrationConfiguration]
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[OrganizationIntegrationId] UNIQUEIDENTIFIER NOT NULL,
[EventType] SMALLINT NOT NULL,
[Configuration] VARCHAR (MAX) NULL,
[Template] VARCHAR (MAX) NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
[RevisionDate] DATETIME2 (7) NOT NULL,
CONSTRAINT [PK_OrganizationIntegrationConfiguration] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_OrganizationIntegrationConfiguration_OrganizationIntegration] FOREIGN KEY ([OrganizationIntegrationId]) REFERENCES [dbo].[OrganizationIntegration] ([Id])
);
GO

View File

@ -0,0 +1,13 @@
CREATE VIEW [dbo].[OrganizationIntegrationConfigurationDetailsView]
AS
SELECT
oi.[OrganizationId],
oi.[Type] AS [IntegrationType],
oic.[EventType],
oic.[Configuration],
oi.[Configuration] AS [IntegrationConfiguration],
oic.[Template]
FROM
[dbo].[OrganizationIntegrationConfiguration] oic
INNER JOIN
[dbo].[OrganizationIntegration] oi ON oi.[Id] = oic.[OrganizationIntegrationId]

View File

@ -0,0 +1,6 @@
CREATE VIEW [dbo].[OrganizationIntegrationConfigurationView]
AS
SELECT
*
FROM
[dbo].[OrganizationIntegrationConfiguration]

View File

@ -0,0 +1,6 @@
CREATE VIEW [dbo].[OrganizationIntegrationView]
AS
SELECT
*
FROM
[dbo].[OrganizationIntegration]

View File

@ -0,0 +1,101 @@
-- OrganizationIntegration
-- Table
IF OBJECT_ID('[dbo].[OrganizationIntegration]') IS NULL
BEGIN
CREATE TABLE [dbo].[OrganizationIntegration]
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[OrganizationId] UNIQUEIDENTIFIER NOT NULL,
[Type] SMALLINT NOT NULL,
[Configuration] VARCHAR (MAX) NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
[RevisionDate] DATETIME2 (7) NOT NULL,
CONSTRAINT [PK_OrganizationIntegration] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_OrganizationIntegration_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id])
);
CREATE NONCLUSTERED INDEX [IX_OrganizationIntegration_OrganizationId]
ON [dbo].[OrganizationIntegration]([OrganizationId] ASC);
CREATE UNIQUE INDEX [IX_OrganizationIntegration_Organization_Type]
ON [dbo].[OrganizationIntegration]([OrganizationId], [Type]);
END
GO
-- View
IF EXISTS(SELECT *
FROM sys.views
WHERE [Name] = 'OrganizationIntegrationView')
BEGIN
DROP VIEW [dbo].[OrganizationIntegrationView];
END
GO
CREATE VIEW [dbo].[OrganizationIntegrationView]
AS
SELECT
*
FROM
[dbo].[OrganizationIntegration]
GO
-- OrganizationIntegrationConfiguration
-- Table
IF OBJECT_ID('[dbo].[OrganizationIntegrationConfiguration]') IS NULL
BEGIN
CREATE TABLE [dbo].[OrganizationIntegrationConfiguration]
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[OrganizationIntegrationId] UNIQUEIDENTIFIER NOT NULL,
[EventType] SMALLINT NOT NULL,
[Configuration] VARCHAR (MAX) NULL,
[Template] VARCHAR (MAX) NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
[RevisionDate] DATETIME2 (7) NOT NULL,
CONSTRAINT [PK_OrganizationIntegrationConfiguration] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_OrganizationIntegrationConfiguration_OrganizationIntegration] FOREIGN KEY ([OrganizationIntegrationId]) REFERENCES [dbo].[OrganizationIntegration] ([Id])
);
END
GO
-- View
IF EXISTS(SELECT *
FROM sys.views
WHERE [Name] = 'OrganizationIntegrationConfigurationView')
BEGIN
DROP VIEW [dbo].[OrganizationIntegrationConfigurationView];
END
GO
CREATE VIEW [dbo].[OrganizationIntegrationConfigurationView]
AS
SELECT
*
FROM
[dbo].[OrganizationIntegrationConfiguration]
GO
CREATE OR ALTER PROCEDURE [dbo].[OrganizationIntegrationConfiguration_ReadManyByEventTypeOrganizationIdIntegrationType]
@EventType SMALLINT,
@OrganizationId UNIQUEIDENTIFIER,
@IntegrationType SMALLINT
AS
BEGIN
SET NOCOUNT ON
SELECT
oic.*
FROM
[dbo].[OrganizationIntegrationConfigurationView] oic
INNER JOIN
[dbo].[OrganizationIntegration] oi ON oi.[Id] = oic.[OrganizationIntegrationId]
WHERE
oic.[EventType] = @EventType
AND
oi.[OrganizationId] = @OrganizationId
AND
oi.[Type] = @IntegrationType
END
GO

View File

@ -0,0 +1,49 @@
IF EXISTS(SELECT *
FROM sys.views
WHERE [Name] = 'OrganizationIntegrationConfigurationDetailsView')
BEGIN
DROP VIEW [dbo].[OrganizationIntegrationConfigurationDetailsView];
END
GO
CREATE VIEW [dbo].[OrganizationIntegrationConfigurationDetailsView]
AS
SELECT
oi.[OrganizationId],
oi.[Type] AS [IntegrationType],
oic.[EventType],
oic.[Configuration],
oi.[Configuration] AS [IntegrationConfiguration],
oic.[Template]
FROM
[dbo].[OrganizationIntegrationConfiguration] oic
INNER JOIN
[dbo].[OrganizationIntegration] oi ON oi.[Id] = oic.[OrganizationIntegrationId]
GO
IF OBJECT_ID('[dbo].[OrganizationIntegrationConfiguration_ReadManyByEventTypeOrganizationIdIntegrationType]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[OrganizationIntegrationConfiguration_ReadManyByEventTypeOrganizationIdIntegrationType]
END
GO
CREATE OR ALTER PROCEDURE [dbo].[OrganizationIntegrationConfigurationDetails_ReadManyByEventTypeOrganizationIdIntegrationType]
@EventType SMALLINT,
@OrganizationId UNIQUEIDENTIFIER,
@IntegrationType SMALLINT
AS
BEGIN
SET NOCOUNT ON
SELECT
oic.*
FROM
[dbo].[OrganizationIntegrationConfigurationDetailsView] oic
WHERE
oic.[EventType] = @EventType
AND
oic.[OrganizationId] = @OrganizationId
AND
oic.[IntegrationType] = @IntegrationType
END
GO

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,89 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.MySqlMigrations.Migrations;
/// <inheritdoc />
public partial class OrganizationIntegrations : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "OrganizationIntegration",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
OrganizationId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
Type = table.Column<int>(type: "int", nullable: false),
Configuration = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
CreationDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
RevisionDate = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_OrganizationIntegration", x => x.Id);
table.ForeignKey(
name: "FK_OrganizationIntegration_Organization_OrganizationId",
column: x => x.OrganizationId,
principalTable: "Organization",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateTable(
name: "OrganizationIntegrationConfiguration",
columns: table => new
{
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
OrganizationIntegrationId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
EventType = table.Column<int>(type: "int", nullable: false),
Configuration = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
Template = table.Column<string>(type: "longtext", nullable: true)
.Annotation("MySql:CharSet", "utf8mb4"),
CreationDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
RevisionDate = table.Column<DateTime>(type: "datetime(6)", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_OrganizationIntegrationConfiguration", x => x.Id);
table.ForeignKey(
name: "FK_OrganizationIntegrationConfiguration_OrganizationIntegration~",
column: x => x.OrganizationIntegrationId,
principalTable: "OrganizationIntegration",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "IX_OrganizationIntegration_OrganizationId",
table: "OrganizationIntegration",
column: "OrganizationId");
migrationBuilder.CreateIndex(
name: "IX_OrganizationIntegration_OrganizationId_Type",
table: "OrganizationIntegration",
columns: new[] { "OrganizationId", "Type" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_OrganizationIntegrationConfiguration_OrganizationIntegration~",
table: "OrganizationIntegrationConfiguration",
column: "OrganizationIntegrationId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "OrganizationIntegrationConfiguration");
migrationBuilder.DropTable(
name: "OrganizationIntegration");
}
}

View File

@ -217,6 +217,68 @@ namespace Bit.MySqlMigrations.Migrations
b.ToTable("Organization", (string)null); b.ToTable("Organization", (string)null);
}); });
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b =>
{
b.Property<Guid>("Id")
.HasColumnType("char(36)");
b.Property<string>("Configuration")
.HasColumnType("longtext");
b.Property<DateTime>("CreationDate")
.HasColumnType("datetime(6)");
b.Property<Guid>("OrganizationId")
.HasColumnType("char(36)");
b.Property<DateTime>("RevisionDate")
.HasColumnType("datetime(6)");
b.Property<int>("Type")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("OrganizationId")
.HasAnnotation("SqlServer:Clustered", false);
b.HasIndex("OrganizationId", "Type")
.IsUnique()
.HasAnnotation("SqlServer:Clustered", false);
b.ToTable("OrganizationIntegration", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b =>
{
b.Property<Guid>("Id")
.HasColumnType("char(36)");
b.Property<string>("Configuration")
.HasColumnType("longtext");
b.Property<DateTime>("CreationDate")
.HasColumnType("datetime(6)");
b.Property<int>("EventType")
.HasColumnType("int");
b.Property<Guid>("OrganizationIntegrationId")
.HasColumnType("char(36)");
b.Property<DateTime>("RevisionDate")
.HasColumnType("datetime(6)");
b.Property<string>("Template")
.HasColumnType("longtext");
b.HasKey("Id");
b.HasIndex("OrganizationIntegrationId");
b.ToTable("OrganizationIntegrationConfiguration", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -407,10 +469,6 @@ namespace Bit.MySqlMigrations.Migrations
b.Property<DateTime?>("AuthenticationDate") b.Property<DateTime?>("AuthenticationDate")
.HasColumnType("datetime(6)"); .HasColumnType("datetime(6)");
b.Property<string>("RequestCountryName")
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<DateTime>("CreationDate") b.Property<DateTime>("CreationDate")
.HasColumnType("datetime(6)"); .HasColumnType("datetime(6)");
@ -426,6 +484,10 @@ namespace Bit.MySqlMigrations.Migrations
b.Property<string>("PublicKey") b.Property<string>("PublicKey")
.HasColumnType("longtext"); .HasColumnType("longtext");
b.Property<string>("RequestCountryName")
.HasMaxLength(200)
.HasColumnType("varchar(200)");
b.Property<string>("RequestDeviceIdentifier") b.Property<string>("RequestDeviceIdentifier")
.HasMaxLength(50) .HasMaxLength(50)
.HasColumnType("varchar(50)"); .HasColumnType("varchar(50)");
@ -2259,6 +2321,28 @@ namespace Bit.MySqlMigrations.Migrations
b.HasDiscriminator().HasValue("user_service_account"); b.HasDiscriminator().HasValue("user_service_account");
}); });
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
.WithMany()
.HasForeignKey("OrganizationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Organization");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration")
.WithMany()
.HasForeignKey("OrganizationIntegrationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("OrganizationIntegration");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b =>
{ {
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,84 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.PostgresMigrations.Migrations;
/// <inheritdoc />
public partial class OrganizationIntegrations : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "OrganizationIntegration",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
OrganizationId = table.Column<Guid>(type: "uuid", nullable: false),
Type = table.Column<int>(type: "integer", nullable: false),
Configuration = table.Column<string>(type: "text", nullable: true),
CreationDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
RevisionDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_OrganizationIntegration", x => x.Id);
table.ForeignKey(
name: "FK_OrganizationIntegration_Organization_OrganizationId",
column: x => x.OrganizationId,
principalTable: "Organization",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "OrganizationIntegrationConfiguration",
columns: table => new
{
Id = table.Column<Guid>(type: "uuid", nullable: false),
OrganizationIntegrationId = table.Column<Guid>(type: "uuid", nullable: false),
EventType = table.Column<int>(type: "integer", nullable: false),
Configuration = table.Column<string>(type: "text", nullable: true),
Template = table.Column<string>(type: "text", nullable: true),
CreationDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
RevisionDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_OrganizationIntegrationConfiguration", x => x.Id);
table.ForeignKey(
name: "FK_OrganizationIntegrationConfiguration_OrganizationIntegratio~",
column: x => x.OrganizationIntegrationId,
principalTable: "OrganizationIntegration",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_OrganizationIntegration_OrganizationId",
table: "OrganizationIntegration",
column: "OrganizationId");
migrationBuilder.CreateIndex(
name: "IX_OrganizationIntegration_OrganizationId_Type",
table: "OrganizationIntegration",
columns: new[] { "OrganizationId", "Type" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_OrganizationIntegrationConfiguration_OrganizationIntegratio~",
table: "OrganizationIntegrationConfiguration",
column: "OrganizationIntegrationId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "OrganizationIntegrationConfiguration");
migrationBuilder.DropTable(
name: "OrganizationIntegration");
}
}

View File

@ -220,6 +220,68 @@ namespace Bit.PostgresMigrations.Migrations
b.ToTable("Organization", (string)null); b.ToTable("Organization", (string)null);
}); });
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid");
b.Property<string>("Configuration")
.HasColumnType("text");
b.Property<DateTime>("CreationDate")
.HasColumnType("timestamp with time zone");
b.Property<Guid>("OrganizationId")
.HasColumnType("uuid");
b.Property<DateTime>("RevisionDate")
.HasColumnType("timestamp with time zone");
b.Property<int>("Type")
.HasColumnType("integer");
b.HasKey("Id");
b.HasIndex("OrganizationId")
.HasAnnotation("SqlServer:Clustered", false);
b.HasIndex("OrganizationId", "Type")
.IsUnique()
.HasAnnotation("SqlServer:Clustered", false);
b.ToTable("OrganizationIntegration", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uuid");
b.Property<string>("Configuration")
.HasColumnType("text");
b.Property<DateTime>("CreationDate")
.HasColumnType("timestamp with time zone");
b.Property<int>("EventType")
.HasColumnType("integer");
b.Property<Guid>("OrganizationIntegrationId")
.HasColumnType("uuid");
b.Property<DateTime>("RevisionDate")
.HasColumnType("timestamp with time zone");
b.Property<string>("Template")
.HasColumnType("text");
b.HasKey("Id");
b.HasIndex("OrganizationIntegrationId");
b.ToTable("OrganizationIntegrationConfiguration", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -410,10 +472,6 @@ namespace Bit.PostgresMigrations.Migrations
b.Property<DateTime?>("AuthenticationDate") b.Property<DateTime?>("AuthenticationDate")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<string>("RequestCountryName")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<DateTime>("CreationDate") b.Property<DateTime>("CreationDate")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
@ -429,6 +487,10 @@ namespace Bit.PostgresMigrations.Migrations
b.Property<string>("PublicKey") b.Property<string>("PublicKey")
.HasColumnType("text"); .HasColumnType("text");
b.Property<string>("RequestCountryName")
.HasMaxLength(200)
.HasColumnType("character varying(200)");
b.Property<string>("RequestDeviceIdentifier") b.Property<string>("RequestDeviceIdentifier")
.HasMaxLength(50) .HasMaxLength(50)
.HasColumnType("character varying(50)"); .HasColumnType("character varying(50)");
@ -2265,6 +2327,28 @@ namespace Bit.PostgresMigrations.Migrations
b.HasDiscriminator().HasValue("user_service_account"); b.HasDiscriminator().HasValue("user_service_account");
}); });
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
.WithMany()
.HasForeignKey("OrganizationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Organization");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration")
.WithMany()
.HasForeignKey("OrganizationIntegrationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("OrganizationIntegration");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b =>
{ {
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,84 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.SqliteMigrations.Migrations;
/// <inheritdoc />
public partial class OrganizationIntegrations : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "OrganizationIntegration",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
OrganizationId = table.Column<Guid>(type: "TEXT", nullable: false),
Type = table.Column<int>(type: "INTEGER", nullable: false),
Configuration = table.Column<string>(type: "TEXT", nullable: true),
CreationDate = table.Column<DateTime>(type: "TEXT", nullable: false),
RevisionDate = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_OrganizationIntegration", x => x.Id);
table.ForeignKey(
name: "FK_OrganizationIntegration_Organization_OrganizationId",
column: x => x.OrganizationId,
principalTable: "Organization",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "OrganizationIntegrationConfiguration",
columns: table => new
{
Id = table.Column<Guid>(type: "TEXT", nullable: false),
OrganizationIntegrationId = table.Column<Guid>(type: "TEXT", nullable: false),
EventType = table.Column<int>(type: "INTEGER", nullable: false),
Configuration = table.Column<string>(type: "TEXT", nullable: true),
Template = table.Column<string>(type: "TEXT", nullable: true),
CreationDate = table.Column<DateTime>(type: "TEXT", nullable: false),
RevisionDate = table.Column<DateTime>(type: "TEXT", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_OrganizationIntegrationConfiguration", x => x.Id);
table.ForeignKey(
name: "FK_OrganizationIntegrationConfiguration_OrganizationIntegration_OrganizationIntegrationId",
column: x => x.OrganizationIntegrationId,
principalTable: "OrganizationIntegration",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateIndex(
name: "IX_OrganizationIntegration_OrganizationId",
table: "OrganizationIntegration",
column: "OrganizationId");
migrationBuilder.CreateIndex(
name: "IX_OrganizationIntegration_OrganizationId_Type",
table: "OrganizationIntegration",
columns: new[] { "OrganizationId", "Type" },
unique: true);
migrationBuilder.CreateIndex(
name: "IX_OrganizationIntegrationConfiguration_OrganizationIntegrationId",
table: "OrganizationIntegrationConfiguration",
column: "OrganizationIntegrationId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "OrganizationIntegrationConfiguration");
migrationBuilder.DropTable(
name: "OrganizationIntegration");
}
}

View File

@ -212,6 +212,68 @@ namespace Bit.SqliteMigrations.Migrations
b.ToTable("Organization", (string)null); b.ToTable("Organization", (string)null);
}); });
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT");
b.Property<string>("Configuration")
.HasColumnType("TEXT");
b.Property<DateTime>("CreationDate")
.HasColumnType("TEXT");
b.Property<Guid>("OrganizationId")
.HasColumnType("TEXT");
b.Property<DateTime>("RevisionDate")
.HasColumnType("TEXT");
b.Property<int>("Type")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("OrganizationId")
.HasAnnotation("SqlServer:Clustered", false);
b.HasIndex("OrganizationId", "Type")
.IsUnique()
.HasAnnotation("SqlServer:Clustered", false);
b.ToTable("OrganizationIntegration", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b =>
{
b.Property<Guid>("Id")
.HasColumnType("TEXT");
b.Property<string>("Configuration")
.HasColumnType("TEXT");
b.Property<DateTime>("CreationDate")
.HasColumnType("TEXT");
b.Property<int>("EventType")
.HasColumnType("INTEGER");
b.Property<Guid>("OrganizationIntegrationId")
.HasColumnType("TEXT");
b.Property<DateTime>("RevisionDate")
.HasColumnType("TEXT");
b.Property<string>("Template")
.HasColumnType("TEXT");
b.HasKey("Id");
b.HasIndex("OrganizationIntegrationId");
b.ToTable("OrganizationIntegrationConfiguration", (string)null);
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -402,10 +464,6 @@ namespace Bit.SqliteMigrations.Migrations
b.Property<DateTime?>("AuthenticationDate") b.Property<DateTime?>("AuthenticationDate")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("RequestCountryName")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<DateTime>("CreationDate") b.Property<DateTime>("CreationDate")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@ -421,6 +479,10 @@ namespace Bit.SqliteMigrations.Migrations
b.Property<string>("PublicKey") b.Property<string>("PublicKey")
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("RequestCountryName")
.HasMaxLength(200)
.HasColumnType("TEXT");
b.Property<string>("RequestDeviceIdentifier") b.Property<string>("RequestDeviceIdentifier")
.HasMaxLength(50) .HasMaxLength(50)
.HasColumnType("TEXT"); .HasColumnType("TEXT");
@ -2248,6 +2310,28 @@ namespace Bit.SqliteMigrations.Migrations
b.HasDiscriminator().HasValue("user_service_account"); b.HasDiscriminator().HasValue("user_service_account");
}); });
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
.WithMany()
.HasForeignKey("OrganizationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Organization");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b =>
{
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration")
.WithMany()
.HasForeignKey("OrganizationIntegrationId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("OrganizationIntegration");
});
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b => modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b =>
{ {
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")