1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-21 12:05:42 +01:00

[EC-449] Event log user for SCIM events (#2306)

* [EC-449] Added new Enum EventSystemUser

* [EC-449] Added SystemUser property to Event model

* [EC-449] Added SQL migration to add new column 'SystemUserType' to Event

* [EC-449] EF migrations

* [EC-449] Added EventSystemUser to EventResponseModel

* [EC-449] Saving EventSystemUser.SCIM on SCIM controller actions

* [EC-449] Updated Event_Create stored procedure on Sql project

* [EC-449] Fixed SystemUser column name on Event table

* [EC-507] SCIM CQRS Refactor - Groups/Put (#2269)

* [EC-390] Added Scim.Test unit tests project

* [EC-390] Added ConflictException type. Updated BadRequestException to have parameterless constructor. Updated NotFoundException to have constructor with a message parameter

* [EC-531] Implemented CQRS for Groups Put and added unit tests

* [EC-507] Created ScimServiceCollectionExtensions

* [EC-507] Renamed AddScimCommands to AddScimGroupCommands

* [EC-507] Created ExceptionHandlerFilterAttribute on SCIM project

* [EC-507] Removed unneeded dependencies from GroupsController

* [EC-507] Update PutGroupCommand to return Group

PutGroupCommand returns Group and GroupsController creates ScimGroupResponseModel response

* [EC-507] Remove Queries/Commands folders from Scim and Scim.Tests

* [EC-507] Remove unneeded check on empty provided memberIds

* [EC-507] SCIM CQRS Refactor - Groups/GetList (#2272)

* [EC-390] Added Scim.Test unit tests project

* [EC-390] Added ConflictException type. Updated BadRequestException to have parameterless constructor. Updated NotFoundException to have constructor with a message parameter

* [EC-508] Implemented CQRS for Groups GetList and added unit tests

* [EC-507] Created ScimServiceCollectionExtensions and renamed GetGroupsListCommand to GetGroupsListQuery

* [EC-507] Renamed AddScimCommands to AddScimGroupQueries

* [EC-507] Removed unneeded dependencies from GroupsController

* [EC-507] Remove 'Queries' folder from Scim and Scim.Test

* [EC-507] Move ScimListResponseModel from GetGroupsListQuery to Scim.GroupsController

* [EC-507] Remove asserts on IGroupRepository.GetManyByOrganizationIdAsync from unit tests

* [EC-507] SCIM CQRS Refactor - Groups/Get (#2271)

* [EC-390] Added Scim.Test unit tests project

* [EC-390] Added ConflictException type. Updated BadRequestException to have parameterless constructor. Updated NotFoundException to have constructor with a message parameter

* [EC-507] Implemented CQRS for Groups Get and added unit tests

* [EC-507] Created ScimServiceCollectionExtensions and renamed GetGroupCommand to GetGroupQuery

* [EC-507] Renamed AddScimCommands to AddScimGroupQueries

* [EC-507] Created ExceptionHandlerFilterAttribute on SCIM project

* [EC-507] Removed unneeded dependencies from GroupsController

* [EC-507] Sorted order of methods

* [EC-507] Removed GetGroupQuery and moved logic to controller

* [EC-507] Remove 'Queries' folder from Scim and Scim.Test

* [EC-507] SCIM CQRS Refactor - Groups/Patch (#2268)

* [EC-390] Added Scim.Test unit tests project

* [EC-390] Added ConflictException type. Updated BadRequestException to have parameterless constructor. Updated NotFoundException to have constructor with a message parameter

* [EC-532] Implemented CQRS for Groups Patch and added unit tests

* [EC-507] Created ScimServiceCollectionExtensions

* [EC-507] Renamed AddScimCommands to AddScimGroupCommands

* [EC-507] Created ExceptionHandlerFilterAttribute on SCIM project

* [EC-507] Removed unneeded dependencies from GroupsController

* [EC-507] Remove Queries/Commands folders from Scim and Scim.Tests

* [EC-507] Assert group.Name after saving. Assert userIds saved.

* [EC-508] SCIM CQRS Refactor - Users/Delete (#2261)

* [EC-390] Added Scim.Test unit tests project

* [EC-390] Added ConflictException type. Updated BadRequestException to have parameterless constructor. Updated NotFoundException to have constructor with a message parameter

* [EC-539] Implemented CQRS for Users Delete and added unit tests

* [EC-508] Created ScimServiceCollectionExtensions

* [EC-508] Created ExceptionHandlerFilterAttribute on SCIM project

* [EC-508] Removed unneeded model from DeleteUserCommand. Removed unneeded dependencies from UsersController

* [EC-508] Removed Bit.Scim.Models dependency from DeleteUserCommandTests

* [EC-508] Deleted 'DeleteUserCommand' from SCIM; Created commands on Core 'DeleteOrganizationUserCommand', 'PushDeleteUserRegistrationOrganizationCommand' and 'OrganizationHasConfirmedOwnersExceptQuery'

* [EC-508] Changed DeleteOrganizationUserCommand back to using IOrganizationService

* [EC-508] Fixed DeleteOrganizationUserCommand unit tests

* [EC-508] Remove unneeded obsolete comments. Update DeleteUserAsync Obsolete comment with ticket reference

* [EC-508] Move DeleteOrganizationUserCommand to OrganizationFeatures folder

* [EC-508] SCIM CQRS Refactor - Users/Post (#2264)

* [EC-390] Added Scim.Test unit tests project

* [EC-390] Added ConflictException type. Updated BadRequestException to have parameterless constructor. Updated NotFoundException to have constructor with a message parameter

* [EC-536] Implemented CQRS for Users Post and added unit tests

* [EC-508] Created ScimServiceCollectionExtensions

* [EC-508] Renamed AddScimCommands to AddScimUserCommands

* [EC-508] Created ExceptionHandlerFilterAttribute on SCIM project

* [EC-508] Catching NotFoundException on ExceptionHandlerFilter

* [EC-508] Remove Queries/Commands folders from Scim and Scim.Tests

* [EC-508] SCIM CQRS Refactor - Users/Patch (#2262)

* [EC-390] Added Scim.Test unit tests project

* [EC-390] Added ConflictException type. Updated BadRequestException to have parameterless constructor. Updated NotFoundException to have constructor with a message parameter

* [EC-538] Implemented CQRS for Users Patch and added unit tests

* [EC-508] Added ScimServiceCollectionExtensions

* [EC-508] Removed HandleActiveOperationAsync method from UsersController

* [EC-508] Renamed AddScimCommands to AddScimUserCommands

* [EC-508] Created ExceptionHandlerFilterAttribute on SCIM project

* [EC-508] Removed unneeded dependencies from UsersController

* [EC-508] Remove 'Query' folder from Scim and Scim.Test

* [EC-507] SCIM CQRS Refactor - Groups/Post (#2270)

* [EC-390] Added Scim.Test unit tests project

* [EC-390] Added ConflictException type. Updated BadRequestException to have parameterless constructor. Updated NotFoundException to have constructor with a message parameter

* [EC-530] Implemented CQRS for Groups Post and added unit tests

* [EC-507] Created ScimServiceCollectionExtensions

* [EC-507] Renamed AddScimCommands to AddScimGroupCommands

* [EC-507] Created ExceptionHandlerFilterAttribute on SCIM project

* [EC-507] Removed unneeded dependencies from GroupsController

* [EC-507] Remove Queries/Commands folders from Scim and Scim.Test

* [EC-507] Remove unneeded skipIfEmpty argument. Updated unit test to check provided userIds

* [EC-507] Remove UpdateGroupMembersAsync from GroupsController

* [EC-508] SCIM CQRS Refactor - Users/GetList (#2265)

* [EC-390] Added Scim.Test unit tests project

* [EC-390] Added ConflictException type. Updated BadRequestException to have parameterless constructor. Updated NotFoundException to have constructor with a message parameter

* [EC-535] Implemented CQRS for Users GetList and added unit tests

* [EC-508] Created ScimServiceCollectionExtensions and renamed GetUsersListCommand to GetUsersListQuery

* [EC-508] Renamed AddScimCommands to AddScimUserQueries

* [EC-508] Removed unneeded IUserRepository and IOptions<ScimSettings> from UsersController

* [EC-508] Sorted UsersController properties and dependencies

* [EC-508] Remove 'Queries' folder from Scim and Scim.Test

* [EC-508] Move ScimListResponseModel creation to Scim.UsersController

* [EC-508] Move ScimUserResponseModel creation to Scim.UsersController

Co-authored-by: Thomas Rittson <trittson@bitwarden.com>

* [EC-507] SCIM CQRS Refactor - Groups/Delete (#2267)

* [EC-390] Added Scim.Test unit tests project

* [EC-390] Added ConflictException type. Updated BadRequestException to have parameterless constructor. Updated NotFoundException to have constructor with a message parameter

* [EC-533] Implemented CQRS for Groups Delete and added unit tests

* [EC-507] Created ScimServiceCollectionExtensions

* [EC-507] Renamed AddScimCommands to AddScimGroupCommands

* [EC-507] Created ExceptionHandlerFilterAttribute on SCIM project

* [EC-507] Removed unneeded dependencies from GroupsController

* [EC-507] Move DeleteGroupCommand to OrganizationFeatures/OrganizationUsers

* [EC-507] Remove IGetUserQuery and move logic to UsersController. Remove unused references.

* [EC-449] Add overloads for EventService and GroupService methods that accept EventSystemUser as an argument

* [EC-507] Move IDeleteGroupCommand to Groups folder

* [EC-449] Add method overloads in IOrganizationService without EventSystemUser

* [EC-449] Add RevokeUserAsync overload without EventSystemUser

* [EC-449] Reverted OrganizationUsersController to not pass EventSystemUser argument

* [EC-449] Uncomment assertion in GroupServiceTests

* [EC-449] Update method overloads to not have nullable EventSystemUser

* [EC-449] Add unit tests around events that can store EventSystemUser

* [EC-449] Deleted private method GroupService.GroupRepositoryDeleteAsync

* [EC-449] Move Event log call to public DeleteUserAsync methods

* [EC-449] Move call to EventService log to public OrganizationService.InviteUsersAsync methods

* [EC-449] Move EventService call to public OrganizationService.DeleteUserAsync methods

* [EC-449] Move EventService call to OrganizationService.RevokeUserAsync methods

* [EC-449] Move EventService call to OrganizationService.RestoreUserAsync methods

* [EC-449] Add missing comma in SQL script for new SystemUser column on the Event table

* [EC-449] Remove Autofixture hack from OrganizationServiceTests

* [EC-449] Remove invitingUser param when methods expect an EventSystemUser param

* [EC-449] Move DeleteUserAsync validation to private method

* [EC-449] Move revokingUserId from RevokeUserAsync private method

* [EC-449] Move restoringUserId to RestoreUserAsync public method

* [EC-449] Set up OrganizationServiceTest Restore and Revoke tests on a single method

* [EC-449] SaveUsersSendInvitesAsync to return both OrganizationUsers and Events list

* [EC-449] Undo unintended change on CipherRepository

* [EC-449] Add SystemUser value to EventTableEntity

Co-authored-by: Thomas Rittson <trittson@bitwarden.com>
This commit is contained in:
Rui Tomé 2022-11-09 12:13:29 +00:00 committed by GitHub
parent 2d5235b43d
commit 37ed4f43b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 3986 additions and 91 deletions

View File

@ -1,4 +1,5 @@
using Bit.Core.Exceptions;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.Repositories;
using Bit.Scim.Groups.Interfaces;
@ -96,7 +97,7 @@ public class GroupsController : Controller
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(Guid organizationId, Guid id)
{
await _deleteGroupCommand.DeleteGroupAsync(organizationId, id);
await _deleteGroupCommand.DeleteGroupAsync(organizationId, id, EventSystemUser.SCIM);
return new NoContentResult();
}
}

View File

@ -119,7 +119,7 @@ public class UsersController : Controller
[HttpDelete("{id}")]
public async Task<IActionResult> Delete(Guid organizationId, Guid id)
{
await _deleteOrganizationUserCommand.DeleteUserAsync(organizationId, id, null);
await _deleteOrganizationUserCommand.DeleteUserAsync(organizationId, id, EventSystemUser.SCIM);
return new NoContentResult();
}
}

View File

@ -1,4 +1,5 @@
using System.Text.Json;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -48,7 +49,7 @@ public class PatchGroupCommand : IPatchGroupCommand
else if (operation.Path?.ToLowerInvariant() == "displayname")
{
group.Name = operation.Value.GetString();
await _groupService.SaveAsync(group);
await _groupService.SaveAsync(group, EventSystemUser.SCIM);
operationHandled = true;
}
// Replace group name from value object
@ -56,7 +57,7 @@ public class PatchGroupCommand : IPatchGroupCommand
operation.Value.TryGetProperty("displayName", out var displayNameProperty))
{
group.Name = displayNameProperty.GetString();
await _groupService.SaveAsync(group);
await _groupService.SaveAsync(group, EventSystemUser.SCIM);
operationHandled = true;
}
}
@ -94,7 +95,7 @@ public class PatchGroupCommand : IPatchGroupCommand
var removeId = GetOperationPathId(operation.Path);
if (removeId.HasValue)
{
await _groupService.DeleteUserAsync(group, removeId.Value);
await _groupService.DeleteUserAsync(group, removeId.Value, EventSystemUser.SCIM);
operationHandled = true;
}
}

View File

@ -1,4 +1,5 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -38,7 +39,7 @@ public class PostGroupCommand : IPostGroupCommand
}
var group = model.ToGroup(organizationId);
await _groupService.SaveAsync(group, null);
await _groupService.SaveAsync(group, EventSystemUser.SCIM, null);
await UpdateGroupMembersAsync(group, model);
return group;

View File

@ -1,4 +1,5 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -33,7 +34,7 @@ public class PutGroupCommand : IPutGroupCommand
}
group.Name = model.DisplayName;
await _groupService.SaveAsync(group);
await _groupService.SaveAsync(group, EventSystemUser.SCIM);
await UpdateGroupMembersAsync(group, model);
return group;

View File

@ -74,12 +74,12 @@ public class PatchUserCommand : IPatchUserCommand
{
if (active && orgUser.Status == OrganizationUserStatusType.Revoked)
{
await _organizationService.RestoreUserAsync(orgUser, null, _userService);
await _organizationService.RestoreUserAsync(orgUser, EventSystemUser.SCIM, _userService);
return true;
}
else if (!active && orgUser.Status != OrganizationUserStatusType.Revoked)
{
await _organizationService.RevokeUserAsync(orgUser, null);
await _organizationService.RevokeUserAsync(orgUser, EventSystemUser.SCIM);
return true;
}
return false;

View File

@ -79,7 +79,7 @@ public class PostUserCommand : IPostUserCommand
throw new ConflictException();
}
var invitedOrgUser = await _organizationService.InviteUserAsync(organizationId, null, email,
var invitedOrgUser = await _organizationService.InviteUserAsync(organizationId, EventSystemUser.SCIM, email,
OrganizationUserType.User, false, externalId, new List<SelectionReadOnly>());
var orgUser = await _organizationUserRepository.GetDetailsByIdAsync(invitedOrgUser.Id);

View File

@ -1,5 +1,6 @@
using System.Text.Json;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -67,7 +68,7 @@ public class PatchGroupCommandTests
await sutProvider.Sut.PatchGroupAsync(group.OrganizationId, group.Id, scimPatchModel);
await sutProvider.GetDependency<IGroupService>().Received(1).SaveAsync(group);
await sutProvider.GetDependency<IGroupService>().Received(1).SaveAsync(group, EventSystemUser.SCIM);
Assert.Equal(displayName, group.Name);
}
@ -94,7 +95,7 @@ public class PatchGroupCommandTests
await sutProvider.Sut.PatchGroupAsync(group.OrganizationId, group.Id, scimPatchModel);
await sutProvider.GetDependency<IGroupService>().Received(1).SaveAsync(group);
await sutProvider.GetDependency<IGroupService>().Received(1).SaveAsync(group, EventSystemUser.SCIM);
Assert.Equal(displayName, group.Name);
}
@ -182,7 +183,7 @@ public class PatchGroupCommandTests
await sutProvider.Sut.PatchGroupAsync(group.OrganizationId, group.Id, scimPatchModel);
await sutProvider.GetDependency<IGroupService>().Received(1).DeleteUserAsync(group, userId);
await sutProvider.GetDependency<IGroupService>().Received(1).DeleteUserAsync(group, userId, EventSystemUser.SCIM);
}
[Theory]

View File

@ -1,4 +1,5 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -42,7 +43,7 @@ public class PostGroupCommandTests
var group = await sutProvider.Sut.PostGroupAsync(organizationId, scimGroupRequestModel);
await sutProvider.GetDependency<IGroupService>().Received(1).SaveAsync(group, null);
await sutProvider.GetDependency<IGroupService>().Received(1).SaveAsync(group, EventSystemUser.SCIM, null);
await sutProvider.GetDependency<IGroupRepository>().Received(0).UpdateUsersAsync(Arg.Any<Guid>(), Arg.Any<IEnumerable<Guid>>());
AssertHelper.AssertPropertyEqual(expectedResult, group, "Id", "CreationDate", "RevisionDate");
@ -77,7 +78,7 @@ public class PostGroupCommandTests
var group = await sutProvider.Sut.PostGroupAsync(organizationId, scimGroupRequestModel);
await sutProvider.GetDependency<IGroupService>().Received(1).SaveAsync(group, null);
await sutProvider.GetDependency<IGroupService>().Received(1).SaveAsync(group, EventSystemUser.SCIM, null);
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(Arg.Any<Guid>(), Arg.Is<IEnumerable<Guid>>(arg => arg.All(id => membersUserIds.Contains(id))));
AssertHelper.AssertPropertyEqual(expectedResult, group, "Id", "CreationDate", "RevisionDate");

View File

@ -1,4 +1,5 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -45,7 +46,7 @@ public class PutGroupCommandTests
AssertHelper.AssertPropertyEqual(expectedResult, result, "CreationDate", "RevisionDate");
Assert.Equal(displayName, group.Name);
await sutProvider.GetDependency<IGroupService>().Received(1).SaveAsync(group);
await sutProvider.GetDependency<IGroupService>().Received(1).SaveAsync(group, EventSystemUser.SCIM);
await sutProvider.GetDependency<IGroupRepository>().Received(0).UpdateUsersAsync(group.Id, Arg.Any<IEnumerable<Guid>>());
}
@ -82,7 +83,7 @@ public class PutGroupCommandTests
AssertHelper.AssertPropertyEqual(expectedResult, result, "CreationDate", "RevisionDate");
Assert.Equal(displayName, group.Name);
await sutProvider.GetDependency<IGroupService>().Received(1).SaveAsync(group);
await sutProvider.GetDependency<IGroupService>().Received(1).SaveAsync(group, EventSystemUser.SCIM);
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(group.Id, Arg.Is<IEnumerable<Guid>>(arg => arg.All(id => membersUserIds.Contains(id))));
}

