1
0
mirror of https://github.com/bitwarden/server.git synced 2025-06-30 07:36:14 -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:
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

@ -366,7 +366,7 @@ public class OrganizationConnectionsControllerTests
}
private static OrganizationConnectionRequestModel<T> RequestModelFromEntity<T>(OrganizationConnection entity)
where T : new()
where T : IConnectionConfig
{
return new(new OrganizationConnectionRequestModel()
{

View File

@ -6,6 +6,7 @@ using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
@ -29,6 +30,7 @@ public class OrganizationsControllerTests : IDisposable
private readonly IGetOrganizationApiKeyQuery _getOrganizationApiKeyQuery;
private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand;
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;
private readonly OrganizationsController _sut;
@ -48,12 +50,13 @@ public class OrganizationsControllerTests : IDisposable
_rotateOrganizationApiKeyCommand = Substitute.For<IRotateOrganizationApiKeyCommand>();
_organizationApiKeyRepository = Substitute.For<IOrganizationApiKeyRepository>();
_userService = Substitute.For<IUserService>();
_cloudGetOrganizationLicenseQuery = Substitute.For<ICloudGetOrganizationLicenseQuery>();
_createOrganizationApiKeyCommand = Substitute.For<ICreateOrganizationApiKeyCommand>();
_sut = new OrganizationsController(_organizationRepository, _organizationUserRepository,
_policyRepository, _organizationService, _userService, _paymentService, _currentContext,
_ssoConfigRepository, _ssoConfigService, _getOrganizationApiKeyQuery, _rotateOrganizationApiKeyCommand,
_createOrganizationApiKeyCommand, _organizationApiKeyRepository, _globalSettings);
_createOrganizationApiKeyCommand, _organizationApiKeyRepository, _cloudGetOrganizationLicenseQuery, _globalSettings);
}
public void Dispose()

View File

@ -0,0 +1,50 @@
using AutoFixture;
using Bit.Core.Services;
using Bit.Core.Settings;
using NSubstitute;
using RichardSzalay.MockHttp;
namespace Bit.Test.Common.AutoFixture;
public static class SutProviderExtensions
{
public static SutProvider<T> ConfigureBaseIdentityClientService<T>(this SutProvider<T> sutProvider,
string requestUrlFragment, HttpMethod requestHttpMethod, string identityResponse = null, string apiResponse = null)
where T : BaseIdentityClientService
{
var fixture = new Fixture().WithAutoNSubstitutionsAutoPopulatedProperties();
fixture.AddMockHttp();
var settings = fixture.Create<IGlobalSettings>();
settings.SelfHosted = true;
settings.EnableCloudCommunication = true;
var apiUri = fixture.Create<Uri>();
var identityUri = fixture.Create<Uri>();
settings.Installation.ApiUri.Returns(apiUri.ToString());
settings.Installation.IdentityUri.Returns(identityUri.ToString());
var apiHandler = new MockHttpMessageHandler();
var identityHandler = new MockHttpMessageHandler();
var syncUri = string.Concat(apiUri, requestUrlFragment);
var tokenUri = string.Concat(identityUri, "connect/token");
apiHandler.When(requestHttpMethod, syncUri)
.Respond("application/json", apiResponse);
identityHandler.When(HttpMethod.Post, tokenUri)
.Respond("application/json", identityResponse ?? "{\"access_token\":\"string\",\"expires_in\":3600,\"token_type\":\"Bearer\",\"scope\":\"string\"}");
var apiHttp = apiHandler.ToHttpClient();
var identityHttp = identityHandler.ToHttpClient();
var mockHttpClientFactory = Substitute.For<IHttpClientFactory>();
mockHttpClientFactory.CreateClient(Arg.Is("client")).Returns(apiHttp);
mockHttpClientFactory.CreateClient(Arg.Is("identity")).Returns(identityHttp);
return sutProvider
.SetDependency(settings)
.SetDependency(mockHttpClientFactory)
.Create();
}
}

View File

@ -0,0 +1,18 @@
using AutoFixture;
using Bit.Core.Models.Business;
using Bit.Test.Common.AutoFixture.Attributes;
namespace Bit.Core.Test.AutoFixture;
public class SubscriptionInfoCustomizeAttribute : BitCustomizeAttribute
{
public override ICustomization GetCustomization() => new SubscriptionInfoCustomization();
}
public class SubscriptionInfoCustomization : ICustomization
{
public void Customize(IFixture fixture)
{
// The Subscription property uses the external Stripe library, which Autofixture doesn't handle
fixture.Customize<SubscriptionInfo>(c => c.Without(s => s.Subscription));
}
}

View File

@ -0,0 +1,78 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Models.OrganizationConnectionConfigs;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Core.Test.Entities;
public class OrganizationConnectionTests
{
[Theory]
[BitAutoData]
public void OrganizationConnection_CanUse_Success(Guid connectionId, Guid organizationId)
{
var connection = new OrganizationConnection<ScimConfig>()
{
Id = connectionId,
OrganizationId = organizationId,
Enabled = true,
Type = OrganizationConnectionType.Scim,
Config = new ScimConfig() { Enabled = true }
};
Assert.True(connection.Validate<ScimConfig>(out var exception));
Assert.True(string.IsNullOrEmpty(exception));
}
[Theory]
[BitAutoData]
public void OrganizationConnection_CanUse_WhenDisabled_ReturnsFalse(Guid connectionId, Guid organizationId)
{
var connection = new OrganizationConnection<ScimConfig>()
{
Id = connectionId,
OrganizationId = organizationId,
Enabled = false,
Type = OrganizationConnectionType.Scim,
Config = new ScimConfig() { Enabled = true }
};
Assert.False(connection.Validate<ScimConfig>(out var exception));
Assert.Contains("Connection disabled", exception);
}
[Theory]
[BitAutoData]
public void OrganizationConnection_CanUse_WhenNoConfig_ReturnsFalse(Guid connectionId, Guid organizationId)
{
var connection = new OrganizationConnection<ScimConfig>()
{
Id = connectionId,
OrganizationId = organizationId,
Enabled = true,
Type = OrganizationConnectionType.Scim,
};
Assert.False(connection.Validate<ScimConfig>(out var exception));
Assert.Contains("No saved Connection config", exception);
}
[Theory]
[BitAutoData]
public void OrganizationConnection_CanUse_WhenConfigInvalid_ReturnsFalse(Guid connectionId, Guid organizationId)
{
var connection = new OrganizationConnection<ScimConfig>()
{
Id = connectionId,
OrganizationId = organizationId,
Enabled = true,
Type = OrganizationConnectionType.Scim,
Config = new ScimConfig() { Enabled = false }
};
Assert.False(connection.Validate<ScimConfig>(out var exception));
Assert.Contains("Scim Config is disabled", exception);
}
}

View File

@ -0,0 +1,27 @@
using Bit.Core.Models.OrganizationConnectionConfigs;
using Bit.Test.Common.AutoFixture.Attributes;
using Xunit;
namespace Bit.Core.Test.Models.OrganizationConnectionConfigs;
public class BillingSyncConfigTests
{
[Theory]
[BitAutoData]
public void BillingSyncConfig_CanUse_Success(string billingSyncKey)
{
var config = new BillingSyncConfig() { BillingSyncKey = billingSyncKey };
Assert.True(config.Validate(out var exception));
Assert.True(string.IsNullOrEmpty(exception));
}
[Fact]
public void BillingSyncConfig_CanUse_WhenNoKey_ReturnsFalse()
{
var config = new BillingSyncConfig();
Assert.False(config.Validate(out var exception));
Assert.Contains("Failed to get Billing Sync Key", exception);
}
}

View File

@ -0,0 +1,23 @@
using Bit.Core.Models.OrganizationConnectionConfigs;
using Xunit;
namespace Bit.Core.Test.Models.OrganizationConnectionConfigs;
public class ScimConfigTests
{
[Fact]
public void ScimConfig_CanUse_Success()
{
var config = new ScimConfig() { Enabled = true };
Assert.True(config.Validate(out var exception));
Assert.True(string.IsNullOrEmpty(exception));
}
[Fact]
public void ScimConfig_CanUse_WhenDisabled_ReturnsFalse()
{
var config = new ScimConfig() { Enabled = false };
Assert.False(config.Validate(out var exception));
Assert.Contains("Config is disabled", exception);
}
}

View File

@ -1,14 +1,14 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;
using Bit.Core.OrganizationFeatures.OrganizationConnections;
using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud;
namespace Bit.Core.Test.OrganizationFeatures.OrganizationConnections;
[SutProviderCustomize]
public class ValidateBillingSyncKeyCommandTests

View File

@ -0,0 +1,64 @@
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationLicenses;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using NSubstitute.ReturnsExtensions;
using Xunit;
namespace Bit.Core.Test.OrganizationFeatures.OrganizationLicenses;
[SubscriptionInfoCustomize]
[OrganizationLicenseCustomize]
[SutProviderCustomize]
public class CloudGetOrganizationLicenseQueryTests
{
[Theory]
[BitAutoData]
public async Task GetLicenseAsync_InvalidInstallationId_Throws(SutProvider<CloudGetOrganizationLicenseQuery> sutProvider,
Organization organization, Guid installationId, int version)
{
sutProvider.GetDependency<IInstallationRepository>().GetByIdAsync(installationId).ReturnsNull();
var exception = await Assert.ThrowsAsync<BadRequestException>(
async () => await sutProvider.Sut.GetLicenseAsync(organization, installationId, version));
Assert.Contains("Invalid installation id", exception.Message);
}
[Theory]
[BitAutoData]
public async Task GetLicenseAsync_DisabledOrganization_Throws(SutProvider<CloudGetOrganizationLicenseQuery> sutProvider,
Organization organization, Guid installationId, Installation installation)
{
installation.Enabled = false;
sutProvider.GetDependency<IInstallationRepository>().GetByIdAsync(installationId).Returns(installation);
var exception = await Assert.ThrowsAsync<BadRequestException>(
async () => await sutProvider.Sut.GetLicenseAsync(organization, installationId));
Assert.Contains("Invalid installation id", exception.Message);
}
[Theory]
[BitAutoData]
public async Task GetLicenseAsync_CreatesAndReturns(SutProvider<CloudGetOrganizationLicenseQuery> sutProvider,
Organization organization, Guid installationId, Installation installation, SubscriptionInfo subInfo,
byte[] licenseSignature)
{
installation.Enabled = true;
sutProvider.GetDependency<IInstallationRepository>().GetByIdAsync(installationId).Returns(installation);
sutProvider.GetDependency<IPaymentService>().GetSubscriptionAsync(organization).Returns(subInfo);
sutProvider.GetDependency<ILicensingService>().SignLicense(Arg.Any<ILicense>()).Returns(licenseSignature);
var result = await sutProvider.Sut.GetLicenseAsync(organization, installationId);
Assert.Equal(LicenseType.Organization, result.LicenseType);
Assert.Equal(organization.Id, result.Id);
Assert.Equal(installationId, result.InstallationId);
Assert.Equal(licenseSignature, result.SignatureBytes);
}
}

View File

@ -0,0 +1,94 @@
using System.Text.Json;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.OrganizationConnectionConfigs;
using Bit.Core.OrganizationFeatures.OrganizationLicenses;
using Bit.Core.Settings;
using Bit.Core.Test.AutoFixture;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using Xunit;
namespace Bit.Core.Test.OrganizationFeatures.OrganizationLicenses;
[SutProviderCustomize]
public class SelfHostedGetOrganizationLicenseQueryTests
{
private static SutProvider<SelfHostedGetOrganizationLicenseQuery> GetSutProvider(BillingSyncConfig config,
string apiResponse = null)
{
return new SutProvider<SelfHostedGetOrganizationLicenseQuery>()
.ConfigureBaseIdentityClientService($"licenses/organization/{config.CloudOrganizationId}",
HttpMethod.Get, apiResponse: apiResponse);
}
[Theory]
[BitAutoData]
[OrganizationLicenseCustomize]
public async void GetLicenseAsync_Success(Organization organization,
OrganizationConnection<BillingSyncConfig> billingSyncConnection, BillingSyncConfig config, OrganizationLicense license)
{
var sutProvider = GetSutProvider(config, JsonSerializer.Serialize(license));
billingSyncConnection.Enabled = true;
billingSyncConnection.Config = config;
var result = await sutProvider.Sut.GetLicenseAsync(organization, billingSyncConnection);
AssertHelper.AssertPropertyEqual(result, license);
}
[Theory]
[BitAutoData]
public async void GetLicenseAsync_WhenNotSelfHosted_Throws(Organization organization,
OrganizationConnection billingSyncConnection, BillingSyncConfig config)
{
var sutProvider = GetSutProvider(config);
sutProvider.GetDependency<IGlobalSettings>().SelfHosted = false;
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.GetLicenseAsync(organization, billingSyncConnection));
Assert.Contains("only available for self-hosted", exception.Message);
}
[Theory]
[BitAutoData]
public async void GetLicenseAsync_WhenCloudCommunicationDisabled_Throws(Organization organization,
OrganizationConnection billingSyncConnection, BillingSyncConfig config)
{
var sutProvider = GetSutProvider(config);
sutProvider.GetDependency<IGlobalSettings>().EnableCloudCommunication = false;
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.GetLicenseAsync(organization, billingSyncConnection));
Assert.Contains("Cloud communication is disabled", exception.Message);
}
[Theory]
[BitAutoData]
public async void GetLicenseAsync_WhenCantUseConnection_Throws(Organization organization,
OrganizationConnection<BillingSyncConfig> billingSyncConnection, BillingSyncConfig config)
{
var sutProvider = GetSutProvider(config);
billingSyncConnection.Enabled = false;
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.GetLicenseAsync(organization, billingSyncConnection));
Assert.Contains("Connection disabled", exception.Message);
}
[Theory]
[BitAutoData]
public async void GetLicenseAsync_WhenNullResponse_Throws(Organization organization,
OrganizationConnection<BillingSyncConfig> billingSyncConnection, BillingSyncConfig config)
{
var sutProvider = GetSutProvider(config);
billingSyncConnection.Enabled = true;
billingSyncConnection.Config = config;
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.GetLicenseAsync(organization, billingSyncConnection));
Assert.Contains("An error has occurred. Check your internet connection and ensure the billing token is correct.",
exception.Message);
}
}

