Skip to content

Commit

Permalink
[PM-14373] Introduce SecurityTask database table and repository (#5025)
Browse files Browse the repository at this point in the history
* [PM-14373] Introduce SecurityTask entity and related enums

* [PM-14373] Add Dapper SecurityTask repository

* [PM-14373] Introduce MSSQL table, view, and stored procedures

* [PM-14373] Add EF SecurityTask repository and type configurations

* [PM-14373] Add EF Migration

* [PM-14373] Add integration tests

* [PM-14373] Formatting

* Typo

Co-authored-by: Matt Bishop <[email protected]>

* Typo

Co-authored-by: Matt Bishop <[email protected]>

* [PM-14373] Remove DeleteById sproc

* [PM-14373] SQL formatting

---------

Co-authored-by: Matt Bishop <[email protected]>
  • Loading branch information
shane-melton and withinfocus authored Nov 14, 2024
1 parent 8b1b078 commit eee7494
Show file tree
Hide file tree
Showing 27 changed files with 9,622 additions and 0 deletions.
20 changes: 20 additions & 0 deletions src/Core/Vault/Entities/SecurityTask.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Bit.Core.Entities;
using Bit.Core.Utilities;

namespace Bit.Core.Vault.Entities;

public class SecurityTask : ITableObject<Guid>
{
public Guid Id { get; set; }
public Guid OrganizationId { get; set; }
public Guid? CipherId { get; set; }
public Enums.SecurityTaskType Type { get; set; }
public Enums.SecurityTaskStatus Status { get; set; }
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;

public void SetNewId()
{
Id = CoreHelpers.GenerateComb();
}
}
18 changes: 18 additions & 0 deletions src/Core/Vault/Enums/SecurityTaskStatus.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using System.ComponentModel.DataAnnotations;

namespace Bit.Core.Vault.Enums;

public enum SecurityTaskStatus : byte
{
/// <summary>
/// Default status for newly created tasks that have not been completed.
/// </summary>
[Display(Name = "Pending")]
Pending = 0,

/// <summary>
/// Status when a task is considered complete and has no remaining actions
/// </summary>
[Display(Name = "Completed")]
Completed = 1,
}
12 changes: 12 additions & 0 deletions src/Core/Vault/Enums/SecurityTaskType.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;

namespace Bit.Core.Vault.Enums;

public enum SecurityTaskType : byte
{
/// <summary>
/// Task to update a cipher's password that was found to be at-risk by an administrator
/// </summary>
[Display(Name = "Update at-risk credential")]
UpdateAtRiskCredential = 0
}
9 changes: 9 additions & 0 deletions src/Core/Vault/Repositories/ISecurityTaskRepository.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Bit.Core.Repositories;
using Bit.Core.Vault.Entities;

namespace Bit.Core.Vault.Repositories;

public interface ISecurityTaskRepository : IRepository<SecurityTask, Guid>
{

}
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public static void AddDapperRepositories(this IServiceCollection services, bool
services
.AddSingleton<IClientOrganizationMigrationRecordRepository, ClientOrganizationMigrationRecordRepository>();
services.AddSingleton<IPasswordHealthReportApplicationRepository, PasswordHealthReportApplicationRepository>();
services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();

if (selfHosted)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using Bit.Core.Settings;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Repositories;
using Bit.Infrastructure.Dapper.Repositories;

namespace Bit.Infrastructure.Dapper.Vault.Repositories;

public class SecurityTaskRepository : Repository<SecurityTask, Guid>, ISecurityTaskRepository
{
public SecurityTaskRepository(GlobalSettings globalSettings)
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
{ }

public SecurityTaskRepository(string connectionString, string readOnlyConnectionString)
: base(connectionString, readOnlyConnectionString)
{ }

}
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ public static void AddPasswordManagerEFRepositories(this IServiceCollection serv
services
.AddSingleton<IClientOrganizationMigrationRecordRepository, ClientOrganizationMigrationRecordRepository>();
services.AddSingleton<IPasswordHealthReportApplicationRepository, PasswordHealthReportApplicationRepository>();
services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();

if (selfHosted)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ public DatabaseContext(DbContextOptions<DatabaseContext> options)
public DbSet<NotificationStatus> NotificationStatuses { get; set; }
public DbSet<ClientOrganizationMigrationRecord> ClientOrganizationMigrationRecords { get; set; }
public DbSet<PasswordHealthReportApplication> PasswordHealthReportApplications { get; set; }
public DbSet<SecurityTask> SecurityTasks { get; set; }

protected override void OnModelCreating(ModelBuilder builder)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Bit.Infrastructure.EntityFramework.Vault.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Bit.Infrastructure.EntityFramework.Vault.Configurations;

public class SecurityTaskEntityTypeConfiguration : IEntityTypeConfiguration<SecurityTask>
{
public void Configure(EntityTypeBuilder<SecurityTask> builder)
{
builder
.Property(s => s.Id)
.ValueGeneratedNever();

builder
.HasKey(s => s.Id)
.IsClustered();

builder
.HasIndex(s => s.OrganizationId)
.IsClustered(false);

builder
.HasIndex(s => s.CipherId)
.IsClustered(false);

builder
.ToTable(nameof(SecurityTask));
}
}
18 changes: 18 additions & 0 deletions src/Infrastructure.EntityFramework/Vault/Models/SecurityTask.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
using AutoMapper;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;

namespace Bit.Infrastructure.EntityFramework.Vault.Models;

public class SecurityTask : Core.Vault.Entities.SecurityTask
{
public virtual Organization Organization { get; set; }
public virtual Cipher Cipher { get; set; }
}

public class SecurityTaskMapperProfile : Profile
{
public SecurityTaskMapperProfile()
{
CreateMap<Core.Vault.Entities.SecurityTask, SecurityTask>().ReverseMap();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using AutoMapper;
using Bit.Core.Vault.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Infrastructure.EntityFramework.Vault.Models;
using Microsoft.Extensions.DependencyInjection;

namespace Bit.Infrastructure.EntityFramework.Vault.Repositories;

public class SecurityTaskRepository : Repository<Core.Vault.Entities.SecurityTask, SecurityTask, Guid>, ISecurityTaskRepository
{
public SecurityTaskRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper)
: base(serviceScopeFactory, mapper, (context) => context.SecurityTasks)
{ }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
CREATE PROCEDURE [dbo].[SecurityTask_Create]
@Id UNIQUEIDENTIFIER OUTPUT,
@OrganizationId UNIQUEIDENTIFIER,
@CipherId UNIQUEIDENTIFIER,
@Type TINYINT,
@Status TINYINT,
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7)
AS
BEGIN
SET NOCOUNT ON

INSERT INTO [dbo].[SecurityTask]
(
[Id],
[OrganizationId],
[CipherId],
[Type],
[Status],
[CreationDate],
[RevisionDate]
)
VALUES
(
@Id,
@OrganizationId,
@CipherId,
@Type,
@Status,
@CreationDate,
@RevisionDate
)
END
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CREATE PROCEDURE [dbo].[SecurityTask_ReadById]
@Id UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON

SELECT
*
FROM
[dbo].[SecurityTaskView]
WHERE
[Id] = @Id
END
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
CREATE PROCEDURE [dbo].[SecurityTask_Update]
@Id UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
@CipherId UNIQUEIDENTIFIER,
@Type TINYINT,
@Status TINYINT,
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7)
AS
BEGIN
SET NOCOUNT ON

UPDATE
[dbo].[SecurityTask]
SET
[OrganizationId] = @OrganizationId,
[CipherId] = @CipherId,
[Type] = @Type,
[Status] = @Status,
[CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate
WHERE
[Id] = @Id
END
21 changes: 21 additions & 0 deletions src/Sql/Vault/dbo/Tables/SecurityTask.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
CREATE TABLE [dbo].[SecurityTask]
(
[Id] UNIQUEIDENTIFIER NOT NULL,
[OrganizationId] UNIQUEIDENTIFIER NOT NULL,
[CipherId] UNIQUEIDENTIFIER NULL,
[Type] TINYINT NOT NULL,
[Status] TINYINT NOT NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
[RevisionDate] DATETIME2 (7) NOT NULL,
CONSTRAINT [PK_SecurityTask] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_SecurityTask_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_SecurityTask_Cipher] FOREIGN KEY ([CipherId]) REFERENCES [dbo].[Cipher] ([Id]) ON DELETE CASCADE,
);

GO
CREATE NONCLUSTERED INDEX [IX_SecurityTask_CipherId]
ON [dbo].[SecurityTask]([CipherId] ASC) WHERE CipherId IS NOT NULL;

GO
CREATE NONCLUSTERED INDEX [IX_SecurityTask_OrganizationId]
ON [dbo].[SecurityTask]([OrganizationId] ASC) WHERE OrganizationId IS NOT NULL;
6 changes: 6 additions & 0 deletions src/Sql/Vault/dbo/Views/SecurityTaskView.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE VIEW [dbo].[SecurityTaskView]
AS
SELECT
*
FROM
[dbo].[SecurityTask]
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Repositories;
using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Enums;
using Bit.Core.Vault.Repositories;
using Xunit;

namespace Bit.Infrastructure.IntegrationTest.Vault.Repositories;

public class SecurityTaskRepositoryTests
{
[DatabaseTheory, DatabaseData]
public async Task CreateAsync(
IOrganizationRepository organizationRepository,
ICipherRepository cipherRepository,
ISecurityTaskRepository securityTaskRepository)
{
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
PlanType = PlanType.EnterpriseAnnually,
Plan = "Test Plan",
BillingEmail = "[email protected]"
});

var cipher = await cipherRepository.CreateAsync(new Cipher
{
Type = CipherType.Login,
OrganizationId = organization.Id,
Data = "",
});

var task = await securityTaskRepository.CreateAsync(new SecurityTask
{
OrganizationId = organization.Id,
CipherId = cipher.Id,
Status = SecurityTaskStatus.Pending,
Type = SecurityTaskType.UpdateAtRiskCredential,
});

Assert.NotNull(task);
}

[DatabaseTheory, DatabaseData]
public async Task ReadByIdAsync(
IOrganizationRepository organizationRepository,
ICipherRepository cipherRepository,
ISecurityTaskRepository securityTaskRepository)
{
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
PlanType = PlanType.EnterpriseAnnually,
Plan = "Test Plan",
BillingEmail = "[email protected]"
});

var cipher = await cipherRepository.CreateAsync(new Cipher
{
Type = CipherType.Login,
OrganizationId = organization.Id,
Data = "",
});

var task = await securityTaskRepository.CreateAsync(new SecurityTask
{
OrganizationId = organization.Id,
CipherId = cipher.Id,
Status = SecurityTaskStatus.Pending,
Type = SecurityTaskType.UpdateAtRiskCredential,
});

Assert.NotNull(task);

var readTask = await securityTaskRepository.GetByIdAsync(task.Id);

Assert.NotNull(readTask);
Assert.Equal(task.Id, readTask.Id);
Assert.Equal(task.Status, readTask.Status);
}

[DatabaseTheory, DatabaseData]
public async Task UpdateAsync(
IOrganizationRepository organizationRepository,
ICipherRepository cipherRepository,
ISecurityTaskRepository securityTaskRepository)
{
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
PlanType = PlanType.EnterpriseAnnually,
Plan = "Test Plan",
BillingEmail = "[email protected]"
});

var cipher = await cipherRepository.CreateAsync(new Cipher
{
Type = CipherType.Login,
OrganizationId = organization.Id,
Data = "",
});

var task = await securityTaskRepository.CreateAsync(new SecurityTask
{
OrganizationId = organization.Id,
CipherId = cipher.Id,
Status = SecurityTaskStatus.Pending,
Type = SecurityTaskType.UpdateAtRiskCredential,
});

Assert.NotNull(task);

task.Status = SecurityTaskStatus.Completed;
await securityTaskRepository.ReplaceAsync(task);

var updatedTask = await securityTaskRepository.GetByIdAsync(task.Id);

Assert.NotNull(updatedTask);
Assert.Equal(task.Id, updatedTask.Id);
Assert.Equal(SecurityTaskStatus.Completed, updatedTask.Status);
}
}
Loading

0 comments on commit eee7494

Please sign in to comment.