1
0
mirror of https://github.com/bitwarden/server synced 2026-01-08 11:33:26 +00:00

[SM-1489] machine account events (#6187)

* Adding new logging for secrets

* fixing secrest controller tests

* fixing the tests

* Server side changes for adding ProjectId to Event table, adding Project event logging to projectsController

* Rough draft with TODO's need to work on EventRepository.cs, and ProjectRepository.cs

* Undoing changes to make projects soft delete, we want those to be fully deleted still. Adding GetManyTrashedSecretsByIds to secret repo so we can get soft deleted secrets, getSecrets in eventsController takes in orgdId, so that we can check the permission even if the secret was permanently deleted and doesn' thave the org Id set. Adding Secret Perm Deleted, and Restored to event logs

* db changes

* fixing the way we log events

* Trying to undo some manual changes that should have been migrations

* adding migration files

* fixing test

* setting up userid for project controller tests

* adding sql

* sql

* Rename file

* Trying to get it to for sure add the column before we try and update sprocs

* Adding code to refresh the view to include ProjectId I hope

* code improvements

* Suggested changes

* suggested changes

* trying to fix sql issues

* fixing swagger issue

* Update src/Core/SecretsManager/Repositories/Noop/NoopSecretRepository.cs

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>

* Suggested changes

* Adding event logging for machine accounts

* fixing two tests

* trying to fix all tests

* trying to fix tests

* fixing test

* Migrations

* fix

* updating eps

* adding migration

* Adding missing SQL changes

* updating sql

* fixing sql

* running migration again

* fixing sql

* adding query to add grantedSErviceAccountId to event table

* Suggested improvements

* removing more migrations

* more removal

* removing all migrations to them redo them

* redoing migration

---------

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
This commit is contained in:
cd-bitwarden
2025-10-01 09:13:49 -04:00
committed by GitHub
parent 721fda0aaa
commit bca1d585c5
40 changed files with 20553 additions and 28 deletions

View File

@@ -30,6 +30,8 @@ public class EventsController : Controller
private readonly ICurrentContext _currentContext;
private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
public EventsController(
IUserService userService,
@@ -39,7 +41,8 @@ public class EventsController : Controller
IEventRepository eventRepository,
ICurrentContext currentContext,
ISecretRepository secretRepository,
IProjectRepository projectRepository)
IProjectRepository projectRepository,
IServiceAccountRepository serviceAccountRepository)
{
_userService = userService;
_cipherRepository = cipherRepository;
@@ -49,6 +52,7 @@ public class EventsController : Controller
_currentContext = currentContext;
_secretRepository = secretRepository;
_projectRepository = projectRepository;
_serviceAccountRepository = serviceAccountRepository;
}
[HttpGet("")]
@@ -184,6 +188,57 @@ public class EventsController : Controller
return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);
}
[HttpGet("~/organization/{orgId}/service-account/{id}/events")]
public async Task<ListResponseModel<EventResponseModel>> GetServiceAccounts(
Guid orgId,
Guid id,
[FromQuery] DateTime? start = null,
[FromQuery] DateTime? end = null,
[FromQuery] string continuationToken = null)
{
if (id == Guid.Empty || orgId == Guid.Empty)
{
throw new NotFoundException();
}
var serviceAccount = await GetServiceAccount(id, orgId);
var org = _currentContext.GetOrganization(orgId);
if (org == null || !await _currentContext.AccessEventLogs(org.Id))
{
throw new NotFoundException();
}
var (fromDate, toDate) = ApiHelpers.GetDateRange(start, end);
var result = await _eventRepository.GetManyByOrganizationServiceAccountAsync(
serviceAccount.OrganizationId,
serviceAccount.Id,
fromDate,
toDate,
new PageOptions { ContinuationToken = continuationToken });
var responses = result.Data.Select(e => new EventResponseModel(e));
return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);
}
[ApiExplorerSettings(IgnoreApi = true)]
private async Task<ServiceAccount> GetServiceAccount(Guid serviceAccountId, Guid orgId)
{
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(serviceAccountId);
if (serviceAccount != null)
{
return serviceAccount;
}
var fallbackServiceAccount = new ServiceAccount
{
Id = serviceAccountId,
OrganizationId = orgId
};
return fallbackServiceAccount;
}
[HttpGet("~/organizations/{orgId}/users/{id}/events")]
public async Task<ListResponseModel<EventResponseModel>> GetOrganizationUser(string orgId, string id,
[FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null, [FromQuery] string continuationToken = null)

View File

@@ -35,6 +35,7 @@ public class EventResponseModel : ResponseModel
SecretId = ev.SecretId;
ProjectId = ev.ProjectId;
ServiceAccountId = ev.ServiceAccountId;
GrantedServiceAccountId = ev.GrantedServiceAccountId;
}
public EventType Type { get; set; }
@@ -58,4 +59,5 @@ public class EventResponseModel : ResponseModel
public Guid? SecretId { get; set; }
public Guid? ProjectId { get; set; }
public Guid? ServiceAccountId { get; set; }
public Guid? GrantedServiceAccountId { get; set; }
}