View File

@ -1,5 +1,6 @@
using System.Text.Json;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -42,7 +43,7 @@ public class PatchUserCommandTests
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
await sutProvider.GetDependency<IOrganizationService>().Received(1).RestoreUserAsync(organizationUser, null, Arg.Any<IUserService>());
await sutProvider.GetDependency<IOrganizationService>().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM, Arg.Any<IUserService>());
}
[Theory]
@ -70,7 +71,7 @@ public class PatchUserCommandTests
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
await sutProvider.GetDependency<IOrganizationService>().Received(1).RestoreUserAsync(organizationUser, null, Arg.Any<IUserService>());
await sutProvider.GetDependency<IOrganizationService>().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM, Arg.Any<IUserService>());
}
[Theory]
@ -99,7 +100,7 @@ public class PatchUserCommandTests
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
await sutProvider.GetDependency<IOrganizationService>().Received(1).RevokeUserAsync(organizationUser, null);
await sutProvider.GetDependency<IOrganizationService>().Received(1).RevokeUserAsync(organizationUser, EventSystemUser.SCIM);
}
[Theory]
@ -127,7 +128,7 @@ public class PatchUserCommandTests
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
await sutProvider.GetDependency<IOrganizationService>().Received(1).RevokeUserAsync(organizationUser, null);
await sutProvider.GetDependency<IOrganizationService>().Received(1).RevokeUserAsync(organizationUser, EventSystemUser.SCIM);
}
[Theory]
@ -146,8 +147,8 @@ public class PatchUserCommandTests
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
await sutProvider.GetDependency<IOrganizationService>().Received(0).RestoreUserAsync(organizationUser, null, Arg.Any<IUserService>());
await sutProvider.GetDependency<IOrganizationService>().Received(0).RevokeUserAsync(organizationUser, null);
await sutProvider.GetDependency<IOrganizationService>().Received(0).RestoreUserAsync(organizationUser, EventSystemUser.SCIM, Arg.Any<IUserService>());
await sutProvider.GetDependency<IOrganizationService>().Received(0).RevokeUserAsync(organizationUser, EventSystemUser.SCIM);
}
[Theory]

