1
0
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:
cd-bitwarden
2025-08-20 10:24:17 -04:00
committed by GitHub
parent 7a6fa5a457
commit 3cad054af1
39 changed files with 10698 additions and 15 deletions

View File

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

View File

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

View File

@@ -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;
}
}

View File

@@ -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; }
}

View File

@@ -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>

View File

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

View File

@@ -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;
}
}
}

View File

@@ -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()

View File

@@ -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,
}

View File

@@ -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; }
}

View File

@@ -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;
}

View File

@@ -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; }
}

View File

@@ -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,

View File

@@ -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)
{

View File

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

View File

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

View File

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

View File

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

View File

@@ -105,4 +105,6 @@ public class NoopSecretRepository : ISecretRepository
{
return Task.FromResult(0);
}
public Task<IEnumerable<Secret>> GetManyTrashedSecretsByIds(IEnumerable<Guid> ids) => Task.FromResult<IEnumerable<Secret>>([]);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

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

View File

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

View File

@@ -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

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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");
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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");
}
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -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");
}
}

View File

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