Skip to content

Commit

Permalink
[PM-11127] Write OrganizationInstallation record when license is re…
Browse files Browse the repository at this point in the history
…trieved (#5090)

* Add SQL files

* Add SQL Server migration

* Add Core entity

* Add Dapper repository

* Add EF repository

* Add EF migrations

* Save OrganizationInstallation during GetLicense invocation

* Run dotnet format
  • Loading branch information
amorask-bitwarden authored Dec 11, 2024
1 parent 4c502f8 commit 2d891b3
Show file tree
Hide file tree
Showing 28 changed files with 9,751 additions and 2 deletions.
27 changes: 26 additions & 1 deletion src/Api/Billing/Controllers/OrganizationsController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
using Bit.Api.Models.Response;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Repositories;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Enums;
Expand Down Expand Up @@ -42,7 +44,8 @@ public class OrganizationsController(
IUpgradeOrganizationPlanCommand upgradeOrganizationPlanCommand,
IAddSecretsManagerSubscriptionCommand addSecretsManagerSubscriptionCommand,
IReferenceEventService referenceEventService,
ISubscriberService subscriberService)
ISubscriberService subscriberService,
IOrganizationInstallationRepository organizationInstallationRepository)
: Controller
{
[HttpGet("{id:guid}/subscription")]
Expand Down Expand Up @@ -97,6 +100,8 @@ public async Task<OrganizationLicense> GetLicense(Guid id, [FromQuery] Guid inst
throw new NotFoundException();
}

await SaveOrganizationInstallationAsync(id, installationId);

return license;
}

Expand Down Expand Up @@ -366,4 +371,24 @@ private async Task<Organization> AdjustOrganizationSeatsForSmTrialAsync(Guid id,

return await organizationRepository.GetByIdAsync(id);
}

private async Task SaveOrganizationInstallationAsync(Guid organizationId, Guid installationId)
{
var organizationInstallation =
await organizationInstallationRepository.GetByInstallationIdAsync(installationId);

if (organizationInstallation == null)
{
await organizationInstallationRepository.CreateAsync(new OrganizationInstallation
{
OrganizationId = organizationId,
InstallationId = installationId
});
}
else if (organizationInstallation.OrganizationId == organizationId)
{
organizationInstallation.RevisionDate = DateTime.UtcNow;
await organizationInstallationRepository.ReplaceAsync(organizationInstallation);
}
}
}
24 changes: 24 additions & 0 deletions src/Core/Billing/Entities/OrganizationInstallation.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Bit.Core.Entities;
using Bit.Core.Utilities;

namespace Bit.Core.Billing.Entities;

#nullable enable