View File

@ -34,12 +34,12 @@ public class PostUserCommandTests
.Returns(organizationUsers);
sutProvider.GetDependency<IOrganizationService>()
.InviteUserAsync(organizationId, null, scimUserRequestModel.PrimaryEmail.ToLowerInvariant(), OrganizationUserType.User, false, externalId, Arg.Any<List<SelectionReadOnly>>())
.InviteUserAsync(organizationId, EventSystemUser.SCIM, scimUserRequestModel.PrimaryEmail.ToLowerInvariant(), OrganizationUserType.User, false, externalId, Arg.Any<List<SelectionReadOnly>>())
.Returns(newUser);
var user = await sutProvider.Sut.PostUserAsync(organizationId, scimUserRequestModel);
await sutProvider.GetDependency<IOrganizationService>().Received(1).InviteUserAsync(organizationId, null, scimUserRequestModel.PrimaryEmail.ToLowerInvariant(),
await sutProvider.GetDependency<IOrganizationService>().Received(1).InviteUserAsync(organizationId, EventSystemUser.SCIM, scimUserRequestModel.PrimaryEmail.ToLowerInvariant(),
OrganizationUserType.User, false, scimUserRequestModel.ExternalId, Arg.Any<List<SelectionReadOnly>>());
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetDetailsByIdAsync(newUser.Id);
}

View File

@ -30,6 +30,7 @@ public class EventResponseModel : ResponseModel
DeviceType = ev.DeviceType;
IpAddress = ev.IpAddress;
InstallationId = ev.InstallationId;
SystemUser = ev.SystemUser;
}
public EventType Type { get; set; }
@ -48,4 +49,5 @@ public class EventResponseModel : ResponseModel
public DateTime Date { get; set; }
public DeviceType? DeviceType { get; set; }
public string IpAddress { get; set; }
public EventSystemUser? SystemUser { get; set; }
}

View File

@ -27,6 +27,7 @@ public class Event : ITableObject<Guid>, IEvent
DeviceType = e.DeviceType;
IpAddress = e.IpAddress;
ActingUserId = e.ActingUserId;
SystemUser = e.SystemUser;
}
public Guid Id { get; set; }
@ -47,6 +48,7 @@ public class Event : ITableObject<Guid>, IEvent
[MaxLength(50)]
public string IpAddress { get; set; }
public Guid? ActingUserId { get; set; }
public EventSystemUser? SystemUser { get; set; }
public void SetNewId()
{

View File

@ -0,0 +1,6 @@
namespace Bit.Core.Enums;
public enum EventSystemUser : byte
{
SCIM = 1
}

View File

@ -31,4 +31,5 @@ public class EventMessage : IEvent
public DeviceType? DeviceType { get; set; }
public string IpAddress { get; set; }
public Guid? IdempotencyId { get; private set; } = Guid.NewGuid();
public EventSystemUser? SystemUser { get; set; }
}

View File

@ -26,6 +26,7 @@ public class EventTableEntity : TableEntity, IEvent
DeviceType = e.DeviceType;
IpAddress = e.IpAddress;
ActingUserId = e.ActingUserId;
SystemUser = e.SystemUser;
}
public DateTime Date { get; set; }
@ -44,6 +45,7 @@ public class EventTableEntity : TableEntity, IEvent
public DeviceType? DeviceType { get; set; }
public string IpAddress { get; set; }
public Guid? ActingUserId { get; set; }
public EventSystemUser? SystemUser { get; set; }
public override IDictionary<string, EntityProperty> WriteEntity(OperationContext operationContext)
{
@ -69,6 +71,16 @@ public class EventTableEntity : TableEntity, IEvent
result.Add(deviceTypeName, new EntityProperty((int?)DeviceType));
}
var systemUserTypeName = nameof(SystemUser);
if (result.ContainsKey(systemUserTypeName))
{
result[systemUserTypeName] = new EntityProperty((int?)SystemUser);
}
else
{
result.Add(systemUserTypeName, new EntityProperty((int?)SystemUser));
}
return result;
}
@ -88,6 +100,12 @@ public class EventTableEntity : TableEntity, IEvent
{
DeviceType = (DeviceType)properties[deviceTypeName].Int32Value.Value;
}
var systemUserTypeName = nameof(SystemUser);
if (properties.ContainsKey(systemUserTypeName) && properties[systemUserTypeName].Int32Value.HasValue)
{
SystemUser = (EventSystemUser)properties[systemUserTypeName].Int32Value.Value;
}
}
public static List<EventTableEntity> IndexEvent(EventMessage e)

View File

@ -20,4 +20,5 @@ public interface IEvent
DeviceType? DeviceType { get; set; }
string IpAddress { get; set; }
DateTime Date { get; set; }
EventSystemUser? SystemUser { get; set; }
}

View File

@ -1,4 +1,6 @@
using Bit.Core.Exceptions;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -17,6 +19,18 @@ public class DeleteGroupCommand : IDeleteGroupCommand
}
public async Task DeleteGroupAsync(Guid organizationId, Guid id)
{
var group = await GroupRepositoryDeleteGroupAsync(organizationId, id);
await _eventService.LogGroupEventAsync(group, Core.Enums.EventType.Group_Deleted);
}
public async Task DeleteGroupAsync(Guid organizationId, Guid id, EventSystemUser eventSystemUser)
{
var group = await GroupRepositoryDeleteGroupAsync(organizationId, id);
await _eventService.LogGroupEventAsync(group, Core.Enums.EventType.Group_Deleted, eventSystemUser);
}
private async Task<Group> GroupRepositoryDeleteGroupAsync(Guid organizationId, Guid id)
{
var group = await _groupRepository.GetByIdAsync(id);
if (group == null || group.OrganizationId != organizationId)
@ -25,6 +39,7 @@ public class DeleteGroupCommand : IDeleteGroupCommand
}
await _groupRepository.DeleteAsync(group);
await _eventService.LogGroupEventAsync(group, Core.Enums.EventType.Group_Deleted);
return group;
}
}

View File