View File

@@ -29,6 +29,7 @@ public class AccessPoliciesController : Controller
private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IUpdateServiceAccountGrantedPoliciesCommand _updateServiceAccountGrantedPoliciesCommand;
private readonly IUserService _userService;
private readonly IEventService _eventService;
private readonly IProjectServiceAccountsAccessPoliciesUpdatesQuery
_projectServiceAccountsAccessPoliciesUpdatesQuery;
private readonly IUpdateProjectServiceAccountsAccessPoliciesCommand
@@ -47,7 +48,8 @@ public class AccessPoliciesController : Controller
IServiceAccountGrantedPolicyUpdatesQuery serviceAccountGrantedPolicyUpdatesQuery,
IProjectServiceAccountsAccessPoliciesUpdatesQuery projectServiceAccountsAccessPoliciesUpdatesQuery,
IUpdateServiceAccountGrantedPoliciesCommand updateServiceAccountGrantedPoliciesCommand,
IUpdateProjectServiceAccountsAccessPoliciesCommand updateProjectServiceAccountsAccessPoliciesCommand)
IUpdateProjectServiceAccountsAccessPoliciesCommand updateProjectServiceAccountsAccessPoliciesCommand,
IEventService eventService)
{
_authorizationService = authorizationService;
_userService = userService;
@@ -61,6 +63,7 @@ public class AccessPoliciesController : Controller
_serviceAccountGrantedPolicyUpdatesQuery = serviceAccountGrantedPolicyUpdatesQuery;
_projectServiceAccountsAccessPoliciesUpdatesQuery = projectServiceAccountsAccessPoliciesUpdatesQuery;
_updateProjectServiceAccountsAccessPoliciesCommand = updateProjectServiceAccountsAccessPoliciesCommand;
_eventService = eventService;
}
[HttpGet("/organizations/{id}/access-policies/people/potential-grantees")]
@@ -186,7 +189,9 @@ public class AccessPoliciesController : Controller
}
var userId = _userService.GetProperUserId(User)!.Value;
var currentPolicies = await _accessPolicyRepository.GetPeoplePoliciesByGrantedServiceAccountIdAsync(peopleAccessPolicies.Id, userId);
var results = await _accessPolicyRepository.ReplaceServiceAccountPeopleAsync(peopleAccessPolicies, userId);
await LogAccessPolicyServiceAccountChanges(currentPolicies, results, userId);
return new ServiceAccountPeopleAccessPoliciesResponseModel(results, userId);
}
@@ -336,4 +341,39 @@ public class AccessPoliciesController : Controller
userId, accessClient);
return new ServiceAccountGrantedPoliciesPermissionDetailsResponseModel(results);
}
public async Task LogAccessPolicyServiceAccountChanges(IEnumerable<BaseAccessPolicy> currentPolicies, IEnumerable<BaseAccessPolicy> updatedPolicies, Guid userId)
{
foreach (var current in currentPolicies.OfType<GroupServiceAccountAccessPolicy>())
{
if (!updatedPolicies.Any(r => r.Id == current.Id))
{
await _eventService.LogServiceAccountGroupEventAsync(userId, current, EventType.ServiceAccount_GroupRemoved, _currentContext.IdentityClientType);
}
}
foreach (var policy in updatedPolicies.OfType<GroupServiceAccountAccessPolicy>())
{
if (!currentPolicies.Any(e => e.Id == policy.Id))
{
await _eventService.LogServiceAccountGroupEventAsync(userId, policy, EventType.ServiceAccount_GroupAdded, _currentContext.IdentityClientType);
}
}
foreach (var current in currentPolicies.OfType<UserServiceAccountAccessPolicy>())
{
if (!updatedPolicies.Any(r => r.Id == current.Id))
{
await _eventService.LogServiceAccountPeopleEventAsync(userId, current, EventType.ServiceAccount_UserRemoved, _currentContext.IdentityClientType);
}
}
foreach (var policy in updatedPolicies.OfType<UserServiceAccountAccessPolicy>())
{
if (!currentPolicies.Any(e => e.Id == policy.Id))
{
await _eventService.LogServiceAccountPeopleEventAsync(userId, policy, EventType.ServiceAccount_UserAdded, _currentContext.IdentityClientType);
}
}
}
}

