mirror of
https://github.com/bitwarden/server.git
synced 2025-06-30 15:42:48 -05: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:
@ -1,15 +1,16 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.OrganizationConnectionConfigs;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Entities;
|
||||
|
||||
public class OrganizationConnection<T> : OrganizationConnection where T : new()
|
||||
public class OrganizationConnection<T> : OrganizationConnection where T : IConnectionConfig
|
||||
{
|
||||
public new T Config
|
||||
{
|
||||
get => base.GetConfig<T>();
|
||||
set => base.SetConfig<T>(value);
|
||||
set => base.SetConfig(value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,7 +27,7 @@ public class OrganizationConnection : ITableObject<Guid>
|
||||
Id = CoreHelpers.GenerateComb();
|
||||
}
|
||||
|
||||
public T GetConfig<T>() where T : new()
|
||||
public T GetConfig<T>() where T : IConnectionConfig
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -38,8 +39,32 @@ public class OrganizationConnection : ITableObject<Guid>
|
||||
}
|
||||
}
|
||||
|
||||
public void SetConfig<T>(T config) where T : new()
|
||||
public void SetConfig<T>(T config) where T : IConnectionConfig
|
||||
{
|
||||
Config = JsonSerializer.Serialize(config);
|
||||
}
|
||||
|
||||
public bool Validate<T>(out string exception) where T : IConnectionConfig
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
exception = $"Connection disabled for organization {OrganizationId}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Config))
|
||||
{
|
||||
exception = $"No saved Connection config for organization {OrganizationId}";
|
||||
return false;
|
||||
}
|
||||
|
||||
var config = GetConfig<T>();
|
||||
if (config == null)
|
||||
{
|
||||
exception = $"Error parsing Connection config for organization {OrganizationId}";
|
||||
return false;
|
||||
}
|
||||
|
||||
return config.Validate(out exception);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.Models.Api.OrganizationLicenses;
|
||||
|
||||
public class SelfHostedOrganizationLicenseRequestModel
|
||||
{
|
||||
public string LicenseKey { get; set; }
|
||||
public string BillingSyncKey { get; set; }
|
||||
}
|
@ -1,9 +1,10 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.OrganizationConnectionConfigs;
|
||||
|
||||
namespace Bit.Core.Models.Data.Organizations.OrganizationConnections;
|
||||
|
||||
public class OrganizationConnectionData<T> where T : new()
|
||||
public class OrganizationConnectionData<T> where T : IConnectionConfig
|
||||
{
|
||||
public Guid? Id { get; set; }
|
||||
public OrganizationConnectionType Type { get; set; }
|
||||
|
@ -1,7 +1,20 @@
|
||||
namespace Bit.Core.Models.OrganizationConnectionConfigs;
|
||||
|
||||
public class BillingSyncConfig
|
||||
public class BillingSyncConfig : IConnectionConfig
|
||||
{
|
||||
public string BillingSyncKey { get; set; }
|
||||
public Guid CloudOrganizationId { get; set; }
|
||||
public DateTime? LastLicenseSync { get; set; }
|
||||
|
||||
public bool Validate(out string exception)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(BillingSyncKey))
|
||||
{
|
||||
exception = "Failed to get Billing Sync Key";
|
||||
return false;
|
||||
}
|
||||
|
||||
exception = "";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,6 @@
|
||||
namespace Bit.Core.Models.OrganizationConnectionConfigs;
|
||||
|
||||
public interface IConnectionConfig
|
||||
{
|
||||
bool Validate(out string exception);
|
||||
}
|
@ -3,9 +3,21 @@ using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models.OrganizationConnectionConfigs;
|
||||
|
||||
public class ScimConfig
|
||||
public class ScimConfig : IConnectionConfig
|
||||
{
|
||||
public bool Enabled { get; set; }
|
||||
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||
public ScimProviderType? ScimProvider { get; set; }
|
||||
|
||||
public bool Validate(out string exception)
|
||||
{
|
||||
if (!Enabled)
|
||||
{
|
||||
exception = "Scim Config is disabled";
|
||||
return false;
|
||||
}
|
||||
|
||||
exception = "";
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
namespace Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
|
||||
|
||||
public interface IValidateBillingSyncKeyCommand
|
||||
{
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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 =>
|
||||
|
@ -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())
|
||||
|
@ -56,9 +56,6 @@ public interface IOrganizationService
|
||||
IEnumerable<Guid> organizationUserIds, Guid? deletingUserId);
|
||||
Task UpdateUserGroupsAsync(OrganizationUser organizationUser, IEnumerable<Guid> groupIds, Guid? loggedInUserId);
|
||||
Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId);
|
||||
Task<OrganizationLicense> GenerateLicenseAsync(Guid organizationId, Guid installationId);
|
||||
Task<OrganizationLicense> GenerateLicenseAsync(Organization organization, Guid installationId,
|
||||
int? version = null);
|
||||
Task ImportAsync(Guid organizationId, Guid? importingUserId, IEnumerable<ImportedGroup> groups,
|
||||
IEnumerable<ImportedOrganizationUser> newUsers, IEnumerable<string> removeUserExternalIds,
|
||||
bool overwriteExisting);
|
||||
|
@ -1926,30 +1926,6 @@ public class OrganizationService : IOrganizationService
|
||||
EventType.OrganizationUser_ResetPassword_Enroll : EventType.OrganizationUser_ResetPassword_Withdraw);
|
||||
}
|
||||
|
||||
public async Task<OrganizationLicense> GenerateLicenseAsync(Guid organizationId, Guid installationId)
|
||||
{
|
||||
var organization = await GetOrgById(organizationId);
|
||||
return await GenerateLicenseAsync(organization, installationId);
|
||||
}
|
||||
|
||||
public async Task<OrganizationLicense> GenerateLicenseAsync(Organization organization, Guid installationId,
|
||||
int? version = null)
|
||||
{
|
||||
if (organization == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var installation = await _installationRepository.GetByIdAsync(installationId);
|
||||
if (installation == null || !installation.Enabled)
|
||||
{
|
||||
throw new BadRequestException("Invalid installation id");
|
||||
}
|
||||
|
||||
var subInfo = await _paymentService.GetSubscriptionAsync(organization);
|
||||
return new OrganizationLicense(organization, subInfo, installationId, _licensingService, version);
|
||||
}
|
||||
|
||||
public async Task<OrganizationUser> InviteUserAsync(Guid organizationId, Guid? invitingUserId, string email,
|
||||
OrganizationUserType type, bool accessAll, string externalId, IEnumerable<CollectionAccessSelection> collections,
|
||||
IEnumerable<Guid> groups)
|
||||
|
Reference in New Issue
Block a user