@ -1,6 +1,9 @@
namespace Bit.Core.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.Enums;
namespace Bit.Core.OrganizationFeatures.Groups.Interfaces;
public interface IDeleteGroupCommand
{
Task DeleteGroupAsync(Guid organizationId, Guid id);
Task DeleteGroupAsync(Guid organizationId, Guid id, EventSystemUser eventSystemUser);
}

View File

@ -1,4 +1,5 @@
using Bit.Core.Exceptions;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -20,13 +21,25 @@ public class DeleteOrganizationUserCommand : IDeleteOrganizationUserCommand
}
public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)
{
await ValidateDeleteUserAsync(organizationId, organizationUserId);
await _organizationService.DeleteUserAsync(organizationId, organizationUserId, deletingUserId);
}
public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser)
{
await ValidateDeleteUserAsync(organizationId, organizationUserId);
await _organizationService.DeleteUserAsync(organizationId, organizationUserId, eventSystemUser);
}
private async Task ValidateDeleteUserAsync(Guid organizationId, Guid organizationUserId)
{
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
if (orgUser == null || orgUser.OrganizationId != organizationId)
{
throw new NotFoundException("User not found.");
}
await _organizationService.DeleteUserAsync(organizationId, organizationUserId, deletingUserId);
}
}

View File

@ -1,6 +1,10 @@
namespace Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Enums;
namespace Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
public interface IDeleteOrganizationUserCommand
{
Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId);
Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser);
}

View File

@ -11,9 +11,12 @@ public interface IEventService
Task LogCipherEventsAsync(IEnumerable<Tuple<Cipher, EventType, DateTime?>> events);
Task LogCollectionEventAsync(Collection collection, EventType type, DateTime? date = null);
Task LogGroupEventAsync(Group group, EventType type, DateTime? date = null);
Task LogGroupEventAsync(Group group, EventType type, EventSystemUser systemUser, DateTime? date = null);
Task LogPolicyEventAsync(Policy policy, EventType type, DateTime? date = null);
Task LogOrganizationUserEventAsync(OrganizationUser organizationUser, EventType type, DateTime? date = null);
Task LogOrganizationUserEventAsync(OrganizationUser organizationUser, EventType type, EventSystemUser systemUser, DateTime? date = null);
Task LogOrganizationUserEventsAsync(IEnumerable<(OrganizationUser, EventType, DateTime?)> events);
Task LogOrganizationUserEventsAsync(IEnumerable<(OrganizationUser, EventType, EventSystemUser, DateTime?)> events);
Task LogOrganizationEventAsync(Organization organization, EventType type, DateTime? date = null);
Task LogProviderUserEventAsync(ProviderUser providerUser, EventType type, DateTime? date = null);
Task LogProviderUsersEventAsync(IEnumerable<(ProviderUser, EventType, DateTime?)> events);

View File

@ -1,4 +1,5 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.Data;
namespace Bit.Core.Services;
@ -6,7 +7,11 @@ namespace Bit.Core.Services;
public interface IGroupService
{
Task SaveAsync(Group group, IEnumerable<SelectionReadOnly> collections = null);
Task SaveAsync(Group group, EventSystemUser systemUser, IEnumerable<SelectionReadOnly> collections = null);
[Obsolete("IDeleteGroupCommand should be used instead. To be removed by EC-608.")]
Task DeleteAsync(Group group);
[Obsolete("IDeleteGroupCommand should be used instead. To be removed by EC-608.")]
Task DeleteAsync(Group group, EventSystemUser systemUser);
Task DeleteUserAsync(Group group, Guid organizationUserId);
Task DeleteUserAsync(Group group, Guid organizationUserId, EventSystemUser systemUser);
}

View File

@ -31,8 +31,12 @@ public interface IOrganizationService
Task DisableTwoFactorProviderAsync(Organization organization, TwoFactorProviderType type);
Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, Guid? invitingUserId,
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites);
Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, EventSystemUser systemUser,
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites);
Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email,
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<SelectionReadOnly> collections);
Task<OrganizationUser> InviteUserAsync(Guid organizationId, EventSystemUser systemUser, string email,
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<SelectionReadOnly> collections);
Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId);
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId);
Task<OrganizationUser> AcceptUserAsync(Guid organizationUserId, User user, string token,
@ -45,6 +49,8 @@ public interface IOrganizationService
Task SaveUserAsync(OrganizationUser user, Guid? savingUserId, IEnumerable<SelectionReadOnly> collections);
[Obsolete("IDeleteOrganizationUserCommand should be used instead. To be removed by EC-607.")]
Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId);
[Obsolete("IDeleteOrganizationUserCommand should be used instead. To be removed by EC-607.")]
Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser systemUser);
Task DeleteUserAsync(Guid organizationId, Guid userId);
Task<List<Tuple<OrganizationUser, string>>> DeleteUsersAsync(Guid organizationId,
IEnumerable<Guid> organizationUserIds, Guid? deletingUserId);
@ -60,9 +66,11 @@ public interface IOrganizationService
Task<Organization> UpdateOrganizationKeysAsync(Guid orgId, string publicKey, string privateKey);
Task<bool> HasConfirmedOwnersExceptAsync(Guid organizationId, IEnumerable<Guid> organizationUsersId, bool includeProvider = true);
Task RevokeUserAsync(OrganizationUser organizationUser, Guid? revokingUserId);
Task RevokeUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser);
Task<List<Tuple<OrganizationUser, string>>> RevokeUsersAsync(Guid organizationId,
IEnumerable<Guid> organizationUserIds, Guid? revokingUserId);
Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId, IUserService userService);
Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser, IUserService userService);
Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,
IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService);
Task<int> GetOccupiedSeatCount(Organization organization);

View File

@ -155,7 +155,19 @@ public class EventService : IEventService
await _eventWriteService.CreateAsync(e);
}
public async Task LogGroupEventAsync(Group group, EventType type, DateTime? date = null)
public async Task LogGroupEventAsync(Group group, EventType type,
DateTime? date = null)
{
await CreateLogGroupEventAsync(group, type, systemUser: null, date);
}
public async Task LogGroupEventAsync(Group group, EventType type, EventSystemUser systemUser,
DateTime? date = null)
{
await CreateLogGroupEventAsync(group, type, systemUser, date);
}
private async Task CreateLogGroupEventAsync(Group group, EventType type, EventSystemUser? systemUser, DateTime? date = null)
{
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
if (!CanUseEvents(orgAbilities, group.OrganizationId))
@ -170,7 +182,8 @@ public class EventService : IEventService
Type = type,
ActingUserId = _currentContext?.UserId,
ProviderId = await GetProviderIdAsync(@group.OrganizationId),
Date = date.GetValueOrDefault(DateTime.UtcNow)
Date = date.GetValueOrDefault(DateTime.UtcNow),
SystemUser = systemUser
};
await _eventWriteService.CreateAsync(e);
}
@ -197,13 +210,29 @@ public class EventService : IEventService
public async Task LogOrganizationUserEventAsync(OrganizationUser organizationUser, EventType type,
DateTime? date = null) =>
await LogOrganizationUserEventsAsync(new[] { (organizationUser, type, date) });
await CreateLogOrganizationUserEventsAsync(new (OrganizationUser, EventType, EventSystemUser?, DateTime?)[] { (organizationUser, type, null, date) });
public async Task LogOrganizationUserEventsAsync(IEnumerable<(OrganizationUser, EventType, DateTime?)> events)
public async Task LogOrganizationUserEventAsync(OrganizationUser organizationUser, EventType type,
EventSystemUser systemUser, DateTime? date = null) =>
await CreateLogOrganizationUserEventsAsync(new (OrganizationUser, EventType, EventSystemUser?, DateTime?)[] { (organizationUser, type, systemUser, date) });
public async Task LogOrganizationUserEventsAsync(
IEnumerable<(OrganizationUser, EventType, DateTime?)> events)
{
await CreateLogOrganizationUserEventsAsync(events.Select(e => (e.Item1, e.Item2, (EventSystemUser?)null, e.Item3)));
}
public async Task LogOrganizationUserEventsAsync(
IEnumerable<(OrganizationUser, EventType, EventSystemUser, DateTime?)> events)
{
await CreateLogOrganizationUserEventsAsync(events.Select(e => (e.Item1, e.Item2, (EventSystemUser?)e.Item3, e.Item4)));
}
private async Task CreateLogOrganizationUserEventsAsync(IEnumerable<(OrganizationUser, EventType, EventSystemUser?, DateTime?)> events)
{
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
var eventMessages = new List<IEvent>();
foreach (var (organizationUser, type, date) in events)
foreach (var (organizationUser, type, systemUser, date) in events)
{
if (!CanUseEvents(orgAbilities, organizationUser.OrganizationId))
{
@ -218,7 +247,8 @@ public class EventService : IEventService
ProviderId = await GetProviderIdAsync(organizationUser.OrganizationId),
Type = type,
ActingUserId = _currentContext?.UserId,
Date = date.GetValueOrDefault(DateTime.UtcNow)
Date = date.GetValueOrDefault(DateTime.UtcNow),
SystemUser = systemUser
});
}

View File