View File

@@ -42,6 +42,8 @@ public class ServiceAccountsController : Controller
private readonly IDeleteServiceAccountsCommand _deleteServiceAccountsCommand;
private readonly IRevokeAccessTokensCommand _revokeAccessTokensCommand;
private readonly IPricingClient _pricingClient;
private readonly IEventService _eventService;
private readonly IOrganizationUserRepository _organizationUserRepository;
public ServiceAccountsController(
ICurrentContext currentContext,
@@ -58,7 +60,9 @@ public class ServiceAccountsController : Controller
IUpdateServiceAccountCommand updateServiceAccountCommand,
IDeleteServiceAccountsCommand deleteServiceAccountsCommand,
IRevokeAccessTokensCommand revokeAccessTokensCommand,
IPricingClient pricingClient)
IPricingClient pricingClient,
IEventService eventService,
IOrganizationUserRepository organizationUserRepository)
{
_currentContext = currentContext;
_userService = userService;
@@ -75,6 +79,8 @@ public class ServiceAccountsController : Controller
_pricingClient = pricingClient;
_createAccessTokenCommand = createAccessTokenCommand;
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
_eventService = eventService;
_organizationUserRepository = organizationUserRepository;
}
[HttpGet("/organizations/{organizationId}/service-accounts")]
@@ -139,8 +145,15 @@ public class ServiceAccountsController : Controller
}
var userId = _userService.GetProperUserId(User).Value;
var result =
await _createServiceAccountCommand.CreateAsync(createRequest.ToServiceAccount(organizationId), userId);
await _createServiceAccountCommand.CreateAsync(serviceAccount, userId);
if (result != null)
{
await _eventService.LogServiceAccountEventAsync(userId, [serviceAccount], EventType.ServiceAccount_Created, _currentContext.IdentityClientType);
}
return new ServiceAccountResponseModel(result);
}
@@ -197,6 +210,9 @@ public class ServiceAccountsController : Controller
}
await _deleteServiceAccountsCommand.DeleteServiceAccounts(serviceAccountsToDelete);
var userId = _userService.GetProperUserId(User)!.Value;
await _eventService.LogServiceAccountEventAsync(userId, serviceAccountsToDelete, EventType.ServiceAccount_Deleted, _currentContext.IdentityClientType);
var responses = results.Select(r => new BulkDeleteResponseModel(r.ServiceAccount.Id, r.Error));
return new ListResponseModel<BulkDeleteResponseModel>(responses);
}

View File

