1
0
mirror of https://github.com/bitwarden/server.git synced 2024-12-23 17:07:42 +01:00

[SM-389] Event log for service account (#2674)

This commit is contained in:
Oscar Hinton 2023-02-24 16:44:33 +01:00 committed by GitHub
parent 4643f5960e
commit 64e0a981c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 6744 additions and 11 deletions

View File

@ -32,6 +32,8 @@ public class EventResponseModel : ResponseModel
InstallationId = ev.InstallationId;
SystemUser = ev.SystemUser;
DomainName = ev.DomainName;
SecretId = ev.SecretId;
ServiceAccountId = ev.ServiceAccountId;
}
public EventType Type { get; set; }
@ -52,4 +54,6 @@ public class EventResponseModel : ResponseModel
public string IpAddress { get; set; }
public EventSystemUser? SystemUser { get; set; }
public string DomainName { get; set; }
public Guid? SecretId { get; set; }
public Guid? ServiceAccountId { get; set; }
}

View File

@ -4,6 +4,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.Commands.Secrets.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
@ -18,22 +19,32 @@ namespace Bit.Api.SecretsManager.Controllers;
public class SecretsController : Controller
{
private readonly ICurrentContext _currentContext;
private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository;
private readonly ISecretRepository _secretRepository;
private readonly ICreateSecretCommand _createSecretCommand;
private readonly IUpdateSecretCommand _updateSecretCommand;
private readonly IDeleteSecretCommand _deleteSecretCommand;
private readonly IUserService _userService;
private readonly IEventService _eventService;
public SecretsController(ISecretRepository secretRepository, IProjectRepository projectRepository, ICreateSecretCommand createSecretCommand, IUpdateSecretCommand updateSecretCommand, IDeleteSecretCommand deleteSecretCommand, IUserService userService, ICurrentContext currentContext)
public SecretsController(
ICurrentContext currentContext,
IProjectRepository projectRepository,
ISecretRepository secretRepository,
ICreateSecretCommand createSecretCommand,
IUpdateSecretCommand updateSecretCommand,
IDeleteSecretCommand deleteSecretCommand,
IUserService userService,
IEventService eventService)
{
_currentContext = currentContext;
_projectRepository = projectRepository;
_secretRepository = secretRepository;
_createSecretCommand = createSecretCommand;
_updateSecretCommand = updateSecretCommand;
_deleteSecretCommand = deleteSecretCommand;
_projectRepository = projectRepository;
_userService = userService;
_eventService = eventService;
}
[HttpGet("organizations/{organizationId}/secrets")]
@ -81,6 +92,12 @@ public class SecretsController : Controller
throw new NotFoundException();
}
if (_currentContext.ClientType == ClientType.ServiceAccount)
{
var userId = _userService.GetProperUserId(User).Value;
await _eventService.LogServiceAccountSecretEventAsync(userId, secret, EventType.Secret_Retrieved);
}
return new SecretResponseModel(secret);
}

View File

@ -29,6 +29,8 @@ public class Event : ITableObject<Guid>, IEvent
ActingUserId = e.ActingUserId;
SystemUser = e.SystemUser;
DomainName = e.DomainName;
SecretId = e.SecretId;
ServiceAccountId = e.ServiceAccountId;
}
public Guid Id { get; set; }
@ -51,7 +53,8 @@ public class Event : ITableObject<Guid>, IEvent
public Guid? ActingUserId { get; set; }
public EventSystemUser? SystemUser { get; set; }
public string DomainName { get; set; }
public Guid? SecretId { get; set; }
public Guid? ServiceAccountId { get; set; }
public void SetNewId()
{

View File

@ -80,5 +80,7 @@ public enum EventType : int
OrganizationDomain_Added = 2000,
OrganizationDomain_Removed = 2001,
OrganizationDomain_Verified = 2002,
OrganizationDomain_NotVerified = 2003
OrganizationDomain_NotVerified = 2003,
Secret_Retrieved = 2100,
}

View File

@ -33,4 +33,6 @@ public class EventMessage : IEvent
public Guid? IdempotencyId { get; private set; } = Guid.NewGuid();
public EventSystemUser? SystemUser { get; set; }
public string DomainName { get; set; }
public Guid? SecretId { get; set; }
public Guid? ServiceAccountId { get; set; }
}

View File