@ -29,7 +29,19 @@ public class GroupService : IGroupService
_referenceEventService = referenceEventService;
}
public async Task SaveAsync(Group group, IEnumerable<SelectionReadOnly> collections = null)
public async Task SaveAsync(Group group,
IEnumerable<SelectionReadOnly> collections = null)
{
await GroupRepositorySaveAsync(group, systemUser: null, collections);
}
public async Task SaveAsync(Group group, EventSystemUser systemUser,
IEnumerable<SelectionReadOnly> collections = null)
{
await GroupRepositorySaveAsync(group, systemUser, collections);
}
private async Task GroupRepositorySaveAsync(Group group, EventSystemUser? systemUser, IEnumerable<SelectionReadOnly> collections = null)
{
var org = await _organizationRepository.GetByIdAsync(group.OrganizationId);
if (org == null)
@ -55,7 +67,15 @@ public class GroupService : IGroupService
await _groupRepository.CreateAsync(group, collections);
}
await _eventService.LogGroupEventAsync(group, Enums.EventType.Group_Created);
if (systemUser.HasValue)
{
await _eventService.LogGroupEventAsync(group, Enums.EventType.Group_Created, systemUser.Value);
}
else
{
await _eventService.LogGroupEventAsync(group, Enums.EventType.Group_Created);
}
await _referenceEventService.RaiseEventAsync(new ReferenceEvent(ReferenceEventType.GroupCreated, org));
}
else
@ -71,7 +91,14 @@ public class GroupService : IGroupService
await _groupRepository.ReplaceAsync(group, collections);
}
await _eventService.LogGroupEventAsync(group, Enums.EventType.Group_Updated);
if (systemUser.HasValue)
{
await _eventService.LogGroupEventAsync(group, Enums.EventType.Group_Updated, systemUser.Value);
}
else
{
await _eventService.LogGroupEventAsync(group, Enums.EventType.Group_Updated);
}
}
}
@ -79,17 +106,38 @@ public class GroupService : IGroupService
public async Task DeleteAsync(Group group)
{
await _groupRepository.DeleteAsync(group);
await _eventService.LogGroupEventAsync(group, Enums.EventType.Group_Deleted);
await _eventService.LogGroupEventAsync(group, EventType.Group_Deleted);
}
[Obsolete("IDeleteGroupCommand should be used instead. To be removed by EC-608.")]
public async Task DeleteAsync(Group group, EventSystemUser systemUser)
{
await _groupRepository.DeleteAsync(group);
await _eventService.LogGroupEventAsync(group, EventType.Group_Deleted, systemUser);
}
public async Task DeleteUserAsync(Group group, Guid organizationUserId)
{
var orgUser = await GroupRepositoryDeleteUserAsync(group, organizationUserId, systemUser: null);
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_UpdatedGroups);
}
public async Task DeleteUserAsync(Group group, Guid organizationUserId, EventSystemUser systemUser)
{
var orgUser = await GroupRepositoryDeleteUserAsync(group, organizationUserId, systemUser);
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_UpdatedGroups, systemUser);
}
private async Task<OrganizationUser> GroupRepositoryDeleteUserAsync(Group group, Guid organizationUserId, EventSystemUser? systemUser)
{
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
if (orgUser == null || orgUser.OrganizationId != group.OrganizationId)
{
throw new NotFoundException();
}
await _groupRepository.DeleteUserAsync(group.Id, organizationUserId);
await _eventService.LogOrganizationUserEventAsync(orgUser, Enums.EventType.OrganizationUser_UpdatedGroups);
return orgUser;
}
}

View File

@ -1115,13 +1115,6 @@ public class OrganizationService : IOrganizationService
public async Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, Guid? invitingUserId,
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites)
{
var organization = await GetOrgById(organizationId);
var initialSeatCount = organization.Seats;
if (organization == null || invites.Any(i => i.invite.Emails == null))
{
throw new NotFoundException();
}
var inviteTypes = new HashSet<OrganizationUserType>(invites.Where(i => i.invite.Type.HasValue)
.Select(i => i.invite.Type.Value));
if (invitingUserId.HasValue && inviteTypes.Count > 0)
@ -1132,6 +1125,33 @@ public class OrganizationService : IOrganizationService
}
}
var (organizationUsers, events) = await SaveUsersSendInvitesAsync(organizationId, invites);
await _eventService.LogOrganizationUserEventsAsync(events);
return organizationUsers;
}
public async Task<List<OrganizationUser>> InviteUsersAsync(Guid organizationId, EventSystemUser systemUser,
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites)
{
var (organizationUsers, events) = await SaveUsersSendInvitesAsync(organizationId, invites);
await _eventService.LogOrganizationUserEventsAsync(events.Select(e => (e.Item1, e.Item2, systemUser, e.Item3)));
return organizationUsers;
}
private async Task<(List<OrganizationUser> organizationUsers, List<(OrganizationUser, EventType, DateTime?)> events)> SaveUsersSendInvitesAsync(Guid organizationId,
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites)
{
var organization = await GetOrgById(organizationId);
var initialSeatCount = organization.Seats;
if (organization == null || invites.Any(i => i.invite.Emails == null))
{
throw new NotFoundException();
}
var newSeatsRequired = 0;
var existingEmails = new HashSet<string>(await _organizationUserRepository.SelectKnownEmailsAsync(
organizationId, invites.SelectMany(i => i.invite.Emails), false), StringComparer.InvariantCultureIgnoreCase);
@ -1157,7 +1177,6 @@ public class OrganizationService : IOrganizationService
throw new BadRequestException("Organization must have at least one confirmed owner.");
}
var orgUsers = new List<OrganizationUser>();
var limitedCollectionOrgUsers = new List<(OrganizationUser, IEnumerable<SelectionReadOnly>)>();
var orgUserInvitedCount = 0;
@ -1235,7 +1254,6 @@ public class OrganizationService : IOrganizationService
await AutoAddSeatsAsync(organization, newSeatsRequired, prorationDate);
await SendInvitesAsync(orgUsers.Concat(limitedCollectionOrgUsers.Select(u => u.Item1)), organization);
await _eventService.LogOrganizationUserEventsAsync(events);
await _referenceEventService.RaiseEventAsync(
new ReferenceEvent(ReferenceEventType.InvitedUsers, organization)
@ -1263,7 +1281,7 @@ public class OrganizationService : IOrganizationService
throw new AggregateException("One or more errors occurred while inviting users.", exceptions);
}
return orgUsers;
return (orgUsers, events);
}
public async Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId,
@ -1647,6 +1665,20 @@ public class OrganizationService : IOrganizationService
[Obsolete("IDeleteOrganizationUserCommand should be used instead. To be removed by EC-607.")]
public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)
{
var orgUser = await RepositoryDeleteUserAsync(organizationId, organizationUserId, deletingUserId);
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed);
}
[Obsolete("IDeleteOrganizationUserCommand should be used instead. To be removed by EC-607.")]
public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId,
EventSystemUser systemUser)
{
var orgUser = await RepositoryDeleteUserAsync(organizationId, organizationUserId, null);
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed, systemUser);
}
private async Task<OrganizationUser> RepositoryDeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)
{
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
if (orgUser == null || orgUser.OrganizationId != organizationId)
@ -1671,12 +1703,13 @@ public class OrganizationService : IOrganizationService
}
await _organizationUserRepository.DeleteAsync(orgUser);
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed);
if (orgUser.UserId.HasValue)
{
await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value);
}
return orgUser;
}
public async Task DeleteUserAsync(Guid organizationId, Guid userId)
@ -1852,6 +1885,18 @@ public class OrganizationService : IOrganizationService
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email,
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<SelectionReadOnly> collections)
{
return await SaveUserSendInviteAsync(organizationId, invitingUserId, systemUser: null, email, type, accessAll, externalId, collections);
}
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, EventSystemUser systemUser, string email,
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<SelectionReadOnly> collections)
{
return await SaveUserSendInviteAsync(organizationId, invitingUserId: null, systemUser, email, type, accessAll, externalId, collections);
}
private async Task<OrganizationUser> SaveUserSendInviteAsync(Guid organizationId, Guid? invitingUserId, EventSystemUser? systemUser, string email,
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<SelectionReadOnly> collections)
{
var invite = new OrganizationUserInvite()
{
@ -1860,7 +1905,8 @@ public class OrganizationService : IOrganizationService
AccessAll = accessAll,
Collections = collections,
};
var results = await InviteUsersAsync(organizationId, invitingUserId,
var results = systemUser.HasValue ? await InviteUsersAsync(organizationId, systemUser.Value,
new (OrganizationUserInvite, string)[] { (invite, externalId) }) : await InviteUsersAsync(organizationId, invitingUserId,
new (OrganizationUserInvite, string)[] { (invite, externalId) });
var result = results.FirstOrDefault();
if (result == null)
@ -2221,11 +2267,6 @@ public class OrganizationService : IOrganizationService
public async Task RevokeUserAsync(OrganizationUser organizationUser, Guid? revokingUserId)
{
if (organizationUser.Status == OrganizationUserStatusType.Revoked)
{
throw new BadRequestException("Already revoked.");
}
if (revokingUserId.HasValue && organizationUser.UserId == revokingUserId.Value)
{
throw new BadRequestException("You cannot revoke yourself.");
@ -2237,6 +2278,24 @@ public class OrganizationService : IOrganizationService
throw new BadRequestException("Only owners can revoke other owners.");
}
await RepositoryRevokeUserAsync(organizationUser);
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked);
}
public async Task RevokeUserAsync(OrganizationUser organizationUser,
EventSystemUser systemUser)
{
await RepositoryRevokeUserAsync(organizationUser);
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked, systemUser);
}
private async Task RepositoryRevokeUserAsync(OrganizationUser organizationUser)
{
if (organizationUser.Status == OrganizationUserStatusType.Revoked)
{
throw new BadRequestException("Already revoked.");
}
if (!await HasConfirmedOwnersExceptAsync(organizationUser.OrganizationId, new[] { organizationUser.Id }))
{
throw new BadRequestException("Organization must have at least one confirmed owner.");
@ -2244,7 +2303,6 @@ public class OrganizationService : IOrganizationService
await _organizationUserRepository.RevokeAsync(organizationUser.Id);
organizationUser.Status = OrganizationUserStatusType.Revoked;
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked);
}
public async Task<List<Tuple<OrganizationUser, string>>> RevokeUsersAsync(Guid organizationId,
@ -2306,13 +2364,9 @@ public class OrganizationService : IOrganizationService
return result;
}
public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId, IUserService userService)
public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId,
IUserService userService)
{
if (organizationUser.Status != OrganizationUserStatusType.Revoked)
{
throw new BadRequestException("Already active.");
}
if (restoringUserId.HasValue && organizationUser.UserId == restoringUserId.Value)
{
throw new BadRequestException("You cannot restore yourself.");
@ -2324,6 +2378,24 @@ public class OrganizationService : IOrganizationService
throw new BadRequestException("Only owners can restore other owners.");
}
await RepositoryRestoreUserAsync(organizationUser, userService);
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
}
public async Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser,
IUserService userService)
{
await RepositoryRestoreUserAsync(organizationUser, userService);
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored, systemUser);
}
private async Task RepositoryRestoreUserAsync(OrganizationUser organizationUser, IUserService userService)
{
if (organizationUser.Status != OrganizationUserStatusType.Revoked)
{
throw new BadRequestException("Already active.");
}
var organization = await _organizationRepository.GetByIdAsync(organizationUser.OrganizationId);
var occupiedSeats = await GetOccupiedSeatCount(organization);
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
@ -2338,7 +2410,6 @@ public class OrganizationService : IOrganizationService
await _organizationUserRepository.RestoreAsync(organizationUser.Id, status);
organizationUser.Status = status;
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
}
public async Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,