@@ -34,6 +34,7 @@ public class Event : ITableObject<Guid>, IEvent
SecretId = e.SecretId;
ProjectId = e.ProjectId;
ServiceAccountId = e.ServiceAccountId;
GrantedServiceAccountId = e.GrantedServiceAccountId;
}
public Guid Id { get; set; }
@@ -59,7 +60,7 @@ public class Event : ITableObject<Guid>, IEvent
public Guid? SecretId { get; set; }
public Guid? ProjectId { get; set; }
public Guid? ServiceAccountId { get; set; }
public Guid? GrantedServiceAccountId { get; set; }
public void SetNewId()
{
Id = CoreHelpers.GenerateComb();

View File

@@ -109,4 +109,11 @@ public enum EventType : int
Project_Created = 2201,
Project_Edited = 2202,
Project_Deleted = 2203,
ServiceAccount_UserAdded = 2300,
ServiceAccount_UserRemoved = 2301,
ServiceAccount_GroupAdded = 2302,
ServiceAccount_GroupRemoved = 2303,
ServiceAccount_Created = 2304,
ServiceAccount_Deleted = 2305,
}

View File

@@ -39,4 +39,5 @@ public class EventMessage : IEvent
public Guid? SecretId { get; set; }
public Guid? ProjectId { get; set; }
public Guid? ServiceAccountId { get; set; }
public Guid? GrantedServiceAccountId { get; set; }
}

View File

@@ -37,6 +37,7 @@ public class AzureEvent : ITableEntity
public Guid? SecretId { get; set; }
public Guid? ProjectId { get; set; }
public Guid? ServiceAccountId { get; set; }
public Guid? GrantedServiceAccountId { get; set; }
public EventTableEntity ToEventTableEntity()
{
@@ -68,6 +69,7 @@ public class AzureEvent : ITableEntity
SecretId = SecretId,
ServiceAccountId = ServiceAccountId,
ProjectId = ProjectId,
GrantedServiceAccountId = GrantedServiceAccountId
};
}
}
@@ -99,6 +101,7 @@ public class EventTableEntity : IEvent
SecretId = e.SecretId;
ProjectId = e.ProjectId;
ServiceAccountId = e.ServiceAccountId;
GrantedServiceAccountId = e.GrantedServiceAccountId;
}
public string PartitionKey { get; set; }
@@ -127,6 +130,7 @@ public class EventTableEntity : IEvent
public Guid? SecretId { get; set; }
public Guid? ProjectId { get; set; }
public Guid? ServiceAccountId { get; set; }
public Guid? GrantedServiceAccountId { get; set; }
public AzureEvent ToAzureEvent()
{
@@ -157,7 +161,8 @@ public class EventTableEntity : IEvent
DomainName = DomainName,
SecretId = SecretId,
ProjectId = ProjectId,
ServiceAccountId = ServiceAccountId
ServiceAccountId = ServiceAccountId,
GrantedServiceAccountId = GrantedServiceAccountId
};
}
@@ -232,6 +237,15 @@ public class EventTableEntity : IEvent
});
}
if (e.GrantedServiceAccountId.HasValue)
{
entities.Add(new EventTableEntity(e)
{
PartitionKey = pKey,
RowKey = $"GrantedServiceAccountId={e.GrantedServiceAccountId}__Date={dateKey}__Uniquifier={uniquifier}"
});
}
return entities;
}

View File

@@ -28,4 +28,5 @@ public interface IEvent
Guid? SecretId { get; set; }
Guid? ProjectId { get; set; }
Guid? ServiceAccountId { get; set; }
Guid? GrantedServiceAccountId { get; set; }
}

View File