View File

@ -1,5 +1,4 @@
using System.Text.Json;
using AutoFixture;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Models.Api.Response.OrganizationSponsorships;
@ -12,55 +11,22 @@ using Bit.Core.Test.AutoFixture.OrganizationSponsorshipFixtures;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using RichardSzalay.MockHttp;
using Xunit;
namespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted;
public class SelfHostedSyncSponsorshipsCommandTests : FamiliesForEnterpriseTestsBase
{
public static SutProvider<SelfHostedSyncSponsorshipsCommand> GetSutProvider(bool enableCloudCommunication = true, string identityResponse = null, string apiResponse = null)
private static SutProvider<SelfHostedSyncSponsorshipsCommand> GetSutProvider(string apiResponse = null)
{
var fixture = new Fixture().WithAutoNSubstitutionsAutoPopulatedProperties();
fixture.AddMockHttp();
var settings = fixture.Create<IGlobalSettings>();
settings.SelfHosted = true;
settings.EnableCloudCommunication = enableCloudCommunication;
var apiUri = fixture.Create<Uri>();
var identityUri = fixture.Create<Uri>();
settings.Installation.ApiUri.Returns(apiUri.ToString());
settings.Installation.IdentityUri.Returns(identityUri.ToString());
var apiHandler = new MockHttpMessageHandler();
var identityHandler = new MockHttpMessageHandler();
var syncUri = string.Concat(apiUri, "organization/sponsorship/sync");
var tokenUri = string.Concat(identityUri, "connect/token");
apiHandler.When(HttpMethod.Post, syncUri)
.Respond("application/json", apiResponse);
identityHandler.When(HttpMethod.Post, tokenUri)
.Respond("application/json", identityResponse ?? "{\"access_token\":\"string\",\"expires_in\":3600,\"token_type\":\"Bearer\",\"scope\":\"string\"}");
var apiHttp = apiHandler.ToHttpClient();
var identityHttp = identityHandler.ToHttpClient();
var mockHttpClientFactory = Substitute.For<IHttpClientFactory>();
mockHttpClientFactory.CreateClient(Arg.Is("client")).Returns(apiHttp);
mockHttpClientFactory.CreateClient(Arg.Is("identity")).Returns(identityHttp);
return new SutProvider<SelfHostedSyncSponsorshipsCommand>(fixture)
.SetDependency(settings)
.SetDependency(mockHttpClientFactory)
.Create();
return new SutProvider<SelfHostedSyncSponsorshipsCommand>()
.ConfigureBaseIdentityClientService("organization/sponsorship/sync",
HttpMethod.Post, apiResponse: apiResponse);
}
[Theory]
[BitAutoData]
public async Task SyncOrganization_BillingSyncKeyDisabled_ThrowsBadRequest(
public async Task SyncOrganization_BillingSyncConnectionDisabled_ThrowsBadRequest(
Guid cloudOrganizationId, OrganizationConnection billingSyncConnection)
{
var sutProvider = GetSutProvider();
@ -73,7 +39,7 @@ public class SelfHostedSyncSponsorshipsCommandTests : FamiliesForEnterpriseTests
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.SyncOrganization(billingSyncConnection.OrganizationId, cloudOrganizationId, billingSyncConnection));
Assert.Contains($"Billing Sync Key disabled", exception.Message);
Assert.Contains($"Connection disabled", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()
.DidNotReceiveWithAnyArgs()
@ -85,7 +51,7 @@ public class SelfHostedSyncSponsorshipsCommandTests : FamiliesForEnterpriseTests
[Theory]
[BitAutoData]
public async Task SyncOrganization_BillingSyncKeyEmpty_ThrowsBadRequest(
public async Task SyncOrganization_BillingSyncConfigEmpty_ThrowsBadRequest(
Guid cloudOrganizationId, OrganizationConnection billingSyncConnection)
{
var sutProvider = GetSutProvider();
@ -94,7 +60,7 @@ public class SelfHostedSyncSponsorshipsCommandTests : FamiliesForEnterpriseTests
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.SyncOrganization(billingSyncConnection.OrganizationId, cloudOrganizationId, billingSyncConnection));
Assert.Contains($"No Billing Sync Key known", exception.Message);
Assert.Contains($"No saved Connection config", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>()
.DidNotReceiveWithAnyArgs()
@ -109,7 +75,8 @@ public class SelfHostedSyncSponsorshipsCommandTests : FamiliesForEnterpriseTests
public async Task SyncOrganization_CloudCommunicationDisabled_EarlyReturn(
Guid cloudOrganizationId, OrganizationConnection billingSyncConnection)
{
var sutProvider = GetSutProvider(false);
var sutProvider = GetSutProvider();
sutProvider.GetDependency<IGlobalSettings>().EnableCloudCommunication = false;
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.SyncOrganization(billingSyncConnection.OrganizationId, cloudOrganizationId, billingSyncConnection));
@ -136,7 +103,8 @@ public class SelfHostedSyncSponsorshipsCommandTests : FamiliesForEnterpriseTests
SponsorshipsBatch = sponsorships.Select(o => new OrganizationSponsorshipData(o))
}));
var sutProvider = GetSutProvider(apiResponse: syncJsonResponse);
var sutProvider = GetSutProvider(syncJsonResponse);
billingSyncConnection.SetConfig(new BillingSyncConfig
{
BillingSyncKey = "okslkcslkjf"
@ -166,7 +134,7 @@ public class SelfHostedSyncSponsorshipsCommandTests : FamiliesForEnterpriseTests
SponsorshipsBatch = sponsorships.Select(o => new OrganizationSponsorshipData(o) { CloudSponsorshipRemoved = true })
}));
var sutProvider = GetSutProvider(apiResponse: syncJsonResponse);
var sutProvider = GetSutProvider(syncJsonResponse);
billingSyncConnection.SetConfig(new BillingSyncConfig
{
BillingSyncKey = "okslkcslkjf"