View File

@ -31,6 +31,11 @@ public class NoopEventService : IEventService
return Task.FromResult(0);
}
public Task LogGroupEventAsync(Group group, EventType type, EventSystemUser systemUser, DateTime? date = null)
{
return Task.FromResult(0);
}
public Task LogOrganizationEventAsync(Organization organization, EventType type, DateTime? date = null)
{
return Task.FromResult(0);
@ -52,8 +57,13 @@ public class NoopEventService : IEventService
return Task.FromResult(0);
}
public Task LogOrganizationUserEventAsync(OrganizationUser organizationUser, EventType type, DateTime? date = null)
{
return Task.FromResult(0);
}
public Task LogOrganizationUserEventAsync(OrganizationUser organizationUser, EventType type,
DateTime? date = null)
EventSystemUser systemUser, DateTime? date = null)
{
return Task.FromResult(0);
}
@ -63,6 +73,11 @@ public class NoopEventService : IEventService
return Task.FromResult(0);
}
public Task LogOrganizationUserEventsAsync(IEnumerable<(OrganizationUser, EventType, EventSystemUser, DateTime?)> events)
{
return Task.FromResult(0);
}
public Task LogUserEventAsync(Guid userId, EventType type, DateTime? date = null)
{
return Task.FromResult(0);

View File

@ -562,8 +562,7 @@ public class CipherRepository : Repository<Core.Entities.Cipher, Cipher, Guid>,
var attachments = string.IsNullOrWhiteSpace(cipher.Attachments) ?
new Dictionary<string, CipherAttachment.MetaData>() :
JsonConvert.DeserializeObject<Dictionary<string, CipherAttachment.MetaData>>(cipher.Attachments);
var metaData = JsonConvert.DeserializeObject<CipherAttachment.MetaData>(attachment.AttachmentData);
attachments[attachment.AttachmentId] = metaData;
attachments.Add(attachment.AttachmentId, JsonConvert.DeserializeObject<CipherAttachment.MetaData>(attachment.AttachmentData));
cipher.Attachments = JsonConvert.SerializeObject(attachments);
await dbContext.SaveChangesAsync();

View File

@ -15,7 +15,8 @@
@ActingUserId UNIQUEIDENTIFIER,
@DeviceType SMALLINT,
@IpAddress VARCHAR(50),
@Date DATETIME2(7)
@Date DATETIME2(7),
@SystemUser TINYINT = null
AS
BEGIN
SET NOCOUNT ON
@ -38,7 +39,8 @@ BEGIN
[ActingUserId],
[DeviceType],
[IpAddress],
[Date]
[Date],
[SystemUser]
)
VALUES
(
@ -58,6 +60,7 @@ BEGIN
@ActingUserId,
@DeviceType,
@IpAddress,
@Date
@Date,
@SystemUser
)
END

View File

@ -16,6 +16,7 @@
[ProviderId] UNIQUEIDENTIFIER NULL,
[ProviderUserId] UNIQUEIDENTIFIER NULL,
[ProviderOrganizationId] UNIQUEIDENTIFIER NULL,
[SystemUser] TINYINT NULL,
CONSTRAINT [PK_Event] PRIMARY KEY CLUSTERED ([Id] ASC)
);

View File

@ -1,4 +1,5 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.Groups;
using Bit.Core.Repositories;
@ -48,4 +49,18 @@ public class DeleteGroupCommandTests
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteGroupAsync(organizationId, groupId));
}
[Theory]
[BitAutoData]
public async Task DeleteGroup_WithEventSystemUser_Success(SutProvider<DeleteGroupCommand> sutProvider, Group group, EventSystemUser eventSystemUser)
{
sutProvider.GetDependency<IGroupRepository>()
.GetByIdAsync(group.Id)
.Returns(group);
await sutProvider.Sut.DeleteGroupAsync(group.OrganizationId, group.Id, eventSystemUser);
await sutProvider.GetDependency<IGroupRepository>().Received(1).DeleteAsync(group);
await sutProvider.GetDependency<IEventService>().Received(1).LogGroupEventAsync(group, Core.Enums.EventType.Group_Deleted, eventSystemUser);
}
}

View File

@ -1,4 +1,5 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationUsers;
using Bit.Core.Repositories;
@ -51,4 +52,21 @@ public class DeleteOrganizationUserCommandTests
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteUserAsync(organizationId, organizationUserId, null));
}
[Theory]
[BitAutoData]
public async Task DeleteUser_WithEventSystemUser_Success(SutProvider<DeleteOrganizationUserCommand> sutProvider, Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser)
{
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUserId)
.Returns(new OrganizationUser
{
Id = organizationUserId,
OrganizationId = organizationId
});
await sutProvider.Sut.DeleteUserAsync(organizationId, organizationUserId, eventSystemUser);
await sutProvider.GetDependency<IOrganizationService>().Received(1).DeleteUserAsync(organizationId, organizationUserId, eventSystemUser);
}
}

View File