@@ -27,6 +27,7 @@ public interface IEventRepository
DateTime startDate, DateTime endDate, PageOptions pageOptions);
Task<PagedResult<IEvent>> GetManyByCipherAsync(Cipher cipher, DateTime startDate, DateTime endDate,
PageOptions pageOptions);
Task CreateAsync(IEvent e);
Task CreateManyAsync(IEnumerable<IEvent> e);
Task<PagedResult<IEvent>> GetManyByOrganizationServiceAccountAsync(Guid organizationId, Guid serviceAccountId,

View File

@@ -77,12 +77,18 @@ public class EventRepository : IEventRepository
return await GetManyAsync(partitionKey, $"CipherId={cipher.Id}__Date={{0}}", startDate, endDate, pageOptions);
}
public async Task<PagedResult<IEvent>> GetManyByOrganizationServiceAccountAsync(Guid organizationId,
Guid serviceAccountId, DateTime startDate, DateTime endDate, PageOptions pageOptions)
public async Task<PagedResult<IEvent>> GetManyByOrganizationServiceAccountAsync(
Guid organizationId,
Guid serviceAccountId,
DateTime startDate,
DateTime endDate,
PageOptions pageOptions)
{
return await GetManyServiceAccountAsync(
$"OrganizationId={organizationId}",
serviceAccountId.ToString(),
startDate, endDate, pageOptions);
return await GetManyAsync($"OrganizationId={organizationId}",
$"ServiceAccountId={serviceAccountId}__Date={{0}}", startDate, endDate, pageOptions);
}
public async Task CreateAsync(IEvent e)
@@ -141,6 +147,40 @@ public class EventRepository : IEventRepository
}
}
public async Task<PagedResult<IEvent>> GetManyServiceAccountAsync(
string partitionKey,
string serviceAccountId,
DateTime startDate,
DateTime endDate,
PageOptions pageOptions)
{
var start = CoreHelpers.DateTimeToTableStorageKey(startDate);
var end = CoreHelpers.DateTimeToTableStorageKey(endDate);
var filter = MakeFilterForServiceAccount(partitionKey, serviceAccountId, startDate, endDate);
var result = new PagedResult<IEvent>();
var query = _tableClient.QueryAsync<AzureEvent>(filter, pageOptions.PageSize);
await using (var enumerator = query.AsPages(pageOptions.ContinuationToken,
pageOptions.PageSize).GetAsyncEnumerator())
{
if (await enumerator.MoveNextAsync())
{
result.ContinuationToken = enumerator.Current.ContinuationToken;
var events = enumerator.Current.Values
.Select(e => e.ToEventTableEntity())
.ToList();
events = events.OrderByDescending(e => e.Date).ToList();
result.Data.AddRange(events);
}
}
return result;
}
public async Task<PagedResult<IEvent>> GetManyAsync(string partitionKey, string rowKey,
DateTime startDate, DateTime endDate, PageOptions pageOptions)
{
@@ -172,4 +212,27 @@ public class EventRepository : IEventRepository
{
return $"PartitionKey eq '{partitionKey}' and RowKey le '{rowStart}' and RowKey ge '{rowEnd}'";
}
private string MakeFilterForServiceAccount(
string partitionKey,
string machineAccountId,
DateTime startDate,
DateTime endDate)
{
var start = CoreHelpers.DateTimeToTableStorageKey(startDate);
var end = CoreHelpers.DateTimeToTableStorageKey(endDate);
var rowKey1Start = $"ServiceAccountId={machineAccountId}__Date={start}";
var rowKey1End = $"ServiceAccountId={machineAccountId}__Date={end}";
var rowKey2Start = $"GrantedServiceAccountId={machineAccountId}__Date={start}";
var rowKey2End = $"GrantedServiceAccountId={machineAccountId}__Date={end}";
var left = $"PartitionKey eq '{partitionKey}' and RowKey le '{rowKey1Start}' and RowKey ge '{rowKey1End}'";
var right = $"PartitionKey eq '{partitionKey}' and RowKey le '{rowKey2Start}' and RowKey ge '{rowKey2End}'";
return $"({left}) or ({right})";
}
}

View File

@@ -4,6 +4,7 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Interfaces;
using Bit.Core.Auth.Identity;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.Entities;
@@ -37,4 +38,7 @@ public interface IEventService
Task LogServiceAccountSecretsEventAsync(Guid serviceAccountId, IEnumerable<Secret> secrets, EventType type, DateTime? date = null);
Task LogUserProjectsEventAsync(Guid userId, IEnumerable<Project> projects, EventType type, DateTime? date = null);
Task LogServiceAccountProjectsEventAsync(Guid serviceAccountId, IEnumerable<Project> projects, EventType type, DateTime? date = null);
Task LogServiceAccountPeopleEventAsync(Guid userId, UserServiceAccountAccessPolicy policy, EventType type, IdentityClientType identityClientType, DateTime? date = null);
Task LogServiceAccountGroupEventAsync(Guid userId, GroupServiceAccountAccessPolicy policy, EventType type, IdentityClientType identityClientType, DateTime? date = null);
Task LogServiceAccountEventAsync(Guid userId, List<ServiceAccount> serviceAccount, EventType type, IdentityClientType identityClientType, DateTime? date = null);
}