@ -28,6 +28,8 @@ public class EventTableEntity : TableEntity, IEvent
ActingUserId = e.ActingUserId;
SystemUser = e.SystemUser;
DomainName = e.DomainName;
SecretId = e.SecretId;
ServiceAccountId = e.ServiceAccountId;
}
public DateTime Date { get; set; }
@ -48,6 +50,8 @@ public class EventTableEntity : TableEntity, IEvent
public Guid? ActingUserId { get; set; }
public EventSystemUser? SystemUser { get; set; }
public string DomainName { get; set; }
public Guid? SecretId { get; set; }
public Guid? ServiceAccountId { get; set; }
public override IDictionary<string, EntityProperty> WriteEntity(OperationContext operationContext)
{
@ -154,6 +158,24 @@ public class EventTableEntity : TableEntity, IEvent
});
}
if (e.OrganizationId.HasValue && e.ServiceAccountId.HasValue)
{
entities.Add(new EventTableEntity(e)
{
PartitionKey = pKey,
RowKey = $"ServiceAccountId={e.ServiceAccountId}__Date={dateKey}__Uniquifier={uniquifier}"
});
}
if (e.SecretId.HasValue)
{
entities.Add(new EventTableEntity(e)
{
PartitionKey = pKey,
RowKey = $"SecretId={e.CipherId}__Date={dateKey}__Uniquifier={uniquifier}"
});
}
return entities;
}

View File

@ -22,4 +22,6 @@ public interface IEvent
DateTime Date { get; set; }
EventSystemUser? SystemUser { get; set; }
string DomainName { get; set; }
Guid? SecretId { get; set; }
Guid? ServiceAccountId { get; set; }
}

View File

@ -1,6 +1,7 @@
using Bit.Core.Entities;
using Bit.Core.Entities.Provider;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.Entities;
namespace Bit.Core.Services;
@ -25,4 +26,5 @@ public interface IEventService
Task LogProviderOrganizationEventAsync(ProviderOrganization providerOrganization, EventType type, DateTime? date = null);
Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type, DateTime? date = null);
Task LogOrganizationDomainEventAsync(OrganizationDomain organizationDomain, EventType type, EventSystemUser systemUser, DateTime? date = null);
Task LogServiceAccountSecretEventAsync(Guid serviceAccountId, Secret secret, EventType type, DateTime? date = null);
}

View File

@ -5,6 +5,7 @@ using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.Settings;
namespace Bit.Core.Services;
@ -391,6 +392,25 @@ public class EventService : IEventService
await _eventWriteService.CreateAsync(e);
}
public async Task LogServiceAccountSecretEventAsync(Guid serviceAccountId, Secret secret, EventType type, DateTime? date = null)
{
var orgAbilities = await _applicationCacheService.GetOrganizationAbilitiesAsync();
if (!CanUseEvents(orgAbilities, secret.OrganizationId))
{
return;
}
var e = new EventMessage(_currentContext)
{
OrganizationId = secret.OrganizationId,
Type = type,
SecretId = secret.Id,
ServiceAccountId = serviceAccountId,
Date = date.GetValueOrDefault(DateTime.UtcNow)
};
await _eventWriteService.CreateAsync(e);
}
private async Task<Guid?> GetProviderIdAsync(Guid? orgId)
{
if (_currentContext == null || !orgId.HasValue)
@ -414,12 +434,12 @@ public class EventService : IEventService
private bool CanUseEvents(IDictionary<Guid, OrganizationAbility> orgAbilities, Guid orgId)
{
return orgAbilities != null && orgAbilities.ContainsKey(orgId) &&
orgAbilities[orgId].Enabled && orgAbilities[orgId].UseEvents;
orgAbilities[orgId].Enabled && orgAbilities[orgId].UseEvents;
}
private bool CanUseProviderEvents(IDictionary<Guid, ProviderAbility> providerAbilities, Guid providerId)
{
return providerAbilities != null && providerAbilities.ContainsKey(providerId) &&
providerAbilities[providerId].Enabled && providerAbilities[providerId].UseEvents;
providerAbilities[providerId].Enabled && providerAbilities[providerId].UseEvents;
}
}

View File

@ -1,6 +1,7 @@
using Bit.Core.Entities;
using Bit.Core.Entities.Provider;
using Bit.Core.Enums;
using Bit.Core.SecretsManager.Entities;
namespace Bit.Core.Services;
@ -107,4 +108,9 @@ public class NoopEventService : IEventService
return Task.FromResult(0);
}
public Task LogServiceAccountSecretEventAsync(Guid serviceAccountId, Secret secret, EventType type,
DateTime? date = null)
{
return Task.FromResult(0);
}
}

View File

@ -17,7 +17,9 @@
@IpAddress VARCHAR(50),
@Date DATETIME2(7),
@SystemUser TINYINT = null,
@DomainName VARCHAR(256)
@DomainName VARCHAR(256),
@SecretId UNIQUEIDENTIFIER = null,
@ServiceAccountId UNIQUEIDENTIFIER = null
AS
BEGIN
SET NOCOUNT ON
@ -42,7 +44,9 @@ BEGIN
[IpAddress],
[Date],
[SystemUser],
[DomainName]
[DomainName],
[SecretId],
[ServiceAccountId]
)
VALUES
(
@ -64,6 +68,8 @@ BEGIN
@IpAddress,
@Date,
@SystemUser,
@DomainName
@DomainName,
@SecretId,
@ServiceAccountId
)
END

View File