@ -21,6 +21,56 @@ public class EventServiceTests
Enum.GetValues<EventType>().Select(e => (object)e)
).Select(p => p.ToArray());
[Theory, BitAutoData]
public async Task LogGroupEvent_LogsRequiredInfo(Group group, EventType eventType, DateTime date,
Guid actingUserId, Guid providerId, DeviceType deviceType, SutProvider<EventService> sutProvider)
{
var orgAbilities = new Dictionary<Guid, OrganizationAbility>()
{
{ group.OrganizationId, new OrganizationAbility() { UseEvents = true, Enabled = true } }
};
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(orgAbilities);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().DeviceType.Returns(deviceType);
sutProvider.GetDependency<ICurrentContext>().ProviderIdForOrg(Arg.Any<Guid>()).Returns(providerId);
await sutProvider.Sut.LogGroupEventAsync(group, eventType, date);
await sutProvider.GetDependency<IEventWriteService>().Received(1).CreateAsync(Arg.Is<IEvent>(e =>
e.OrganizationId == group.OrganizationId &&
e.GroupId == group.Id &&
e.Type == eventType &&
e.ActingUserId == actingUserId &&
e.ProviderId == providerId &&
e.Date == date &&
e.SystemUser == null));
}
[Theory, BitAutoData]
public async Task LogGroupEvent_WithEventSystemUser_LogsRequiredInfo(Group group, EventType eventType, EventSystemUser eventSystemUser, DateTime date,
Guid actingUserId, Guid providerId, DeviceType deviceType, SutProvider<EventService> sutProvider)
{
var orgAbilities = new Dictionary<Guid, OrganizationAbility>()
{
{ group.OrganizationId, new OrganizationAbility() { UseEvents = true, Enabled = true } }
};
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(orgAbilities);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().DeviceType.Returns(deviceType);
sutProvider.GetDependency<ICurrentContext>().ProviderIdForOrg(Arg.Any<Guid>()).Returns(providerId);
await sutProvider.Sut.LogGroupEventAsync(group, eventType, eventSystemUser, date);
await sutProvider.GetDependency<IEventWriteService>().Received(1).CreateAsync(Arg.Is<IEvent>(e =>
e.OrganizationId == group.OrganizationId &&
e.GroupId == group.Id &&
e.Type == eventType &&
e.ActingUserId == actingUserId &&
e.ProviderId == providerId &&
e.Date == date &&
e.SystemUser == eventSystemUser));
}
[Theory]
[BitMemberAutoData(nameof(InstallationIdTestCases))]
public async Task LogOrganizationEvent_ProvidesInstallationId(Guid? installationId, EventType eventType,
@ -73,6 +123,41 @@ public class EventServiceTests
await sutProvider.GetDependency<IEventWriteService>().Received(1).CreateManyAsync(Arg.Is(AssertHelper.AssertPropertyEqual<IEvent>(expected, new[] { "IdempotencyId" })));
}
[Theory, BitAutoData]
public async Task LogOrganizationUserEvent_WithEventSystemUser_LogsRequiredInfo(OrganizationUser orgUser, EventType eventType, EventSystemUser eventSystemUser, DateTime date,
Guid actingUserId, Guid providerId, string ipAddress, DeviceType deviceType, SutProvider<EventService> sutProvider)
{
var orgAbilities = new Dictionary<Guid, OrganizationAbility>()
{
{orgUser.OrganizationId, new OrganizationAbility() { UseEvents = true, Enabled = true } }
};
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(orgAbilities);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().IpAddress.Returns(ipAddress);
sutProvider.GetDependency<ICurrentContext>().ProviderIdForOrg(Arg.Any<Guid>()).Returns(providerId);
sutProvider.GetDependency<ICurrentContext>().DeviceType.Returns(deviceType);
await sutProvider.Sut.LogOrganizationUserEventAsync(orgUser, eventType, eventSystemUser, date);
var expected = new List<IEvent>() {
new EventMessage()
{
IpAddress = ipAddress,
DeviceType = deviceType,
OrganizationId = orgUser.OrganizationId,
UserId = orgUser.UserId,
OrganizationUserId = orgUser.Id,
ProviderId = providerId,
Type = eventType,
ActingUserId = actingUserId,
Date = date,
SystemUser = eventSystemUser
}
};
await sutProvider.GetDependency<IEventWriteService>().Received(1).CreateManyAsync(Arg.Is(AssertHelper.AssertPropertyEqual<IEvent>(expected, new[] { "IdempotencyId" })));
}
[Theory, BitAutoData]
public async Task LogProviderUserEvent_LogsRequiredInfo(ProviderUser providerUser, EventType eventType, DateTime date,
Guid actingUserId, Guid providerId, string ipAddress, DeviceType deviceType, SutProvider<EventService> sutProvider)

View File

@ -27,8 +27,23 @@ public class GroupServiceTests
await sutProvider.Sut.SaveAsync(group);
await sutProvider.GetDependency<IGroupRepository>().Received().CreateAsync(group);
await sutProvider.GetDependency<IEventService>().Received()
.LogGroupEventAsync(group, EventType.Group_Created);
await sutProvider.GetDependency<IEventService>().Received().LogGroupEventAsync(group, EventType.Group_Created);
Assert.True(group.CreationDate - utcNow < TimeSpan.FromSeconds(1));
Assert.True(group.RevisionDate - utcNow < TimeSpan.FromSeconds(1));
}
[Theory, BitAutoData]
public async Task SaveAsync_DefaultGroupId_WithEventSystemUser_CreatesGroupInRepository(Group group, Organization organization, EventSystemUser eventSystemUser, SutProvider<GroupService> sutProvider)
{
group.Id = default(Guid);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
organization.UseGroups = true;
var utcNow = DateTime.UtcNow;
await sutProvider.Sut.SaveAsync(group, eventSystemUser);
await sutProvider.GetDependency<IGroupRepository>().Received().CreateAsync(group);
await sutProvider.GetDependency<IEventService>().Received().LogGroupEventAsync(group, EventType.Group_Created, eventSystemUser);
Assert.True(group.CreationDate - utcNow < TimeSpan.FromSeconds(1));
Assert.True(group.RevisionDate - utcNow < TimeSpan.FromSeconds(1));
}
@ -44,8 +59,7 @@ public class GroupServiceTests
await sutProvider.Sut.SaveAsync(group, collections);
await sutProvider.GetDependency<IGroupRepository>().Received().CreateAsync(group, collections);
await sutProvider.GetDependency<IEventService>().Received()
.LogGroupEventAsync(group, EventType.Group_Created);
await sutProvider.GetDependency<IEventService>().Received().LogGroupEventAsync(group, EventType.Group_Created);
Assert.True(group.CreationDate - utcNow < TimeSpan.FromSeconds(1));
Assert.True(group.RevisionDate - utcNow < TimeSpan.FromSeconds(1));
}
@ -59,8 +73,7 @@ public class GroupServiceTests
await sutProvider.Sut.SaveAsync(group, collections);
await sutProvider.GetDependency<IGroupRepository>().Received().ReplaceAsync(group, collections);
await sutProvider.GetDependency<IEventService>().Received()
.LogGroupEventAsync(group, EventType.Group_Updated);
await sutProvider.GetDependency<IEventService>().Received().LogGroupEventAsync(group, EventType.Group_Updated);
Assert.True(group.RevisionDate - DateTime.UtcNow < TimeSpan.FromSeconds(1));
}
@ -73,8 +86,7 @@ public class GroupServiceTests
await sutProvider.Sut.SaveAsync(group, null);
await sutProvider.GetDependency<IGroupRepository>().Received().ReplaceAsync(group);
await sutProvider.GetDependency<IEventService>().Received()
.LogGroupEventAsync(group, EventType.Group_Updated);
await sutProvider.GetDependency<IEventService>().Received().LogGroupEventAsync(group, EventType.Group_Updated);
Assert.True(group.RevisionDate - DateTime.UtcNow < TimeSpan.FromSeconds(1));
}
@ -109,8 +121,16 @@ public class GroupServiceTests
await sutProvider.Sut.DeleteAsync(group);
await sutProvider.GetDependency<IGroupRepository>().Received().DeleteAsync(group);
await sutProvider.GetDependency<IEventService>().Received()
.LogGroupEventAsync(group, EventType.Group_Deleted);
await sutProvider.GetDependency<IEventService>().Received().LogGroupEventAsync(group, EventType.Group_Deleted);
}
[Theory, BitAutoData]
public async Task DeleteAsync_ValidData_WithEventSystemUser_DeletesGroup(Group group, EventSystemUser eventSystemUser, SutProvider<GroupService> sutProvider)
{
await sutProvider.Sut.DeleteAsync(group, eventSystemUser);
await sutProvider.GetDependency<IGroupRepository>().Received().DeleteAsync(group);
await sutProvider.GetDependency<IEventService>().Received().LogGroupEventAsync(group, EventType.Group_Deleted, eventSystemUser);
}
[Theory, BitAutoData]
@ -130,6 +150,23 @@ public class GroupServiceTests
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_UpdatedGroups);
}
[Theory, BitAutoData]
public async Task DeleteUserAsync_ValidData_WithEventSystemUser_DeletesUserInGroupRepository(Group group, Organization organization, OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider<GroupService> sutProvider)
{
group.OrganizationId = organization.Id;
organization.UseGroups = true;
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
organizationUser.OrganizationId = organization.Id;
sutProvider.GetDependency<IOrganizationUserRepository>().GetByIdAsync(organizationUser.Id)
.Returns(organizationUser);
await sutProvider.Sut.DeleteUserAsync(group, organizationUser.Id, eventSystemUser);
await sutProvider.GetDependency<IGroupRepository>().Received().DeleteUserAsync(group.Id, organizationUser.Id);
await sutProvider.GetDependency<IEventService>().Received()
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_UpdatedGroups, eventSystemUser);
}
[Theory, BitAutoData]
public async Task DeleteUserAsync_InvalidUser_ThrowsNotFound(Group group, Organization organization, OrganizationUser organizationUser, SutProvider<GroupService> sutProvider)
{

View File

@ -186,11 +186,13 @@ public class OrganizationServiceTests
}
[Theory]
[OrganizationInviteCustomize, BitAutoData]
[OrganizationInviteCustomize(InviteeUserType = OrganizationUserType.User,
InvitorUserType = OrganizationUserType.Owner), BitAutoData]
public async Task InviteUser_NoEmails_Throws(Organization organization, OrganizationUser invitor,
OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
{
invite.Emails = null;
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, new (OrganizationUserInvite, string)[] { (invite, null) }));
@ -357,11 +359,6 @@ public class OrganizationServiceTests
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
SutProvider<OrganizationService> sutProvider)
{
// Autofixture will add collections for all of the invites, remove the first and for all the rest set all access false
invites.First().invite.AccessAll = true;
invites.First().invite.Collections = null;
invites.Skip(1).ToList().ForEach(i => i.invite.AccessAll = false);
invitor.Permissions = JsonSerializer.Serialize(new Permissions() { ManageUsers = true },
new JsonSerializerOptions
{
@ -382,6 +379,42 @@ public class OrganizationServiceTests
await sutProvider.GetDependency<IMailService>().Received(1)
.BulkSendOrganizationInviteEmailAsync(organization.Name,
Arg.Is<IEnumerable<(OrganizationUser, ExpiringToken)>>(v => v.Count() == invites.SelectMany(i => i.invite.Emails).Count()));
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, DateTime?)>>());
}
[Theory]
[OrganizationInviteCustomize(
InviteeUserType = OrganizationUserType.User,
InvitorUserType = OrganizationUserType.Custom
), BitAutoData]
public async Task InviteUser_WithEventSystemUser_Passes(Organization organization, EventSystemUser eventSystemUser, IEnumerable<(OrganizationUserInvite invite, string externalId)> invites,
OrganizationUser invitor,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
SutProvider<OrganizationService> sutProvider)
{
invitor.Permissions = JsonSerializer.Serialize(new Permissions() { ManageUsers = true },
new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
});
var organizationRepository = sutProvider.GetDependency<IOrganizationRepository>();
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var currentContext = sutProvider.GetDependency<ICurrentContext>();
organizationRepository.GetByIdAsync(organization.Id).Returns(organization);
organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner)
.Returns(new[] { owner });
currentContext.ManageUsers(organization.Id).Returns(true);
await sutProvider.Sut.InviteUsersAsync(organization.Id, eventSystemUser, invites);
await sutProvider.GetDependency<IMailService>().Received(1)
.BulkSendOrganizationInviteEmailAsync(organization.Name,
Arg.Is<IEnumerable<(OrganizationUser, ExpiringToken)>>(v => v.Count() == invites.SelectMany(i => i.invite.Emails).Count()));
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventsAsync(Arg.Any<IEnumerable<(OrganizationUser, EventType, EventSystemUser, DateTime?)>>());
}
[Theory, BitAutoData]
@ -505,6 +538,29 @@ public class OrganizationServiceTests
currentContext.OrganizationOwner(deletingUser.OrganizationId).Returns(true);
await sutProvider.Sut.DeleteUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId);
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed);
}
[Theory, BitAutoData]
public async Task DeleteUser_WithEventSystemUser_Success(
OrganizationUser organizationUser,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser deletingUser, EventSystemUser eventSystemUser,
SutProvider<OrganizationService> sutProvider)
{
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var currentContext = sutProvider.GetDependency<ICurrentContext>();
organizationUser.OrganizationId = deletingUser.OrganizationId;
organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser);
organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser);
organizationUserRepository.GetManyByOrganizationAsync(deletingUser.OrganizationId, OrganizationUserType.Owner)
.Returns(new[] { deletingUser, organizationUser });
currentContext.OrganizationOwner(deletingUser.OrganizationId).Returns(true);
await sutProvider.Sut.DeleteUserAsync(deletingUser.OrganizationId, organizationUser.Id, eventSystemUser);
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser);
}
[Theory, BitAutoData]
@ -962,4 +1018,77 @@ public class OrganizationServiceTests
await organizationRepository.DidNotReceiveWithAnyArgs().DeleteAsync(default);
await applicationCacheService.DidNotReceiveWithAnyArgs().DeleteOrganizationAbilityAsync(default);
}
private void RestoreRevokeUser_Setup(Organization organization, OrganizationUser owner, OrganizationUser organizationUser, SutProvider<OrganizationService> sutProvider)
{
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organizationUser.OrganizationId).Returns(organization);
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
organizationUserRepository.GetManyByOrganizationAsync(organizationUser.OrganizationId, OrganizationUserType.Owner)
.Returns(new[] { owner });
}
[Theory, BitAutoData]
public async Task RevokeUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
[OrganizationUser] OrganizationUser organizationUser, SutProvider<OrganizationService> sutProvider)
{
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var eventService = sutProvider.GetDependency<IEventService>();
await sutProvider.Sut.RevokeUserAsync(organizationUser, owner.Id);
await organizationUserRepository.Received().RevokeAsync(organizationUser.Id);
await eventService.Received()
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked);
}
[Theory, BitAutoData]
public async Task RevokeUser_WithEventSystemUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
[OrganizationUser] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider<OrganizationService> sutProvider)
{
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var eventService = sutProvider.GetDependency<IEventService>();
await sutProvider.Sut.RevokeUserAsync(organizationUser, eventSystemUser);
await organizationUserRepository.Received().RevokeAsync(organizationUser.Id);
await eventService.Received()
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Revoked, eventSystemUser);
}
[Theory, BitAutoData]
public async Task RestoreUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider<OrganizationService> sutProvider)
{
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
var userService = Substitute.For<IUserService>();
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var eventService = sutProvider.GetDependency<IEventService>();
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id, userService);
await organizationUserRepository.Received().RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited);
await eventService.Received()
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
}
[Theory, BitAutoData]
public async Task RestoreUser_WithEventSystemUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider<OrganizationService> sutProvider)
{
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
var userService = Substitute.For<IUserService>();
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var eventService = sutProvider.GetDependency<IEventService>();
await sutProvider.Sut.RestoreUserAsync(organizationUser, eventSystemUser, userService);
await organizationUserRepository.Received().RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited);
await eventService.Received()
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored, eventSystemUser);
}
}

