1
0
mirror of https://github.com/bitwarden/server synced 2026-01-07 19:13:50 +00:00

[EC-826] Merge license sync feature branch to master (#2587)

* [EC-634] Extract GenerateLicenseAsync to a query (#2373)

* [EC-637] Add license sync to server (#2453)

* [EC-1036] Show correct license sync date (#2626)

* Update method name per new pattern
This commit is contained in:
Thomas Rittson
2023-01-31 07:42:10 +10:00
committed by GitHub
parent d0355fcd12
commit 82908b1fb7
37 changed files with 746 additions and 123 deletions

View File

@@ -1,5 +1,6 @@
using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations.OrganizationConnections;
using Bit.Core.Models.OrganizationConnectionConfigs;
using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
using Bit.Core.Repositories;
@@ -14,7 +15,7 @@ public class CreateOrganizationConnectionCommand : ICreateOrganizationConnection
_organizationConnectionRepository = organizationConnectionRepository;
}
public async Task<OrganizationConnection> CreateAsync<T>(OrganizationConnectionData<T> connectionData) where T : new()
public async Task<OrganizationConnection> CreateAsync<T>(OrganizationConnectionData<T> connectionData) where T : IConnectionConfig
{
return await _organizationConnectionRepository.CreateAsync(connectionData.ToEntity());
}

View File

@@ -1,9 +1,10 @@
using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations.OrganizationConnections;
using Bit.Core.Models.OrganizationConnectionConfigs;
namespace Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
public interface ICreateOrganizationConnectionCommand
{
Task<OrganizationConnection> CreateAsync<T>(OrganizationConnectionData<T> connectionData) where T : new();
Task<OrganizationConnection> CreateAsync<T>(OrganizationConnectionData<T> connectionData) where T : IConnectionConfig;
}

View File

@@ -1,9 +1,10 @@
using Bit.Core.Entities;
using Bit.Core.Models.Data.Organizations.OrganizationConnections;
using Bit.Core.Models.OrganizationConnectionConfigs;
namespace Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
public interface IUpdateOrganizationConnectionCommand
{
Task<OrganizationConnection> UpdateAsync<T>(OrganizationConnectionData<T> connectionData) where T : new();
Task<OrganizationConnection> UpdateAsync<T>(OrganizationConnectionData<T> connectionData) where T : IConnectionConfig;
}

View File

@@ -1,6 +1,6 @@
using Bit.Core.Entities;
namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
namespace Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
public interface IValidateBillingSyncKeyCommand
{

View File

@@ -1,6 +1,7 @@
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations.OrganizationConnections;
using Bit.Core.Models.OrganizationConnectionConfigs;
using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
using Bit.Core.Repositories;
@@ -15,7 +16,7 @@ public class UpdateOrganizationConnectionCommand : IUpdateOrganizationConnection
_organizationConnectionRepository = organizationConnectionRepository;
}
public async Task<OrganizationConnection> UpdateAsync<T>(OrganizationConnectionData<T> connectionData) where T : new()
public async Task<OrganizationConnection> UpdateAsync<T>(OrganizationConnectionData<T> connectionData) where T : IConnectionConfig
{
if (!connectionData.Id.HasValue)
{

View File

@@ -1,20 +1,17 @@
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
using Bit.Core.Repositories;
namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;
namespace Bit.Core.OrganizationFeatures.OrganizationConnections;
public class ValidateBillingSyncKeyCommand : IValidateBillingSyncKeyCommand
{
private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository;
private readonly IOrganizationApiKeyRepository _apiKeyRepository;
public ValidateBillingSyncKeyCommand(
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
IOrganizationApiKeyRepository organizationApiKeyRepository)
{
_organizationSponsorshipRepository = organizationSponsorshipRepository;
_apiKeyRepository = organizationApiKeyRepository;
}

View File

@@ -0,0 +1,38 @@
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
namespace Bit.Core.OrganizationFeatures.OrganizationLicenses;
public class CloudGetOrganizationLicenseQuery : ICloudGetOrganizationLicenseQuery
{
private readonly IInstallationRepository _installationRepository;
private readonly IPaymentService _paymentService;
private readonly ILicensingService _licensingService;
public CloudGetOrganizationLicenseQuery(
IInstallationRepository installationRepository,
IPaymentService paymentService,
ILicensingService licensingService)
{
_installationRepository = installationRepository;
_paymentService = paymentService;
_licensingService = licensingService;
}
public async Task<OrganizationLicense> GetLicenseAsync(Organization organization, Guid installationId,
int? version = null)
{
var installation = await _installationRepository.GetByIdAsync(installationId);
if (installation is not { Enabled: true })
{
throw new BadRequestException("Invalid installation id");
}
var subInfo = await _paymentService.GetSubscriptionAsync(organization);
return new OrganizationLicense(organization, subInfo, installationId, _licensingService, version);
}
}

View File

@@ -0,0 +1,15 @@
using Bit.Core.Entities;
using Bit.Core.Models.Business;
namespace Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
public interface ICloudGetOrganizationLicenseQuery
{
Task<OrganizationLicense> GetLicenseAsync(Organization organization, Guid installationId,
int? version = null);
}
public interface ISelfHostedGetOrganizationLicenseQuery
{
Task<OrganizationLicense> GetLicenseAsync(Organization organization, OrganizationConnection billingSyncConnection);
}

View File

@@ -0,0 +1,66 @@
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Models.Api.OrganizationLicenses;
using Bit.Core.Models.Business;
using Bit.Core.Models.OrganizationConnectionConfigs;
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
using Bit.Core.Services;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging;
namespace Bit.Core.OrganizationFeatures.OrganizationLicenses;
public class SelfHostedGetOrganizationLicenseQuery : BaseIdentityClientService, ISelfHostedGetOrganizationLicenseQuery
{
private readonly IGlobalSettings _globalSettings;
public SelfHostedGetOrganizationLicenseQuery(IHttpClientFactory httpFactory, IGlobalSettings globalSettings, ILogger<SelfHostedGetOrganizationLicenseQuery> logger, ICurrentContext currentContext)
: base(
httpFactory,
globalSettings.Installation.ApiUri,
globalSettings.Installation.IdentityUri,
"api.licensing",
$"installation.{globalSettings.Installation.Id}",
globalSettings.Installation.Key,
logger)
{
_globalSettings = globalSettings;
}
public async Task<OrganizationLicense> GetLicenseAsync(Organization organization, OrganizationConnection billingSyncConnection)
{
if (!_globalSettings.SelfHosted)
{
throw new BadRequestException("This action is only available for self-hosted.");
}
if (!_globalSettings.EnableCloudCommunication)
{
throw new BadRequestException("Cloud communication is disabled in global settings");
}
if (!billingSyncConnection.Validate<BillingSyncConfig>(out var exception))
{
throw new BadRequestException(exception);
}
var billingSyncConfig = billingSyncConnection.GetConfig<BillingSyncConfig>();
var cloudOrganizationId = billingSyncConfig.CloudOrganizationId;
var response = await SendAsync<SelfHostedOrganizationLicenseRequestModel, OrganizationLicense>(
HttpMethod.Get, $"licenses/organization/{cloudOrganizationId}", new SelfHostedOrganizationLicenseRequestModel()
{
BillingSyncKey = billingSyncConfig.BillingSyncKey,
LicenseKey = organization.LicenseKey,
}, true);
if (response == null)
{
_logger.LogDebug("Organization License sync failed for '{OrgId}'", organization.Id);
throw new BadRequestException("An error has occurred. Check your internet connection and ensure the billing token is correct.");
}
return response;
}
}

View File

@@ -7,6 +7,8 @@ using Bit.Core.OrganizationFeatures.OrganizationCollections;
using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationConnections;
using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationLicenses;
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
@@ -32,6 +34,7 @@ public static class OrganizationServiceCollectionExtensions
services.AddOrganizationApiKeyCommandsQueries();
services.AddOrganizationCollectionCommands();
services.AddOrganizationGroupCommands();
services.AddOrganizationLicenseCommandQueries();
}
private static void AddOrganizationConnectionCommands(this IServiceCollection services)
@@ -85,6 +88,12 @@ public static class OrganizationServiceCollectionExtensions
services.AddScoped<IUpdateGroupCommand, UpdateGroupCommand>();
}
private static void AddOrganizationLicenseCommandQueries(this IServiceCollection services)
{
services.AddScoped<ICloudGetOrganizationLicenseQuery, CloudGetOrganizationLicenseQuery>();
services.AddScoped<ISelfHostedGetOrganizationLicenseQuery, SelfHostedGetOrganizationLicenseQuery>();
}
private static void AddTokenizers(this IServiceCollection services)
{
services.AddSingleton<IDataProtectorTokenFactory<OrganizationSponsorshipOfferTokenable>>(serviceProvider =>

View File

@@ -48,20 +48,13 @@ public class SelfHostedSyncSponsorshipsCommand : BaseIdentityClientService, ISel
{
throw new BadRequestException("Failed to sync instance with cloud - Cloud communication is disabled in global settings");
}
if (!billingSyncConnection.Enabled)
if (!billingSyncConnection.Validate<BillingSyncConfig>(out var exception))
{
throw new BadRequestException($"Billing Sync Key disabled for organization {organizationId}");
}
if (string.IsNullOrWhiteSpace(billingSyncConnection.Config))
{
throw new BadRequestException($"No Billing Sync Key known for organization {organizationId}");
}
var billingSyncConfig = billingSyncConnection.GetConfig<BillingSyncConfig>();
if (billingSyncConfig == null || string.IsNullOrWhiteSpace(billingSyncConfig.BillingSyncKey))
{
throw new BadRequestException($"Failed to get Billing Sync Key for organization {organizationId}");
throw new BadRequestException(exception);
}
var billingSyncConfig = billingSyncConnection.GetConfig<BillingSyncConfig>();
var organizationSponsorshipsDict = (await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(organizationId))
.ToDictionary(i => i.SponsoringOrganizationUserId);
if (!organizationSponsorshipsDict.Any())