1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 15:42:48 -05: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
44 changed files with 3986 additions and 91 deletions

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);