View File

@ -0,0 +1,92 @@
-- Add column SystemUser to Event table
IF COL_LENGTH('[dbo].[Event]', 'SystemUser') IS NULL
BEGIN
ALTER TABLE
[dbo].[Event]
ADD
[SystemUser] TINYINT NULL;
END
GO
-- Recreate EventView so that it includes the SystemUser column
IF OBJECT_ID('[dbo].[EventView]') IS NOT NULL
BEGIN
DROP VIEW [dbo].[EventView]
END
GO
CREATE VIEW [dbo].[EventView]
AS
SELECT
*
FROM
[dbo].[Event]
GO
CREATE OR ALTER PROCEDURE [dbo].[Event_Create]
@Id UNIQUEIDENTIFIER OUTPUT,
@Type INT,
@UserId UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
@InstallationId UNIQUEIDENTIFIER,
@ProviderId UNIQUEIDENTIFIER,
@CipherId UNIQUEIDENTIFIER,
@CollectionId UNIQUEIDENTIFIER,
@PolicyId UNIQUEIDENTIFIER,
@GroupId UNIQUEIDENTIFIER,
@OrganizationUserId UNIQUEIDENTIFIER,
@ProviderUserId UNIQUEIDENTIFIER,
@ProviderOrganizationId UNIQUEIDENTIFIER = null,
@ActingUserId UNIQUEIDENTIFIER,
@DeviceType SMALLINT,
@IpAddress VARCHAR(50),
@Date DATETIME2(7),
@SystemUser TINYINT = null
AS
BEGIN
SET NOCOUNT ON
INSERT INTO [dbo].[Event]
(
[Id],
[Type],
[UserId],
[OrganizationId],
[InstallationId],
[ProviderId],
[CipherId],
[CollectionId],
[PolicyId],
[GroupId],
[OrganizationUserId],
[ProviderUserId],
[ProviderOrganizationId],
[ActingUserId],
[DeviceType],
[IpAddress],
[Date],
[SystemUser]
)
VALUES
(
@Id,
@Type,
@UserId,
@OrganizationId,
@InstallationId,
@ProviderId,
@CipherId,
@CollectionId,
@PolicyId,
@GroupId,
@OrganizationUserId,
@ProviderUserId,
@ProviderOrganizationId,
@ActingUserId,
@DeviceType,
@IpAddress,
@Date,
@SystemUser
)
END
GO

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.MySqlMigrations.Migrations;
public partial class EventsSystemUser : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<byte>(
name: "SystemUser",
table: "Event",
type: "tinyint unsigned",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SystemUser",
table: "Event");
}
}

View File

@ -351,6 +351,9 @@ namespace Bit.MySqlMigrations.Migrations
b.Property<Guid?>("ProviderUserId")
.HasColumnType("char(36)");
b.Property<byte?>("SystemUser")
.HasColumnType("tinyint unsigned");
b.Property<int>("Type")
.HasColumnType("int");

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.PostgresMigrations.Migrations;
public partial class EventsSystemUser : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<byte>(
name: "SystemUser",
table: "Event",
type: "smallint",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SystemUser",
table: "Event");
}
}

View File

@ -352,6 +352,9 @@ namespace Bit.PostgresMigrations.Migrations
b.Property<Guid?>("ProviderUserId")
.HasColumnType("uuid");
b.Property<byte?>("SystemUser")
.HasColumnType("smallint");
b.Property<int>("Type")
.HasColumnType("integer");