mirror of
https://github.com/bitwarden/server
synced 2025-12-06 00:03:34 +00:00
[SM-1274] Adding Project Events (#6022)
* 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 --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
This commit is contained in:
@@ -28,7 +28,10 @@ public class ProjectRepository : Repository<Core.SecretsManager.Entities.Project
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ProjectPermissionDetails>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)
|
||||
public async Task<IEnumerable<ProjectPermissionDetails>> GetManyByOrganizationIdAsync(
|
||||
Guid organizationId,
|
||||
Guid userId,
|
||||
AccessClientType accessType)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
@@ -45,6 +45,19 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyTrashedSecretsByIds(IEnumerable<Guid> ids)
|
||||
{
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var secrets = await dbContext.Secret
|
||||
.Where(c => ids.Contains(c.Id) && c.DeletedDate != null)
|
||||
.Include(c => c.Projects)
|
||||
.ToListAsync();
|
||||
return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Core.SecretsManager.Entities.Secret>> GetManyByOrganizationIdAsync(
|
||||
Guid organizationId, Guid userId, AccessClientType accessType)
|
||||
{
|
||||
@@ -66,10 +79,14 @@ public class SecretRepository : Repository<Core.SecretsManager.Entities.Secret,
|
||||
return Mapper.Map<List<Core.SecretsManager.Entities.Secret>>(secrets);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType)
|
||||
public async Task<IEnumerable<SecretPermissionDetails>> GetManyDetailsByOrganizationIdAsync(
|
||||
Guid organizationId,
|
||||
Guid userId,
|
||||
AccessClientType accessType)
|
||||
{
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
|
||||
var query = dbContext.Secret
|
||||
.Include(c => c.Projects)
|
||||
.Where(c => c.OrganizationId == organizationId && c.DeletedDate == null)
|
||||
|
||||
@@ -5,9 +5,12 @@ using Bit.Api.Models.Response;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Vault.Repositories;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -25,6 +28,8 @@ public class EventsController : Controller
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IEventRepository _eventRepository;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ISecretRepository _secretRepository;
|
||||
private readonly IProjectRepository _projectRepository;
|
||||
|
||||
public EventsController(
|
||||
IUserService userService,
|
||||
@@ -32,7 +37,9 @@ public class EventsController : Controller
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
IEventRepository eventRepository,
|
||||
ICurrentContext currentContext)
|
||||
ICurrentContext currentContext,
|
||||
ISecretRepository secretRepository,
|
||||
IProjectRepository projectRepository)
|
||||
{
|
||||
_userService = userService;
|
||||
_cipherRepository = cipherRepository;
|
||||
@@ -40,6 +47,8 @@ public class EventsController : Controller
|
||||
_providerUserRepository = providerUserRepository;
|
||||
_eventRepository = eventRepository;
|
||||
_currentContext = currentContext;
|
||||
_secretRepository = secretRepository;
|
||||
_projectRepository = projectRepository;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
@@ -104,6 +113,77 @@ public class EventsController : Controller
|
||||
return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);
|
||||
}
|
||||
|
||||
[HttpGet("~/organization/{orgId}/secrets/{id}/events")]
|
||||
public async Task<ListResponseModel<EventResponseModel>> GetSecrets(
|
||||
Guid id, Guid orgId,
|
||||
[FromQuery] DateTime? start = null,
|
||||
[FromQuery] DateTime? end = null,
|
||||
[FromQuery] string continuationToken = null)
|
||||
{
|
||||
if (id == Guid.Empty || orgId == Guid.Empty)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var secret = await _secretRepository.GetByIdAsync(id);
|
||||
var orgIdForVerification = secret?.OrganizationId ?? orgId;
|
||||
var secretOrg = _currentContext.GetOrganization(orgIdForVerification);
|
||||
|
||||
if (secretOrg == null || !await _currentContext.AccessEventLogs(secretOrg.Id))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
bool canViewLogs = false;
|
||||
|
||||
if (secret == null)
|
||||
{
|
||||
secret = new Core.SecretsManager.Entities.Secret { Id = id, OrganizationId = orgId };
|
||||
canViewLogs = secretOrg.Type is Core.Enums.OrganizationUserType.Admin or Core.Enums.OrganizationUserType.Owner;
|
||||
}
|
||||
else
|
||||
{
|
||||
canViewLogs = await CanViewSecretsLogs(secret);
|
||||
}
|
||||
|
||||
if (!canViewLogs)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var (fromDate, toDate) = ApiHelpers.GetDateRange(start, end);
|
||||
var result = await _eventRepository.GetManyBySecretAsync(secret, fromDate, toDate, new PageOptions { ContinuationToken = continuationToken });
|
||||
var responses = result.Data.Select(e => new EventResponseModel(e));
|
||||
return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);
|
||||
}
|
||||
|
||||
[HttpGet("~/organization/{orgId}/projects/{id}/events")]
|
||||
public async Task<ListResponseModel<EventResponseModel>> GetProjects(
|
||||
Guid id,
|
||||
Guid orgId,
|
||||
[FromQuery] DateTime? start = null,
|
||||
[FromQuery] DateTime? end = null,
|
||||
[FromQuery] string continuationToken = null)
|
||||
{
|
||||
if (id == Guid.Empty || orgId == Guid.Empty)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var project = await GetProject(id, orgId);
|
||||
await ValidateOrganization(project);
|
||||
|
||||
var (fromDate, toDate) = ApiHelpers.GetDateRange(start, end);
|
||||
var result = await _eventRepository.GetManyByProjectAsync(
|
||||
project,
|
||||
fromDate,
|
||||
toDate,
|
||||
new PageOptions { ContinuationToken = continuationToken });
|
||||
|
||||
var responses = result.Data.Select(e => new EventResponseModel(e));
|
||||
return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);
|
||||
}
|
||||
|
||||
[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)
|
||||
@@ -157,4 +237,48 @@ public class EventsController : Controller
|
||||
var responses = result.Data.Select(e => new EventResponseModel(e));
|
||||
return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);
|
||||
}
|
||||
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
private async Task ValidateOrganization(Project project)
|
||||
{
|
||||
var org = _currentContext.GetOrganization(project.OrganizationId);
|
||||
|
||||
if (org == null || !await _currentContext.AccessEventLogs(org.Id))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
private async Task<Project> GetProject(Guid projectGuid, Guid orgGuid)
|
||||
{
|
||||
var project = await _projectRepository.GetByIdAsync(projectGuid);
|
||||
if (project != null)
|
||||
{
|
||||
return project;
|
||||
}
|
||||
|
||||
var fallbackProject = new Project
|
||||
{
|
||||
Id = projectGuid,
|
||||
OrganizationId = orgGuid
|
||||
};
|
||||
|
||||
return fallbackProject;
|
||||
}
|
||||
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
private async Task<bool> CanViewSecretsLogs(Secret secret)
|
||||
{
|
||||
if (!_currentContext.AccessSecretsManager(secret.OrganizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User)!.Value;
|
||||
var isAdmin = await _currentContext.OrganizationAdmin(secret.OrganizationId);
|
||||
var accessClient = AccessClientHelper.ToAccessClient(_currentContext.IdentityClientType, isAdmin);
|
||||
var access = await _secretRepository.AccessToSecretAsync(secret.Id, userId, accessClient);
|
||||
return access.Read;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ public class EventResponseModel : ResponseModel
|
||||
SystemUser = ev.SystemUser;
|
||||
DomainName = ev.DomainName;
|
||||
SecretId = ev.SecretId;
|
||||
ProjectId = ev.ProjectId;
|
||||
ServiceAccountId = ev.ServiceAccountId;
|
||||
}
|
||||
|
||||
@@ -55,5 +56,6 @@ public class EventResponseModel : ResponseModel
|
||||
public EventSystemUser? SystemUser { get; set; }
|
||||
public string DomainName { get; set; }
|
||||
public Guid? SecretId { get; set; }
|
||||
public Guid? ProjectId { get; set; }
|
||||
public Guid? ServiceAccountId { get; set; }
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ public class EventResponseModel : IResponseModel
|
||||
IpAddress = ev.IpAddress;
|
||||
InstallationId = ev.InstallationId;
|
||||
SecretId = ev.SecretId;
|
||||
ProjectId = ev.ProjectId;
|
||||
ServiceAccountId = ev.ServiceAccountId;
|
||||
}
|
||||
|
||||
@@ -97,6 +98,11 @@ public class EventResponseModel : IResponseModel
|
||||
/// <example>e68b8629-85eb-4929-92c0-b84464976ba4</example>
|
||||
public Guid? SecretId { get; set; }
|
||||
/// <summary>
|
||||
/// The unique identifier of the related project that the event describes.
|
||||
/// </summary>
|
||||
/// <example>e68b8629-85eb-4929-92c0-b84464976ba4</example>
|
||||
public Guid? ProjectId { get; set; }
|
||||
/// <summary>
|
||||
/// The unique identifier of the related service account that the event describes.
|
||||
/// </summary>
|
||||
/// <example>e68b8629-85eb-4929-92c0-b84464976ba4</example>
|
||||
|
||||
@@ -7,6 +7,7 @@ using Bit.Api.SecretsManager.Models.Response;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||
using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
@@ -29,6 +30,7 @@ public class ProjectsController : Controller
|
||||
private readonly IUpdateProjectCommand _updateProjectCommand;
|
||||
private readonly IDeleteProjectCommand _deleteProjectCommand;
|
||||
private readonly IAuthorizationService _authorizationService;
|
||||
private readonly IEventService _eventService;
|
||||
|
||||
public ProjectsController(
|
||||
ICurrentContext currentContext,
|
||||
@@ -38,7 +40,8 @@ public class ProjectsController : Controller
|
||||
ICreateProjectCommand createProjectCommand,
|
||||
IUpdateProjectCommand updateProjectCommand,
|
||||
IDeleteProjectCommand deleteProjectCommand,
|
||||
IAuthorizationService authorizationService)
|
||||
IAuthorizationService authorizationService,
|
||||
IEventService eventService)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_userService = userService;
|
||||
@@ -48,6 +51,7 @@ public class ProjectsController : Controller
|
||||
_updateProjectCommand = updateProjectCommand;
|
||||
_deleteProjectCommand = deleteProjectCommand;
|
||||
_authorizationService = authorizationService;
|
||||
_eventService = eventService;
|
||||
}
|
||||
|
||||
[HttpGet("organizations/{organizationId}/projects")]
|
||||
@@ -89,6 +93,11 @@ public class ProjectsController : Controller
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var result = await _createProjectCommand.CreateAsync(project, userId, _currentContext.IdentityClientType);
|
||||
|
||||
if (result != null)
|
||||
{
|
||||
await LogProjectEventAsync(project, EventType.Project_Created);
|
||||
}
|
||||
|
||||
// Creating a project means you have read & write permission.
|
||||
return new ProjectResponseModel(result, true, true);
|
||||
}
|
||||
@@ -106,6 +115,10 @@ public class ProjectsController : Controller
|
||||
}
|
||||
|
||||
var result = await _updateProjectCommand.UpdateAsync(updateRequest.ToProject(id));
|
||||
if (result != null)
|
||||
{
|
||||
await LogProjectEventAsync(project, EventType.Project_Edited);
|
||||
}
|
||||
|
||||
// Updating a project means you have read & write permission.
|
||||
return new ProjectResponseModel(result, true, true);
|
||||
@@ -136,6 +149,8 @@ public class ProjectsController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await LogProjectEventAsync(project, EventType.Project_Retrieved);
|
||||
|
||||
return new ProjectResponseModel(project, access.Read, access.Write);
|
||||
}
|
||||
|
||||
@@ -175,9 +190,32 @@ public class ProjectsController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
await _deleteProjectCommand.DeleteProjects(projectsToDelete);
|
||||
if (projectsToDelete.Count > 0)
|
||||
{
|
||||
await _deleteProjectCommand.DeleteProjects(projectsToDelete);
|
||||
await LogProjectsEventAsync(projectsToDelete, EventType.Project_Deleted);
|
||||
}
|
||||
|
||||
var responses = results.Select(r => new BulkDeleteResponseModel(r.Project.Id, r.Error));
|
||||
return new ListResponseModel<BulkDeleteResponseModel>(responses);
|
||||
}
|
||||
|
||||
|
||||
private async Task LogProjectsEventAsync(IEnumerable<Project> projects, EventType eventType)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User)!.Value;
|
||||
|
||||
switch (_currentContext.IdentityClientType)
|
||||
{
|
||||
case IdentityClientType.ServiceAccount:
|
||||
await _eventService.LogServiceAccountProjectsEventAsync(userId, projects, eventType);
|
||||
break;
|
||||
case IdentityClientType.User:
|
||||
await _eventService.LogUserProjectsEventAsync(userId, projects, eventType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private Task LogProjectEventAsync(Project project, EventType eventType) =>
|
||||
LogProjectsEventAsync(new[] { project }, eventType);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
using Bit.Api.SecretsManager.Models.Response;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.SecretsManager.Commands.Trash.Interfaces;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@@ -15,17 +19,23 @@ public class TrashController : Controller
|
||||
private readonly ISecretRepository _secretRepository;
|
||||
private readonly IEmptyTrashCommand _emptyTrashCommand;
|
||||
private readonly IRestoreTrashCommand _restoreTrashCommand;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IEventService _eventService;
|
||||
|
||||
public TrashController(
|
||||
ICurrentContext currentContext,
|
||||
ISecretRepository secretRepository,
|
||||
IEmptyTrashCommand emptyTrashCommand,
|
||||
IRestoreTrashCommand restoreTrashCommand)
|
||||
IRestoreTrashCommand restoreTrashCommand,
|
||||
IUserService userService,
|
||||
IEventService eventService)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_secretRepository = secretRepository;
|
||||
_emptyTrashCommand = emptyTrashCommand;
|
||||
_restoreTrashCommand = restoreTrashCommand;
|
||||
_userService = userService;
|
||||
_eventService = eventService;
|
||||
}
|
||||
|
||||
[HttpGet("secrets/{organizationId}/trash")]
|
||||
@@ -58,7 +68,9 @@ public class TrashController : Controller
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var deletedSecrets = await _secretRepository.GetManyTrashedSecretsByIds(ids);
|
||||
await _emptyTrashCommand.EmptyTrash(organizationId, ids);
|
||||
await LogSecretsTrashEventAsync(deletedSecrets, EventType.Secret_Permanently_Deleted);
|
||||
}
|
||||
|
||||
[HttpPost("secrets/{organizationId}/trash/restore")]
|
||||
@@ -75,5 +87,27 @@ public class TrashController : Controller
|
||||
}
|
||||
|
||||
await _restoreTrashCommand.RestoreTrash(organizationId, ids);
|
||||
await LogSecretsTrashEventAsync(ids, EventType.Secret_Restored);
|
||||
}
|
||||
|
||||
private async Task LogSecretsTrashEventAsync(IEnumerable<Guid> secretIds, EventType eventType)
|
||||
{
|
||||
var secrets = await _secretRepository.GetManyByIds(secretIds);
|
||||
await LogSecretsTrashEventAsync(secrets, eventType);
|
||||
}
|
||||
|
||||
private async Task LogSecretsTrashEventAsync(IEnumerable<Secret> secrets, EventType eventType)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User)!.Value;
|
||||
|
||||
switch (_currentContext.IdentityClientType)
|
||||
{
|
||||
case IdentityClientType.ServiceAccount:
|
||||
await _eventService.LogServiceAccountSecretsEventAsync(userId, secrets, eventType);
|
||||
break;
|
||||
case IdentityClientType.User:
|
||||
await _eventService.LogUserSecretsEventAsync(userId, secrets, eventType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ public class Event : ITableObject<Guid>, IEvent
|
||||
SystemUser = e.SystemUser;
|
||||
DomainName = e.DomainName;
|
||||
SecretId = e.SecretId;
|
||||
ProjectId = e.ProjectId;
|
||||
ServiceAccountId = e.ServiceAccountId;
|
||||
}
|
||||
|
||||
@@ -56,6 +57,7 @@ public class Event : ITableObject<Guid>, IEvent
|
||||
public EventSystemUser? SystemUser { get; set; }
|
||||
public string? DomainName { get; set; }
|
||||
public Guid? SecretId { get; set; }
|
||||
public Guid? ProjectId { get; set; }
|
||||
public Guid? ServiceAccountId { get; set; }
|
||||
|
||||
public void SetNewId()
|
||||
|
||||
@@ -93,4 +93,11 @@ public enum EventType : int
|
||||
Secret_Created = 2101,
|
||||
Secret_Edited = 2102,
|
||||
Secret_Deleted = 2103,
|
||||
Secret_Permanently_Deleted = 2104,
|
||||
Secret_Restored = 2105,
|
||||
|
||||
Project_Retrieved = 2200,
|
||||
Project_Created = 2201,
|
||||
Project_Edited = 2202,
|
||||
Project_Deleted = 2203,
|
||||
}
|
||||
|
||||
@@ -37,5 +37,6 @@ public class EventMessage : IEvent
|
||||
public EventSystemUser? SystemUser { get; set; }
|
||||
public string DomainName { get; set; }
|
||||
public Guid? SecretId { get; set; }
|
||||
public Guid? ProjectId { get; set; }
|
||||
public Guid? ServiceAccountId { get; set; }
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ public class AzureEvent : ITableEntity
|
||||
public int? SystemUser { get; set; }
|
||||
public string DomainName { get; set; }
|
||||
public Guid? SecretId { get; set; }
|
||||
public Guid? ProjectId { get; set; }
|
||||
public Guid? ServiceAccountId { get; set; }
|
||||
|
||||
public EventTableEntity ToEventTableEntity()
|
||||
@@ -65,7 +66,8 @@ public class AzureEvent : ITableEntity
|
||||
SystemUser = SystemUser.HasValue ? (EventSystemUser)SystemUser.Value : null,
|
||||
DomainName = DomainName,
|
||||
SecretId = SecretId,
|
||||
ServiceAccountId = ServiceAccountId
|
||||
ServiceAccountId = ServiceAccountId,
|
||||
ProjectId = ProjectId,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -95,6 +97,7 @@ public class EventTableEntity : IEvent
|
||||
SystemUser = e.SystemUser;
|
||||
DomainName = e.DomainName;
|
||||
SecretId = e.SecretId;
|
||||
ProjectId = e.ProjectId;
|
||||
ServiceAccountId = e.ServiceAccountId;
|
||||
}
|
||||
|
||||
@@ -122,6 +125,7 @@ public class EventTableEntity : IEvent
|
||||
public EventSystemUser? SystemUser { get; set; }
|
||||
public string DomainName { get; set; }
|
||||
public Guid? SecretId { get; set; }
|
||||
public Guid? ProjectId { get; set; }
|
||||
public Guid? ServiceAccountId { get; set; }
|
||||
|
||||
public AzureEvent ToAzureEvent()
|
||||
@@ -152,6 +156,7 @@ public class EventTableEntity : IEvent
|
||||
SystemUser = SystemUser.HasValue ? (int)SystemUser.Value : null,
|
||||
DomainName = DomainName,
|
||||
SecretId = SecretId,
|
||||
ProjectId = ProjectId,
|
||||
ServiceAccountId = ServiceAccountId
|
||||
};
|
||||
}
|
||||
@@ -218,6 +223,15 @@ public class EventTableEntity : IEvent
|
||||
});
|
||||
}
|
||||
|
||||
if (e.ProjectId.HasValue)
|
||||
{
|
||||
entities.Add(new EventTableEntity(e)
|
||||
{
|
||||
PartitionKey = pKey,
|
||||
RowKey = $"ProjectId={e.ProjectId}__Date={dateKey}__Uniquifier={uniquifier}"
|
||||
});
|
||||
}
|
||||
|
||||
return entities;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,5 +26,6 @@ public interface IEvent
|
||||
EventSystemUser? SystemUser { get; set; }
|
||||
string DomainName { get; set; }
|
||||
Guid? SecretId { get; set; }
|
||||
Guid? ProjectId { get; set; }
|
||||
Guid? ServiceAccountId { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
|
||||
#nullable enable
|
||||
@@ -11,6 +12,13 @@ public interface IEventRepository
|
||||
PageOptions pageOptions);
|
||||
Task<PagedResult<IEvent>> GetManyByOrganizationAsync(Guid organizationId, DateTime startDate, DateTime endDate,
|
||||
PageOptions pageOptions);
|
||||
|
||||
Task<PagedResult<IEvent>> GetManyBySecretAsync(Secret secret, DateTime startDate, DateTime endDate,
|
||||
PageOptions pageOptions);
|
||||
|
||||
Task<PagedResult<IEvent>> GetManyByProjectAsync(Project project, DateTime startDate, DateTime endDate,
|
||||
PageOptions pageOptions);
|
||||
|
||||
Task<PagedResult<IEvent>> GetManyByOrganizationActingUserAsync(Guid organizationId, Guid actingUserId,
|
||||
DateTime startDate, DateTime endDate, PageOptions pageOptions);
|
||||
Task<PagedResult<IEvent>> GetManyByProviderAsync(Guid providerId, DateTime startDate, DateTime endDate,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Azure.Data.Tables;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
@@ -34,6 +35,20 @@ public class EventRepository : IEventRepository
|
||||
return await GetManyAsync($"OrganizationId={organizationId}", "Date={0}", startDate, endDate, pageOptions);
|
||||
}
|
||||
|
||||
public async Task<PagedResult<IEvent>> GetManyBySecretAsync(Secret secret,
|
||||
DateTime startDate, DateTime endDate, PageOptions pageOptions)
|
||||
{
|
||||
return await GetManyAsync($"OrganizationId={secret.OrganizationId}",
|
||||
$"SecretId={secret.Id}__Date={{0}}", startDate, endDate, pageOptions); ;
|
||||
}
|
||||
|
||||
public async Task<PagedResult<IEvent>> GetManyByProjectAsync(Project project,
|
||||
DateTime startDate, DateTime endDate, PageOptions pageOptions)
|
||||
{
|
||||
return await GetManyAsync($"OrganizationId={project.OrganizationId}",
|
||||
$"ProjectId={project.Id}__Date={{0}}", startDate, endDate, pageOptions);
|
||||
}
|
||||
|
||||
public async Task<PagedResult<IEvent>> GetManyByOrganizationActingUserAsync(Guid organizationId, Guid actingUserId,
|
||||
DateTime startDate, DateTime endDate, PageOptions pageOptions)
|
||||
{
|
||||
|
||||
@@ -35,4 +35,6 @@ public interface IEventService
|
||||
Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type, EventSystemUser systemUser, DateTime? date = null);
|
||||
Task LogUserSecretsEventAsync(Guid userId, IEnumerable<Secret> secrets, EventType type, DateTime? date = null);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -464,6 +464,58 @@ public class EventService : IEventService
|
||||
await _eventWriteService.CreateManyAsync(eventMessages);
|
||||
}
|
||||
|
||||
public async Task LogUserProjectsEventAsync(Guid userId, IEnumerable<Project> projects, EventType type, DateTime? date = null)
|
||||
{
|
||||
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
var eventMessages = new List<IEvent>();
|
||||
|
||||
foreach (var project in projects)
|
||||
{
|
||||
if (!CanUseEvents(orgAbilities, project.OrganizationId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var e = new EventMessage(_currentContext)
|
||||
{
|
||||
OrganizationId = project.OrganizationId,
|
||||
Type = type,
|
||||
ProjectId = project.Id,
|
||||
UserId = userId,
|
||||
Date = date.GetValueOrDefault(DateTime.UtcNow)
|
||||
};
|
||||
eventMessages.Add(e);
|
||||
}
|
||||
|
||||
await _eventWriteService.CreateManyAsync(eventMessages);
|
||||
}
|
||||
|
||||
public async Task LogServiceAccountProjectsEventAsync(Guid serviceAccountId, IEnumerable<Project> projects, EventType type, DateTime? date = null)
|
||||
{
|
||||
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
|
||||
var eventMessages = new List<IEvent>();
|
||||
|
||||
foreach (var project in projects)
|
||||
{
|
||||
if (!CanUseEvents(orgAbilities, project.OrganizationId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var e = new EventMessage(_currentContext)
|
||||
{
|
||||
OrganizationId = project.OrganizationId,
|
||||
Type = type,
|
||||
ProjectId = project.Id,
|
||||
ServiceAccountId = serviceAccountId,
|
||||
Date = date.GetValueOrDefault(DateTime.UtcNow)
|
||||
};
|
||||
eventMessages.Add(e);
|
||||
}
|
||||
|
||||
await _eventWriteService.CreateManyAsync(eventMessages);
|
||||
}
|
||||
|
||||
private async Task<Guid?> GetProviderIdAsync(Guid? orgId)
|
||||
{
|
||||
if (_currentContext == null || !orgId.HasValue)
|
||||
|
||||
@@ -127,4 +127,16 @@ public class NoopEventService : IEventService
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task LogUserProjectsEventAsync(Guid userId, IEnumerable<Project> projects, EventType type,
|
||||
DateTime? date = null)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task LogServiceAccountProjectsEventAsync(Guid serviceAccountId, IEnumerable<Project> projects, EventType type,
|
||||
DateTime? date = null)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ public interface ISecretRepository
|
||||
Task<IEnumerable<Secret>> GetManyByOrganizationIdAsync(Guid organizationId, Guid userId, AccessClientType accessType);
|
||||
Task<IEnumerable<Secret>> GetManyByOrganizationIdInTrashByIdsAsync(Guid organizationId, IEnumerable<Guid> ids);
|
||||
Task<IEnumerable<Secret>> GetManyByIds(IEnumerable<Guid> ids);
|
||||
Task<IEnumerable<Secret>> GetManyTrashedSecretsByIds(IEnumerable<Guid> ids);
|
||||
Task<Secret> GetByIdAsync(Guid id);
|
||||
Task<Secret> CreateAsync(Secret secret, SecretAccessPoliciesUpdates accessPoliciesUpdates = null);
|
||||
Task<Secret> UpdateAsync(Secret secret, SecretAccessPoliciesUpdates accessPoliciesUpdates = null);
|
||||
|
||||
@@ -105,4 +105,6 @@ public class NoopSecretRepository : ISecretRepository
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task<IEnumerable<Secret>> GetManyTrashedSecretsByIds(IEnumerable<Guid> ids) => Task.FromResult<IEnumerable<Secret>>([]);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Dapper;
|
||||
@@ -41,8 +42,30 @@ public class EventRepository : Repository<Event, Guid>, IEventRepository
|
||||
}, startDate, endDate, pageOptions);
|
||||
}
|
||||
|
||||
public async Task<PagedResult<IEvent>> GetManyByOrganizationActingUserAsync(Guid organizationId, Guid actingUserId,
|
||||
public async Task<PagedResult<IEvent>> GetManyBySecretAsync(Secret secret,
|
||||
DateTime startDate, DateTime endDate, PageOptions pageOptions)
|
||||
{
|
||||
return await GetManyAsync($"[{Schema}].[Event_ReadPageBySecretId]",
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["@SecretId"] = secret.Id
|
||||
}, startDate, endDate, pageOptions);
|
||||
|
||||
}
|
||||
|
||||
public async Task<PagedResult<IEvent>> GetManyByProjectAsync(Project project,
|
||||
DateTime startDate, DateTime endDate, PageOptions pageOptions)
|
||||
{
|
||||
return await GetManyAsync($"[{Schema}].[Event_ReadPageByProjectId]",
|
||||
new Dictionary<string, object?>
|
||||
{
|
||||
["@ProjectId"] = project.Id
|
||||
}, startDate, endDate, pageOptions);
|
||||
|
||||
}
|
||||
|
||||
public async Task<PagedResult<IEvent>> GetManyByOrganizationActingUserAsync(Guid organizationId, Guid actingUserId,
|
||||
DateTime startDate, DateTime endDate, PageOptions pageOptions)
|
||||
{
|
||||
return await GetManyAsync($"[{Schema}].[Event_ReadPageByOrganizationIdActingUserId]",
|
||||
new Dictionary<string, object?>
|
||||
@@ -205,6 +228,8 @@ public class EventRepository : Repository<Event, Guid>, IEventRepository
|
||||
eventsTable.Columns.Add(secretIdColumn);
|
||||
var serviceAccountIdColumn = new DataColumn(nameof(e.ServiceAccountId), typeof(Guid));
|
||||
eventsTable.Columns.Add(serviceAccountIdColumn);
|
||||
var projectIdColumn = new DataColumn(nameof(e.ProjectId), typeof(Guid));
|
||||
eventsTable.Columns.Add(projectIdColumn);
|
||||
|
||||
foreach (DataColumn col in eventsTable.Columns)
|
||||
{
|
||||
@@ -237,7 +262,7 @@ public class EventRepository : Repository<Event, Guid>, IEventRepository
|
||||
row[dateColumn] = ev.Date;
|
||||
row[secretIdColumn] = ev.SecretId.HasValue ? ev.SecretId.Value : DBNull.Value;
|
||||
row[serviceAccountIdColumn] = ev.ServiceAccountId.HasValue ? ev.ServiceAccountId.Value : DBNull.Value;
|
||||
|
||||
row[projectIdColumn] = ev.ProjectId.HasValue ? ev.ProjectId.Value : DBNull.Value;
|
||||
eventsTable.Rows.Add(row);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using AutoMapper;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Infrastructure.EntityFramework.Models;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories.Queries;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
@@ -77,6 +78,57 @@ public class EventRepository : Repository<Core.Entities.Event, Event, Guid>, IEv
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<PagedResult<IEvent>> GetManyBySecretAsync(Secret secret,
|
||||
DateTime startDate, DateTime endDate, PageOptions pageOptions)
|
||||
{
|
||||
DateTime? beforeDate = null;
|
||||
if (!string.IsNullOrWhiteSpace(pageOptions.ContinuationToken) &&
|
||||
long.TryParse(pageOptions.ContinuationToken, out var binaryDate))
|
||||
{
|
||||
beforeDate = DateTime.SpecifyKind(DateTime.FromBinary(binaryDate), DateTimeKind.Utc);
|
||||
}
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var query = new EventReadPageBySecretQuery(secret, startDate, endDate, beforeDate, pageOptions);
|
||||
var events = await query.Run(dbContext).ToListAsync();
|
||||
|
||||
var result = new PagedResult<IEvent>();
|
||||
if (events.Any() && events.Count >= pageOptions.PageSize)
|
||||
{
|
||||
result.ContinuationToken = events.Last().Date.ToBinary().ToString();
|
||||
}
|
||||
result.Data.AddRange(events);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<PagedResult<IEvent>> GetManyByProjectAsync(Project project,
|
||||
DateTime startDate, DateTime endDate, PageOptions pageOptions)
|
||||
{
|
||||
DateTime? beforeDate = null;
|
||||
if (!string.IsNullOrWhiteSpace(pageOptions.ContinuationToken) &&
|
||||
long.TryParse(pageOptions.ContinuationToken, out var binaryDate))
|
||||
{
|
||||
beforeDate = DateTime.SpecifyKind(DateTime.FromBinary(binaryDate), DateTimeKind.Utc);
|
||||
}
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
var query = new EventReadPageByProjectQuery(project, startDate, endDate, beforeDate, pageOptions);
|
||||
var events = await query.Run(dbContext).ToListAsync();
|
||||
|
||||
var result = new PagedResult<IEvent>();
|
||||
if (events.Any() && events.Count >= pageOptions.PageSize)
|
||||
{
|
||||
result.ContinuationToken = events.Last().Date.ToBinary().ToString();
|
||||
}
|
||||
result.Data.AddRange(events);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public async Task<PagedResult<IEvent>> GetManyByCipherAsync(Cipher cipher, DateTime startDate, DateTime endDate, PageOptions pageOptions)
|
||||
{
|
||||
DateTime? beforeDate = null;
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
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 EventReadPageByProjectQuery : IQuery<Event>
|
||||
{
|
||||
private readonly Project _project;
|
||||
private readonly DateTime _startDate;
|
||||
private readonly DateTime _endDate;
|
||||
private readonly DateTime? _beforeDate;
|
||||
private readonly PageOptions _pageOptions;
|
||||
|
||||
public EventReadPageByProjectQuery(Project project, DateTime startDate, DateTime endDate, PageOptions pageOptions)
|
||||
{
|
||||
_project = project;
|
||||
_startDate = startDate;
|
||||
_endDate = endDate;
|
||||
_beforeDate = null;
|
||||
_pageOptions = pageOptions;
|
||||
}
|
||||
|
||||
public EventReadPageByProjectQuery(Project project, DateTime startDate, DateTime endDate, DateTime? beforeDate, PageOptions pageOptions)
|
||||
{
|
||||
_project = project;
|
||||
_startDate = startDate;
|
||||
_endDate = endDate;
|
||||
_beforeDate = beforeDate;
|
||||
_pageOptions = pageOptions;
|
||||
}
|
||||
|
||||
public IQueryable<Event> Run(DatabaseContext dbContext)
|
||||
{
|
||||
var emptyGuid = Guid.Empty;
|
||||
var q = from e in dbContext.Events
|
||||
where e.Date >= _startDate &&
|
||||
(_beforeDate == null || e.Date < _beforeDate.Value) &&
|
||||
(
|
||||
(_project.OrganizationId == emptyGuid && !e.OrganizationId.HasValue) ||
|
||||
(_project.OrganizationId != emptyGuid && e.OrganizationId == _project.OrganizationId)
|
||||
) &&
|
||||
e.ProjectId == _project.Id
|
||||
orderby e.Date descending
|
||||
select e;
|
||||
|
||||
return q.Take(_pageOptions.PageSize);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
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 EventReadPageBySecretQuery : IQuery<Event>
|
||||
{
|
||||
private readonly Secret _secret;
|
||||
private readonly DateTime _startDate;
|
||||
private readonly DateTime _endDate;
|
||||
private readonly DateTime? _beforeDate;
|
||||
private readonly PageOptions _pageOptions;
|
||||
|
||||
public EventReadPageBySecretQuery(Secret secret, DateTime startDate, DateTime endDate, PageOptions pageOptions)
|
||||
{
|
||||
_secret = secret;
|
||||
_startDate = startDate;
|
||||
_endDate = endDate;
|
||||
_beforeDate = null;
|
||||
_pageOptions = pageOptions;
|
||||
}
|
||||
|
||||
public EventReadPageBySecretQuery(Secret secret, DateTime startDate, DateTime endDate, DateTime? beforeDate, PageOptions pageOptions)
|
||||
{
|
||||
_secret = secret;
|
||||
_startDate = startDate;
|
||||
_endDate = endDate;
|
||||
_beforeDate = beforeDate;
|
||||
_pageOptions = pageOptions;
|
||||
}
|
||||
|
||||
public IQueryable<Event> Run(DatabaseContext dbContext)
|
||||
{
|
||||
var emptyGuid = Guid.Empty;
|
||||
var q = from e in dbContext.Events
|
||||
where e.Date >= _startDate &&
|
||||
(_beforeDate == null || e.Date < _beforeDate.Value) &&
|
||||
(
|
||||
(_secret.OrganizationId == emptyGuid && !e.OrganizationId.HasValue) ||
|
||||
(_secret.OrganizationId != emptyGuid && e.OrganizationId == _secret.OrganizationId)
|
||||
) &&
|
||||
e.SecretId == _secret.Id
|
||||
orderby e.Date descending
|
||||
select e;
|
||||
|
||||
return q.Take(_pageOptions.PageSize);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
CREATE PROCEDURE [dbo].[Event_ReadPageByProjectId]
|
||||
@ProjectId 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
|
||||
FROM
|
||||
[dbo].[EventView] e
|
||||
WHERE
|
||||
[Date] >= @StartDate
|
||||
AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate)
|
||||
AND (@BeforeDate IS NULL OR [Date] < @BeforeDate)
|
||||
AND [ProjectId] = @ProjectId
|
||||
ORDER BY [Date] DESC
|
||||
OFFSET 0 ROWS
|
||||
FETCH NEXT @PageSize ROWS ONLY
|
||||
END
|
||||
@@ -0,0 +1,44 @@
|
||||
CREATE PROCEDURE [dbo].[Event_ReadPageBySecretId]
|
||||
@SecretId 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
|
||||
FROM
|
||||
[dbo].[EventView] e
|
||||
WHERE
|
||||
[Date] >= @StartDate
|
||||
AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate)
|
||||
AND (@BeforeDate IS NULL OR [Date] < @BeforeDate)
|
||||
AND [SecretId] = @SecretId
|
||||
ORDER BY [Date] DESC
|
||||
OFFSET 0 ROWS
|
||||
FETCH NEXT @PageSize ROWS ONLY
|
||||
END
|
||||
@@ -19,7 +19,8 @@
|
||||
@SystemUser TINYINT = null,
|
||||
@DomainName VARCHAR(256),
|
||||
@SecretId UNIQUEIDENTIFIER = null,
|
||||
@ServiceAccountId UNIQUEIDENTIFIER = null
|
||||
@ServiceAccountId UNIQUEIDENTIFIER = null,
|
||||
@ProjectId UNIQUEIDENTIFIER = null
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@@ -46,7 +47,8 @@ BEGIN
|
||||
[SystemUser],
|
||||
[DomainName],
|
||||
[SecretId],
|
||||
[ServiceAccountId]
|
||||
[ServiceAccountId],
|
||||
[ProjectId]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@@ -70,6 +72,7 @@ BEGIN
|
||||
@SystemUser,
|
||||
@DomainName,
|
||||
@SecretId,
|
||||
@ServiceAccountId
|
||||
@ServiceAccountId,
|
||||
@ProjectId
|
||||
)
|
||||
END
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
[DomainName] VARCHAR(256) NULL,
|
||||
[SecretId] UNIQUEIDENTIFIER NULL,
|
||||
[ServiceAccountId] UNIQUEIDENTIFIER NULL,
|
||||
[ProjectId] UNIQUEIDENTIFIER NULL,
|
||||
CONSTRAINT [PK_Event] PRIMARY KEY CLUSTERED ([Id] ASC)
|
||||
);
|
||||
|
||||
|
||||
@@ -317,7 +317,7 @@ public class ProjectsControllerTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task BulkDeleteProjects_ReturnsAccessDeniedForProjectsWithoutAccess_Success(
|
||||
SutProvider<ProjectsController> sutProvider, List<Project> data)
|
||||
SutProvider<ProjectsController> sutProvider, Guid userId, List<Project> data)
|
||||
{
|
||||
|
||||
var ids = data.Select(project => project.Id).ToList();
|
||||
@@ -333,6 +333,7 @@ public class ProjectsControllerTests
|
||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), data.First(),
|
||||
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).Returns(AuthorizationResult.Failed());
|
||||
|
||||
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<IProjectRepository>().GetManyWithSecretsByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);
|
||||
var results = await sutProvider.Sut.BulkDeleteAsync(ids);
|
||||
@@ -346,7 +347,7 @@ public class ProjectsControllerTests
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task BulkDeleteProjects_Success(SutProvider<ProjectsController> sutProvider, List<Project> data)
|
||||
public async Task BulkDeleteProjects_Success(SutProvider<ProjectsController> sutProvider, Guid userId, List<Project> data)
|
||||
{
|
||||
var ids = data.Select(project => project.Id).ToList();
|
||||
var organizationId = data.First().OrganizationId;
|
||||
@@ -357,7 +358,7 @@ public class ProjectsControllerTests
|
||||
.AuthorizeAsync(Arg.Any<ClaimsPrincipal>(), project,
|
||||
Arg.Any<IEnumerable<IAuthorizationRequirement>>()).ReturnsForAnyArgs(AuthorizationResult.Success());
|
||||
}
|
||||
|
||||
sutProvider.GetDependency<IUserService>().GetProperUserId(default).ReturnsForAnyArgs(userId);
|
||||
sutProvider.GetDependency<IProjectRepository>().GetManyWithSecretsByIds(Arg.Is(ids)).ReturnsForAnyArgs(data);
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessSecretsManager(Arg.Is(organizationId)).ReturnsForAnyArgs(true);
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
IF COL_LENGTH('[dbo].[Event]', 'ProjectId') IS NULL
|
||||
BEGIN
|
||||
EXEC('ALTER TABLE [dbo].[Event] ADD [ProjectId] UNIQUEIDENTIFIER NULL');
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID('[dbo].[EventView]', 'V') IS NOT NULL
|
||||
BEGIN
|
||||
DROP VIEW [dbo].[EventView];
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE VIEW [dbo].[EventView]
|
||||
AS
|
||||
SELECT * FROM [dbo].[Event];
|
||||
GO
|
||||
@@ -0,0 +1,174 @@
|
||||
-- Create or alter Event_Create procedure
|
||||
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,
|
||||
@DomainName VARCHAR(256),
|
||||
@SecretId UNIQUEIDENTIFIER = NULL,
|
||||
@ServiceAccountId UNIQUEIDENTIFIER = NULL,
|
||||
@ProjectId UNIQUEIDENTIFIER = 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],
|
||||
[DomainName],
|
||||
[SecretId],
|
||||
[ServiceAccountId],
|
||||
[ProjectId]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@Id,
|
||||
@Type,
|
||||
@UserId,
|
||||
@OrganizationId,
|
||||
@InstallationId,
|
||||
@ProviderId,
|
||||
@CipherId,
|
||||
@CollectionId,
|
||||
@PolicyId,
|
||||
@GroupId,
|
||||
@OrganizationUserId,
|
||||
@ProviderUserId,
|
||||
@ProviderOrganizationId,
|
||||
@ActingUserId,
|
||||
@DeviceType,
|
||||
@IpAddress,
|
||||
@Date,
|
||||
@SystemUser,
|
||||
@DomainName,
|
||||
@SecretId,
|
||||
@ServiceAccountId,
|
||||
@ProjectId
|
||||
);
|
||||
END
|
||||
GO
|
||||
|
||||
-- Create or alter Event_ReadPageByProjectId procedure
|
||||
CREATE OR ALTER PROCEDURE [dbo].[Event_ReadPageByProjectId]
|
||||
@ProjectId 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
|
||||
FROM
|
||||
[dbo].[EventView] e
|
||||
WHERE
|
||||
[Date] >= @StartDate
|
||||
AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate)
|
||||
AND (@BeforeDate IS NULL OR [Date] < @BeforeDate)
|
||||
AND [ProjectId] = @ProjectId
|
||||
ORDER BY [Date] DESC
|
||||
OFFSET 0 ROWS
|
||||
FETCH NEXT @PageSize ROWS ONLY;
|
||||
END
|
||||
GO
|
||||
|
||||
-- Create or alter Event_ReadPageBySecretId procedure
|
||||
CREATE OR ALTER PROCEDURE [dbo].[Event_ReadPageBySecretId]
|
||||
@SecretId 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
|
||||
FROM
|
||||
[dbo].[EventView] e
|
||||
WHERE
|
||||
[Date] >= @StartDate
|
||||
AND (@BeforeDate IS NOT NULL OR [Date] <= @EndDate)
|
||||
AND (@BeforeDate IS NULL OR [Date] < @BeforeDate)
|
||||
AND [SecretId] = @SecretId
|
||||
ORDER BY [Date] DESC
|
||||
OFFSET 0 ROWS
|
||||
FETCH NEXT @PageSize ROWS ONLY;
|
||||
END
|
||||
GO
|
||||
3266
util/MySqlMigrations/Migrations/20250717164642_20250717_AddingProjectIdToEvent.Designer.cs
generated
Normal file
3266
util/MySqlMigrations/Migrations/20250717164642_20250717_AddingProjectIdToEvent.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.MySqlMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class _20250717_AddingProjectIdToEvent : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "ProjectId",
|
||||
table: "Event",
|
||||
type: "char(36)",
|
||||
nullable: true,
|
||||
collation: "ascii_general_ci");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ProjectId",
|
||||
table: "Event");
|
||||
}
|
||||
}
|
||||
@@ -1295,6 +1295,9 @@ namespace Bit.MySqlMigrations.Migrations
|
||||
b.Property<Guid?>("PolicyId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid?>("ProjectId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<Guid?>("ProviderId")
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
|
||||
3272
util/PostgresMigrations/Migrations/20250717164620_20250717_AddingProjectIdToEvent.Designer.cs
generated
Normal file
3272
util/PostgresMigrations/Migrations/20250717164620_20250717_AddingProjectIdToEvent.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.PostgresMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class _20250717_AddingProjectIdToEvent : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "ProjectId",
|
||||
table: "Event",
|
||||
type: "uuid",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ProjectId",
|
||||
table: "Event");
|
||||
}
|
||||
}
|
||||
@@ -1300,6 +1300,9 @@ namespace Bit.PostgresMigrations.Migrations
|
||||
b.Property<Guid?>("PolicyId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ProjectId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<Guid?>("ProviderId")
|
||||
.HasColumnType("uuid");
|
||||
|
||||
|
||||
3255
util/SqliteMigrations/Migrations/20250717164556_20250717_AddingProjectIdToEvent.Designer.cs
generated
Normal file
3255
util/SqliteMigrations/Migrations/20250717164556_20250717_AddingProjectIdToEvent.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,27 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.SqliteMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class _20250717_AddingProjectIdToEvent : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<Guid>(
|
||||
name: "ProjectId",
|
||||
table: "Event",
|
||||
type: "TEXT",
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "ProjectId",
|
||||
table: "Event");
|
||||
}
|
||||
}
|
||||
@@ -1284,6 +1284,9 @@ namespace Bit.SqliteMigrations.Migrations
|
||||
b.Property<Guid?>("PolicyId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("ProjectId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("ProviderId")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user