View File

@@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Interfaces;
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Identity;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
@@ -516,6 +517,135 @@ public class EventService : IEventService
await _eventWriteService.CreateManyAsync(eventMessages);
}
public async Task LogServiceAccountPeopleEventAsync(Guid userId, UserServiceAccountAccessPolicy policy, EventType type, IdentityClientType identityClientType, DateTime? date = null)
{
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
var eventMessages = new List<IEvent>();
var orgUser = await _organizationUserRepository.GetByIdAsync((Guid)policy.OrganizationUserId);
if (!CanUseEvents(orgAbilities, orgUser.OrganizationId))
{
return;
}
var (actingUserId, serviceAccountId) = MapIdentityClientType(userId, identityClientType);
if (actingUserId is null && serviceAccountId is null)
{
return;
}
if (policy.OrganizationUserId != null)
{
var e = new EventMessage(_currentContext)
{
OrganizationId = orgUser.OrganizationId,
Type = type,
GrantedServiceAccountId = policy.GrantedServiceAccountId,
ServiceAccountId = serviceAccountId,
UserId = policy.OrganizationUserId,
ActingUserId = actingUserId,
Date = date.GetValueOrDefault(DateTime.UtcNow)
};
eventMessages.Add(e);
await _eventWriteService.CreateManyAsync(eventMessages);
}
}
public async Task LogServiceAccountGroupEventAsync(Guid userId, GroupServiceAccountAccessPolicy policy, EventType type, IdentityClientType identityClientType, DateTime? date = null)
{
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
var eventMessages = new List<IEvent>();
if (!CanUseEvents(orgAbilities, policy.Group.OrganizationId))
{
return;
}
var (actingUserId, serviceAccountId) = MapIdentityClientType(userId, identityClientType);
if (actingUserId is null && serviceAccountId is null)
{
return;
}
if (policy.GroupId != null)
{
var e = new EventMessage(_currentContext)
{
OrganizationId = policy.Group.OrganizationId,
Type = type,
GrantedServiceAccountId = policy.GrantedServiceAccountId,
ServiceAccountId = serviceAccountId,
GroupId = policy.GroupId,
ActingUserId = actingUserId,
Date = date.GetValueOrDefault(DateTime.UtcNow)
};
eventMessages.Add(e);
await _eventWriteService.CreateManyAsync(eventMessages);
}
}
public async Task LogServiceAccountEventAsync(Guid userId, List<ServiceAccount> serviceAccounts, EventType type, IdentityClientType identityClientType, DateTime? date = null)
{
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
var eventMessages = new List<IEvent>();
foreach (var serviceAccount in serviceAccounts)
{
if (!CanUseEvents(orgAbilities, serviceAccount.OrganizationId))
{
continue;
}
var (actingUserId, serviceAccountId) = MapIdentityClientType(userId, identityClientType);
if (actingUserId is null && serviceAccountId is null)
{
continue;
}
if (serviceAccount != null)
{
var e = new EventMessage(_currentContext)
{
OrganizationId = serviceAccount.OrganizationId,
Type = type,
GrantedServiceAccountId = serviceAccount.Id,
ServiceAccountId = serviceAccountId,
ActingUserId = actingUserId,
Date = date.GetValueOrDefault(DateTime.UtcNow)
};
eventMessages.Add(e);
}
}
if (eventMessages.Any())
{
await _eventWriteService.CreateManyAsync(eventMessages);
}
}
private (Guid? actingUserId, Guid? serviceAccountId) MapIdentityClientType(
Guid userId, IdentityClientType identityClientType)
{
if (identityClientType == IdentityClientType.Organization)
{
return (null, null);
}
return identityClientType switch
{
IdentityClientType.User => (userId, null),
IdentityClientType.ServiceAccount => (null, userId),
_ => throw new InvalidOperationException("Unknown identity client type.")
};
}
private async Task<Guid?> GetProviderIdAsync(Guid? orgId)
{
if (_currentContext == null || !orgId.HasValue)

View File

@@ -1,6 +1,7 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Interfaces;
using Bit.Core.Auth.Identity;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.Entities;
@@ -139,4 +140,19 @@ public class NoopEventService : IEventService
{
return Task.FromResult(0);
}
public Task LogServiceAccountPeopleEventAsync(Guid userId, UserServiceAccountAccessPolicy policy, EventType type, IdentityClientType identityClientType, DateTime? date = null)
{
return Task.FromResult(0);
}
public Task LogServiceAccountGroupEventAsync(Guid userId, GroupServiceAccountAccessPolicy policy, EventType type, IdentityClientType identityClientType, DateTime? date = null)
{
return Task.FromResult(0);
}
public Task LogServiceAccountEventAsync(Guid userId, List<ServiceAccount> serviceAccount, EventType type, IdentityClientType identityClientType, DateTime? date = null)
{
return Task.FromResult(0);
}
}