public class OrganizationInstallation : ITableObject<Guid>
{
public Guid Id { get; set; }

public Guid OrganizationId { get; set; }
public Guid InstallationId { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime? RevisionDate { get; set; }

public void SetNewId()
{
if (Id == default)
{
Id = CoreHelpers.GenerateComb();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Bit.Core.Billing.Entities;
using Bit.Core.Repositories;

namespace Bit.Core.Billing.Repositories;

public interface IOrganizationInstallationRepository : IRepository<OrganizationInstallation, Guid>
{
Task<OrganizationInstallation> GetByInstallationIdAsync(Guid installationId);
Task<ICollection<OrganizationInstallation>> GetByOrganizationIdAsync(Guid organizationId);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using System.Data;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Repositories;
using Bit.Core.Settings;
using Bit.Infrastructure.Dapper.Repositories;
using Dapper;
using Microsoft.Data.SqlClient;

namespace Bit.Infrastructure.Dapper.Billing.Repositories;

public class OrganizationInstallationRepository(
GlobalSettings globalSettings) : Repository<OrganizationInstallation, Guid>(
globalSettings.SqlServer.ConnectionString,
globalSettings.SqlServer.ReadOnlyConnectionString), IOrganizationInstallationRepository
{
public async Task<OrganizationInstallation> GetByInstallationIdAsync(Guid installationId)
{
var sqlConnection = new SqlConnection(ConnectionString);

var results = await sqlConnection.QueryAsync<OrganizationInstallation>(
"[dbo].[OrganizationInstallation_ReadByInstallationId]",
new { InstallationId = installationId },
commandType: CommandType.StoredProcedure);

return results.FirstOrDefault();
}

public async Task<ICollection<OrganizationInstallation>> GetByOrganizationIdAsync(Guid organizationId)
{
var sqlConnection = new SqlConnection(ConnectionString);

var results = await sqlConnection.QueryAsync<OrganizationInstallation>(
"[dbo].[OrganizationInstallation_ReadByOrganizationId]",
new { OrganizationId = organizationId },
commandType: CommandType.StoredProcedure);

return results.ToArray();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public static void AddDapperRepositories(this IServiceCollection services, bool
services.AddSingleton<IPasswordHealthReportApplicationRepository, PasswordHealthReportApplicationRepository>();
services.AddSingleton<ISecurityTaskRepository, SecurityTaskRepository>();
services.AddSingleton<IUserAsymmetricKeysRepository, UserAsymmetricKeysRepository>();
services.AddSingleton<IOrganizationInstallationRepository, OrganizationInstallationRepository>();

if (selfHosted)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using Bit.Infrastructure.EntityFramework.Billing.Models;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;

namespace Bit.Infrastructure.EntityFramework.Billing.Configurations;

public class OrganizationInstallationEntityTypeConfiguration : IEntityTypeConfiguration<OrganizationInstallation>
{
public void Configure(EntityTypeBuilder<OrganizationInstallation> builder)
{
builder
.Property(oi => oi.Id)
.ValueGeneratedNever();

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

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

builder
.HasIndex(oi => oi.InstallationId)
.IsClustered(false);

builder.ToTable(nameof(OrganizationInstallation));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
using AutoMapper;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
using Bit.Infrastructure.EntityFramework.Models;

namespace Bit.Infrastructure.EntityFramework.Billing.Models;

public class OrganizationInstallation : Core.Billing.Entities.OrganizationInstallation
{
public virtual Installation Installation { get; set; }
public virtual Organization Organization { get; set; }
}

public class OrganizationInstallationMapperProfile : Profile
{
public OrganizationInstallationMapperProfile()
{
CreateMap<Core.Billing.Entities.OrganizationInstallation, OrganizationInstallation>().ReverseMap();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
using AutoMapper;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using EFOrganizationInstallation = Bit.Infrastructure.EntityFramework.Billing.Models.OrganizationInstallation;

namespace Bit.Infrastructure.EntityFramework.Billing.Repositories;

public class OrganizationInstallationRepository(
IMapper mapper,
IServiceScopeFactory serviceScopeFactory) : Repository<OrganizationInstallation, EFOrganizationInstallation, Guid>(
serviceScopeFactory,
mapper,
context => context.OrganizationInstallations), IOrganizationInstallationRepository
{
public async Task<OrganizationInstallation> GetByInstallationIdAsync(Guid installationId)
{
using var serviceScope = ServiceScopeFactory.CreateScope();

var databaseContext = GetDatabaseContext(serviceScope);

var query =
from organizationInstallation in databaseContext.OrganizationInstallations
where organizationInstallation.Id == installationId
select organizationInstallation;

return await query.FirstOrDefaultAsync();
}

public async Task<ICollection<OrganizationInstallation>> GetByOrganizationIdAsync(Guid organizationId)
{
using var serviceScope = ServiceScopeFactory.CreateScope();

var databaseContext = GetDatabaseContext(serviceScope);

var query =
from organizationInstallation in databaseContext.OrganizationInstallations
where organizationInstallation.OrganizationId == organizationId
select organizationInstallation;

return await query.ToArrayAsync();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public DatabaseContext(DbContextOptions<DatabaseContext> options)
public DbSet<ClientOrganizationMigrationRecord> ClientOrganizationMigrationRecords { get; set; }
public DbSet<PasswordHealthReportApplication> PasswordHealthReportApplications { get; set; }
public DbSet<SecurityTask> SecurityTasks { get; set; }
public DbSet<OrganizationInstallation> OrganizationInstallations { get; set; }

protected override void OnModelCreating(ModelBuilder builder)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
CREATE PROCEDURE [dbo].[OrganizationInstallation_Create]
@Id UNIQUEIDENTIFIER OUTPUT,
@OrganizationId UNIQUEIDENTIFIER,
@InstallationId UNIQUEIDENTIFIER,
@CreationDate DATETIME2 (7),
@RevisionDate DATETIME2 (7) = NULL
AS
BEGIN
SET NOCOUNT ON

INSERT INTO [dbo].[OrganizationInstallation]
(
[Id],
[OrganizationId],
[InstallationId],
[CreationDate],
[RevisionDate]
)
VALUES
(
@Id,
@OrganizationId,
@InstallationId,
@CreationDate,
@RevisionDate
)
END
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE PROCEDURE [dbo].[OrganizationInstallation_DeleteById]
@Id UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON

DELETE
FROM
[dbo].[OrganizationInstallation]
WHERE
[Id] = @Id
END
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CREATE PROCEDURE [dbo].[OrganizationInstallation_ReadById]
@Id UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON

SELECT
*
FROM
[dbo].[OrganizationInstallationView]
WHERE
[Id] = @Id
END
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CREATE PROCEDURE [dbo].[OrganizationInstallation_ReadByInstallationId]
@InstallationId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON

SELECT
*
FROM
[dbo].[OrganizationInstallationView]
WHERE
[InstallationId] = @InstallationId
END
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CREATE PROCEDURE [dbo].[OrganizationInstallation_ReadByOrganizationId]
@OrganizationId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON

SELECT
*
FROM
[dbo].[OrganizationInstallationView]
WHERE
[OrganizationId] = @OrganizationId
END
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
CREATE PROCEDURE [dbo].[OrganizationInstallation_Update]
@Id UNIQUEIDENTIFIER OUTPUT,
@OrganizationId UNIQUEIDENTIFIER,
@InstallationId UNIQUEIDENTIFIER,
@CreationDate DATETIME2 (7),
@RevisionDate DATETIME2 (7)
AS
BEGIN
SET NOCOUNT ON

UPDATE
[dbo].[OrganizationInstallation]
SET
[RevisionDate] = @RevisionDate
WHERE
[Id] = @Id
END
18 changes: 18 additions & 0 deletions src/Sql/Billing/dbo/Tables/OrganizationInstallation.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
CREATE TABLE [dbo].[OrganizationInstallation] (
[Id] UNIQUEIDENTIFIER NOT NULL,
[OrganizationId] UNIQUEIDENTIFIER NOT NULL,
[InstallationId] UNIQUEIDENTIFIER NOT NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
[RevisionDate] DATETIME2 (7) NULL,
CONSTRAINT [PK_OrganizationInstallation] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_OrganizationInstallation_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE,
CONSTRAINT [FK_OrganizationInstallation_Installation] FOREIGN KEY ([InstallationId]) REFERENCES [dbo].[Installation] ([Id]) ON DELETE CASCADE
);
GO

CREATE NONCLUSTERED INDEX [IX_OrganizationInstallation_OrganizationId]
ON [dbo].[OrganizationInstallation]([OrganizationId] ASC);
GO

CREATE NONCLUSTERED INDEX [IX_OrganizationInstallation_InstallationId]
ON [dbo].[OrganizationInstallation]([InstallationId] ASC);
6 changes: 6 additions & 0 deletions src/Sql/Billing/dbo/Views/OrganizationInstallationView.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE VIEW [dbo].[OrganizationInstallationView]
AS
SELECT
*
FROM
[dbo].[OrganizationInstallation];
Loading

0 comments on commit 2d891b3

Please sign in to comment.