@ -17,7 +17,9 @@
[ProviderUserId] UNIQUEIDENTIFIER NULL,
[ProviderOrganizationId] UNIQUEIDENTIFIER NULL,
[SystemUser] TINYINT NULL,
[DomainName] VARCHAR(256) NULL
[DomainName] VARCHAR(256) NULL,
[SecretId] UNIQUEIDENTIFIER NULL,
[ServiceAccountId] UNIQUEIDENTIFIER NULL,
CONSTRAINT [PK_Event] PRIMARY KEY CLUSTERED ([Id] ASC)
);

View File

@ -0,0 +1,100 @@
IF COL_LENGTH('[dbo].[Event]', 'SecretId') IS NULL
BEGIN
ALTER TABLE
[dbo].[Event]
ADD
[SecretId] UNIQUEIDENTIFIER NULL
END
GO
IF COL_LENGTH('[dbo].[Event]', 'ServiceAccountId') IS NULL
BEGIN
ALTER TABLE
[dbo].[Event]
ADD
[ServiceAccountId] UNIQUEIDENTIFIER NULL
END
GO
IF OBJECT_ID('[dbo].[EventView]') IS NOT NULL
BEGIN
EXECUTE sp_refreshsqlmodule N'[dbo].[EventView]';
END
GO
CREATE OR ALTER PROCEDURE [dbo].[Event_Create]
@Id UNIQUEIDENTIFIER OUTPUT,
@Type INT,
@UserId UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
@InstallationId UNIQUEIDENTIFIER,
@ProviderId UNIQUEIDENTIFIER,
@CipherId UNIQUEIDENTIFIER,
@CollectionId UNIQUEIDENTIFIER,
@PolicyId UNIQUEIDENTIFIER,
@GroupId UNIQUEIDENTIFIER,
@OrganizationUserId UNIQUEIDENTIFIER,
@ProviderUserId UNIQUEIDENTIFIER,
@ProviderOrganizationId UNIQUEIDENTIFIER = null,
@ActingUserId UNIQUEIDENTIFIER,
@DeviceType SMALLINT,
@IpAddress VARCHAR(50),
@Date DATETIME2(7),
@SystemUser TINYINT = null,
@DomainName VARCHAR(256),
@SecretId UNIQUEIDENTIFIER = null,
@ServiceAccountId 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]
)
VALUES
(
@Id,
@Type,
@UserId,
@OrganizationId,
@InstallationId,
@ProviderId,
@CipherId,
@CollectionId,
@PolicyId,
@GroupId,
@OrganizationUserId,
@ProviderUserId,
@ProviderOrganizationId,
@ActingUserId,
@DeviceType,
@IpAddress,
@Date,
@SystemUser,
@DomainName,
@SecretId,
@ServiceAccountId
)
END
GO

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,36 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.MySqlMigrations.Migrations;
public partial class SecretsManagerEvent : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "SecretId",
table: "Event",
type: "char(36)",
nullable: true,
collation: "ascii_general_ci");
migrationBuilder.AddColumn<Guid>(
name: "ServiceAccountId",
table: "Event",
type: "char(36)",
nullable: true,
collation: "ascii_general_ci");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SecretId",
table: "Event");
migrationBuilder.DropColumn(
name: "ServiceAccountId",
table: "Event");
}
}

View File

@ -349,6 +349,12 @@ namespace Bit.MySqlMigrations.Migrations
b.Property<Guid?>("ProviderUserId")
.HasColumnType("char(36)");
b.Property<Guid?>("SecretId")
.HasColumnType("char(36)");
b.Property<Guid?>("ServiceAccountId")
.HasColumnType("char(36)");
b.Property<byte?>("SystemUser")
.HasColumnType("tinyint unsigned");

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -353,6 +353,12 @@ namespace Bit.PostgresMigrations.Migrations
b.Property<Guid?>("ProviderUserId")
.HasColumnType("uuid");
b.Property<Guid?>("SecretId")
.HasColumnType("uuid");
b.Property<Guid?>("ServiceAccountId")
.HasColumnType("uuid");
b.Property<byte?>("SystemUser")
.HasColumnType("smallint");

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,34 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.SqliteMigrations.Migrations;
public partial class SecretsManagerEvent : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<Guid>(
name: "SecretId",
table: "Event",
type: "TEXT",
nullable: true);
migrationBuilder.AddColumn<Guid>(
name: "ServiceAccountId",
table: "Event",
type: "TEXT",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "SecretId",
table: "Event");
migrationBuilder.DropColumn(
name: "ServiceAccountId",
table: "Event");
}
}

View File

@ -344,6 +344,12 @@ namespace Bit.SqliteMigrations.Migrations
b.Property<Guid?>("ProviderUserId")
.HasColumnType("TEXT");
b.Property<Guid?>("SecretId")
.HasColumnType("TEXT");
b.Property<Guid?>("ServiceAccountId")
.HasColumnType("TEXT");
b.Property<byte?>("SystemUser")
.HasColumnType("INTEGER");