View File

@@ -12,9 +12,16 @@ public class EventEntityTypeConfiguration : IEntityTypeConfiguration<Event>
.Property(e => e.Id)
.ValueGeneratedNever();
builder
.HasIndex(e => new { e.Date, e.OrganizationId, e.ActingUserId, e.CipherId })
.IsClustered(false);
builder.HasKey(e => e.Id)
.IsClustered();
var index = builder.HasIndex(e => new { e.Date, e.OrganizationId, e.ActingUserId, e.CipherId })
.IsClustered(false)
.HasDatabaseName("IX_Event_DateOrganizationIdUserId");
SqlServerIndexBuilderExtensions.IncludeProperties(
index,
e => new { e.ServiceAccountId, e.GrantedServiceAccountId });
builder.ToTable(nameof(Event));
}

View File

@@ -30,7 +30,7 @@ public class EventReadPageByOrganizationIdServiceAccountIdQuery : IQuery<Event>
(_beforeDate != null || e.Date <= _endDate) &&
(_beforeDate == null || e.Date < _beforeDate.Value) &&
e.OrganizationId == _organizationId &&
e.ServiceAccountId == _serviceAccountId
(e.ServiceAccountId == _serviceAccountId || e.GrantedServiceAccountId == _serviceAccountId)
orderby e.Date descending
select e;
return q.Skip(0).Take(_pageOptions.PageSize);

View File

@@ -0,0 +1,48 @@
using Bit.Core.Models.Data;
using Bit.Core.SecretsManager.Entities;
using Event = Bit.Infrastructure.EntityFramework.Models.Event;
namespace Bit.Infrastructure.EntityFramework.Repositories.Queries;
public class EventReadPageByServiceAccountQuery : IQuery<Event>
{
private readonly ServiceAccount _serviceAccount;
private readonly DateTime _startDate;
private readonly DateTime _endDate;
private readonly DateTime? _beforeDate;
private readonly PageOptions _pageOptions;
public EventReadPageByServiceAccountQuery(ServiceAccount serviceAccount, DateTime startDate, DateTime endDate, PageOptions pageOptions)
{
_serviceAccount = serviceAccount;
_startDate = startDate;
_endDate = endDate;
_beforeDate = null;
_pageOptions = pageOptions;
}
public EventReadPageByServiceAccountQuery(ServiceAccount serviceAccount, DateTime startDate, DateTime endDate, DateTime? beforeDate, PageOptions pageOptions)
{
_serviceAccount = serviceAccount;
_startDate = startDate;
_endDate = endDate;
_beforeDate = beforeDate;
_pageOptions = pageOptions;
}
public IQueryable<Event> Run(DatabaseContext dbContext)
{
var q = from e in dbContext.Events
where e.Date >= _startDate &&
(_beforeDate == null || e.Date < _beforeDate.Value) &&
(
(_serviceAccount.OrganizationId == Guid.Empty && !e.OrganizationId.HasValue) ||
(_serviceAccount.OrganizationId != Guid.Empty && e.OrganizationId == _serviceAccount.OrganizationId)
) &&
e.GrantedServiceAccountId == _serviceAccount.Id
orderby e.Date descending
select e;
return q.Take(_pageOptions.PageSize);
}
}

View File

@@ -18,7 +18,7 @@ BEGIN
AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate)
AND (@BeforeDate IS NULL OR [Date] < @BeforeDate)
AND [OrganizationId] = @OrganizationId
AND [ServiceAccountId] = @ServiceAccountId
AND ([ServiceAccountId] = @ServiceAccountId OR [GrantedServiceAccountId] = @ServiceAccountId)
ORDER BY [Date] DESC
OFFSET 0 ROWS
FETCH NEXT @PageSize ROWS ONLY

View File

@@ -0,0 +1,45 @@
CREATE PROCEDURE [dbo].[Event_ReadPageByServiceAccountId]
@GrantedServiceAccountId UNIQUEIDENTIFIER,
@StartDate DATETIME2(7),
@EndDate DATETIME2(7),
@BeforeDate DATETIME2(7),
@PageSize INT
AS
BEGIN
SET NOCOUNT ON
SELECT
e.Id,
e.Date,
e.Type,
e.UserId,
e.OrganizationId,
e.InstallationId,
e.ProviderId,
e.CipherId,
e.CollectionId,
e.PolicyId,
e.GroupId,
e.OrganizationUserId,
e.ProviderUserId,
e.ProviderOrganizationId,
e.DeviceType,
e.IpAddress,
e.ActingUserId,
e.SystemUser,
e.DomainName,
e.SecretId,
e.ServiceAccountId,
e.ProjectId,
e.GrantedServiceAccountId
FROM
[dbo].[EventView] e
WHERE
[Date] >= @StartDate
AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate)
AND (@BeforeDate IS NULL OR [Date] < @BeforeDate)
AND [GrantedServiceAccountId] = @GrantedServiceAccountId
ORDER BY [Date] DESC
OFFSET 0 ROWS
FETCH NEXT @PageSize ROWS ONLY
END

View File

@@ -20,7 +20,8 @@
@DomainName VARCHAR(256),
@SecretId UNIQUEIDENTIFIER = null,
@ServiceAccountId UNIQUEIDENTIFIER = null,
@ProjectId UNIQUEIDENTIFIER = null
@ProjectId UNIQUEIDENTIFIER = null,
@GrantedServiceAccountId UNIQUEIDENTIFIER = null
AS
BEGIN
SET NOCOUNT ON
@@ -48,7 +49,8 @@ BEGIN
[DomainName],
[SecretId],
[ServiceAccountId],
[ProjectId]
[ProjectId],
[GrantedServiceAccountId]
)
VALUES
(
@@ -73,6 +75,7 @@ BEGIN
@DomainName,
@SecretId,
@ServiceAccountId,
@ProjectId
@ProjectId,
@GrantedServiceAccountId
)
END

View File

@@ -21,11 +21,12 @@
[SecretId] UNIQUEIDENTIFIER NULL,
[ServiceAccountId] UNIQUEIDENTIFIER NULL,
[ProjectId] UNIQUEIDENTIFIER NULL,
[GrantedServiceAccountId] UNIQUEIDENTIFIER NULL,
CONSTRAINT [PK_Event] PRIMARY KEY CLUSTERED ([Id] ASC)
);
GO
CREATE NONCLUSTERED INDEX [IX_Event_DateOrganizationIdUserId]
ON [dbo].[Event]([Date] DESC, [OrganizationId] ASC, [ActingUserId] ASC, [CipherId] ASC);
ON [dbo].[Event]([Date] DESC, [OrganizationId] ASC, [ActingUserId] ASC, [CipherId] ASC) INCLUDE ([ServiceAccountId], [GrantedServiceAccountId]);