mirror of
https://github.com/bitwarden/server.git
synced 2024-11-21 12:05:42 +01: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:
parent
d0355fcd12
commit
82908b1fb7
@ -3,6 +3,7 @@ using System.Text.Json;
|
||||
using Bit.Admin.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.BitStripe;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@ -18,7 +19,7 @@ public class ToolsController : Controller
|
||||
{
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
|
||||
private readonly IUserService _userService;
|
||||
private readonly ITransactionRepository _transactionRepository;
|
||||
private readonly IInstallationRepository _installationRepository;
|
||||
@ -30,7 +31,7 @@ public class ToolsController : Controller
|
||||
public ToolsController(
|
||||
GlobalSettings globalSettings,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationService organizationService,
|
||||
ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery,
|
||||
IUserService userService,
|
||||
ITransactionRepository transactionRepository,
|
||||
IInstallationRepository installationRepository,
|
||||
@ -41,7 +42,7 @@ public class ToolsController : Controller
|
||||
{
|
||||
_globalSettings = globalSettings;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationService = organizationService;
|
||||
_cloudGetOrganizationLicenseQuery = cloudGetOrganizationLicenseQuery;
|
||||
_userService = userService;
|
||||
_transactionRepository = transactionRepository;
|
||||
_installationRepository = installationRepository;
|
||||
@ -259,7 +260,7 @@ public class ToolsController : Controller
|
||||
|
||||
if (organization != null)
|
||||
{
|
||||
var license = await _organizationService.GenerateLicenseAsync(organization,
|
||||
var license = await _cloudGetOrganizationLicenseQuery.GetLicenseAsync(organization,
|
||||
model.InstallationId.Value, model.Version);
|
||||
var ms = new MemoryStream();
|
||||
await JsonSerializer.SerializeAsync(ms, license, JsonHelpers.Indented);
|
||||
|
@ -1,6 +1,9 @@
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Api.OrganizationLicenses;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
@ -14,26 +17,26 @@ namespace Bit.Api.Controllers;
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public class LicensesController : Controller
|
||||
{
|
||||
private readonly ILicensingService _licensingService;
|
||||
private readonly IUserRepository _userRepository;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
|
||||
private readonly IValidateBillingSyncKeyCommand _validateBillingSyncKeyCommand;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
|
||||
public LicensesController(
|
||||
ILicensingService licensingService,
|
||||
IUserRepository userRepository,
|
||||
IUserService userService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationService organizationService,
|
||||
ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery,
|
||||
IValidateBillingSyncKeyCommand validateBillingSyncKeyCommand,
|
||||
ICurrentContext currentContext)
|
||||
{
|
||||
_licensingService = licensingService;
|
||||
_userRepository = userRepository;
|
||||
_userService = userService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationService = organizationService;
|
||||
_cloudGetOrganizationLicenseQuery = cloudGetOrganizationLicenseQuery;
|
||||
_validateBillingSyncKeyCommand = validateBillingSyncKeyCommand;
|
||||
_currentContext = currentContext;
|
||||
}
|
||||
|
||||
@ -55,21 +58,30 @@ public class LicensesController : Controller
|
||||
return license;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Used by self-hosted installations to get an updated license file
|
||||
/// </summary>
|
||||
[HttpGet("organization/{id}")]
|
||||
public async Task<OrganizationLicense> GetOrganization(string id, [FromQuery] string key)
|
||||
public async Task<OrganizationLicense> OrganizationSync(string id, [FromBody] SelfHostedOrganizationLicenseRequestModel model)
|
||||
{
|
||||
var org = await _organizationRepository.GetByIdAsync(new Guid(id));
|
||||
if (org == null)
|
||||
var organization = await _organizationRepository.GetByIdAsync(new Guid(id));
|
||||
if (organization == null)
|
||||
{
|
||||
return null;
|
||||
throw new NotFoundException("Organization not found.");
|
||||
}
|
||||
else if (!org.LicenseKey.Equals(key))
|
||||
|
||||
if (!organization.LicenseKey.Equals(model.LicenseKey))
|
||||
{
|
||||
await Task.Delay(2000);
|
||||
throw new BadRequestException("Invalid license key.");
|
||||
}
|
||||
|
||||
var license = await _organizationService.GenerateLicenseAsync(org, _currentContext.InstallationId.Value);
|
||||
if (!await _validateBillingSyncKeyCommand.ValidateBillingSyncKeyAsync(organization, model.BillingSyncKey))
|
||||
{
|
||||
throw new BadRequestException("Invalid Billing Sync Key");
|
||||
}
|
||||
|
||||
var license = await _cloudGetOrganizationLicenseQuery.GetLicenseAsync(organization, _currentContext.InstallationId.Value);
|
||||
return license;
|
||||
}
|
||||
}
|
||||
|
@ -191,7 +191,7 @@ public class OrganizationConnectionsController : Controller
|
||||
Guid? organizationConnectionId,
|
||||
OrganizationConnectionRequestModel model,
|
||||
Func<OrganizationConnectionRequestModel<T>, Task> validateAction = null)
|
||||
where T : new()
|
||||
where T : IConnectionConfig
|
||||
{
|
||||
var typedModel = new OrganizationConnectionRequestModel<T>(model);
|
||||
if (validateAction != null)
|
||||
|
@ -5,6 +5,7 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Api.Request.OrganizationSponsorships;
|
||||
using Bit.Core.Models.Api.Response.OrganizationSponsorships;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
|
@ -11,6 +11,7 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@ -37,6 +38,7 @@ public class OrganizationsController : Controller
|
||||
private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand;
|
||||
private readonly ICreateOrganizationApiKeyCommand _createOrganizationApiKeyCommand;
|
||||
private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository;
|
||||
private readonly ICloudGetOrganizationLicenseQuery _cloudGetOrganizationLicenseQuery;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public OrganizationsController(
|
||||
@ -53,6 +55,7 @@ public class OrganizationsController : Controller
|
||||
IRotateOrganizationApiKeyCommand rotateOrganizationApiKeyCommand,
|
||||
ICreateOrganizationApiKeyCommand createOrganizationApiKeyCommand,
|
||||
IOrganizationApiKeyRepository organizationApiKeyRepository,
|
||||
ICloudGetOrganizationLicenseQuery cloudGetOrganizationLicenseQuery,
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
_organizationRepository = organizationRepository;
|
||||
@ -68,6 +71,7 @@ public class OrganizationsController : Controller
|
||||
_rotateOrganizationApiKeyCommand = rotateOrganizationApiKeyCommand;
|
||||
_createOrganizationApiKeyCommand = createOrganizationApiKeyCommand;
|
||||
_organizationApiKeyRepository = organizationApiKeyRepository;
|
||||
_cloudGetOrganizationLicenseQuery = cloudGetOrganizationLicenseQuery;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
@ -149,7 +153,8 @@ public class OrganizationsController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var license = await _organizationService.GenerateLicenseAsync(orgIdGuid, installationId);
|
||||
var org = await _organizationRepository.GetByIdAsync(new Guid(id));
|
||||
var license = await _cloudGetOrganizationLicenseQuery.GetLicenseAsync(org, installationId);
|
||||
if (license == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
@ -215,6 +220,7 @@ public class OrganizationsController : Controller
|
||||
return new OrganizationResponseModel(result.Item1);
|
||||
}
|
||||
|
||||
[Obsolete("2022-12-7 Moved to SelfHostedOrganizationLicensesController, to be removed in EC-815")]
|
||||
[HttpPost("license")]
|
||||
[SelfHosted(SelfHostedOnly = true)]
|
||||
public async Task<OrganizationResponseModel> PostLicense(OrganizationCreateLicenseRequestModel model)
|
||||
@ -448,6 +454,7 @@ public class OrganizationsController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete("2022-12-7 Moved to SelfHostedOrganizationLicensesController, to be removed in EC-815")]
|
||||
[HttpPost("{id}/license")]
|
||||
[SelfHosted(SelfHostedOnly = true)]
|
||||
public async Task PostLicense(string id, LicenseRequestModel model)
|
||||
|
@ -0,0 +1,117 @@
|
||||
using Bit.Api.Models.Request;
|
||||
using Bit.Api.Models.Request.Organizations;
|
||||
using Bit.Api.Models.Response.Organizations;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.OrganizationConnectionConfigs;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Controllers.SelfHosted;
|
||||
|
||||
[Route("organizations/licenses/self-hosted")]
|
||||
[Authorize("Application")]
|
||||
[SelfHosted(SelfHostedOnly = true)]
|
||||
public class SelfHostedOrganizationLicensesController : Controller
|
||||
{
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly ISelfHostedGetOrganizationLicenseQuery _selfHostedGetOrganizationLicenseQuery;
|
||||
private readonly IOrganizationConnectionRepository _organizationConnectionRepository;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IUserService _userService;
|
||||
|
||||
public SelfHostedOrganizationLicensesController(
|
||||
ICurrentContext currentContext,
|
||||
ISelfHostedGetOrganizationLicenseQuery selfHostedGetOrganizationLicenseQuery,
|
||||
IOrganizationConnectionRepository organizationConnectionRepository,
|
||||
IOrganizationService organizationService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IUserService userService)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_selfHostedGetOrganizationLicenseQuery = selfHostedGetOrganizationLicenseQuery;
|
||||
_organizationConnectionRepository = organizationConnectionRepository;
|
||||
_organizationService = organizationService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_userService = userService;
|
||||
}
|
||||
|
||||
[HttpPost("")]
|
||||
public async Task<OrganizationResponseModel> PostLicenseAsync(OrganizationCreateLicenseRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var license = await ApiHelpers.ReadJsonFileFromBody<OrganizationLicense>(HttpContext, model.License);
|
||||
if (license == null)
|
||||
{
|
||||
throw new BadRequestException("Invalid license");
|
||||
}
|
||||
|
||||
var result = await _organizationService.SignUpAsync(license, user, model.Key,
|
||||
model.CollectionName, model.Keys?.PublicKey, model.Keys?.EncryptedPrivateKey);
|
||||
return new OrganizationResponseModel(result.Item1);
|
||||
}
|
||||
|
||||
[HttpPost("{id}")]
|
||||
public async Task PostLicenseAsync(string id, LicenseRequestModel model)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.OrganizationOwner(orgIdGuid))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var license = await ApiHelpers.ReadJsonFileFromBody<OrganizationLicense>(HttpContext, model.License);
|
||||
if (license == null)
|
||||
{
|
||||
throw new BadRequestException("Invalid license");
|
||||
}
|
||||
|
||||
await _organizationService.UpdateLicenseAsync(new Guid(id), license);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/sync")]
|
||||
public async Task SyncLicenseAsync(string id)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(new Guid(id));
|
||||
if (organization == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!await _currentContext.OrganizationOwner(organization.Id))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var billingSyncConnection =
|
||||
(await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organization.Id,
|
||||
OrganizationConnectionType.CloudBillingSync)).FirstOrDefault();
|
||||
if (billingSyncConnection == null)
|
||||
{
|
||||
throw new NotFoundException("Unable to get Cloud Billing Sync connection");
|
||||
}
|
||||
|
||||
var license =
|
||||
await _selfHostedGetOrganizationLicenseQuery.GetLicenseAsync(organization, billingSyncConnection);
|
||||
|
||||
await _organizationService.UpdateLicenseAsync(organization.Id, license);
|
||||
|
||||
var config = billingSyncConnection.GetConfig<BillingSyncConfig>();
|
||||
config.LastLicenseSync = DateTime.Now;
|
||||
billingSyncConnection.SetConfig(config);
|
||||
await _organizationConnectionRepository.ReplaceAsync(billingSyncConnection);
|
||||
}
|
||||
}
|
@ -2,6 +2,7 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationConnections;
|
||||
using Bit.Core.Models.OrganizationConnectionConfigs;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.Models.Request.Organizations;
|
||||
@ -17,7 +18,7 @@ public class OrganizationConnectionRequestModel
|
||||
}
|
||||
|
||||
|
||||
public class OrganizationConnectionRequestModel<T> : OrganizationConnectionRequestModel where T : new()
|
||||
public class OrganizationConnectionRequestModel<T> : OrganizationConnectionRequestModel where T : IConnectionConfig
|
||||
{
|
||||
public T ParsedConfig { get; private set; }
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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()
|
||||
|
50
test/Common/AutoFixture/SutProviderExtensions.cs
Normal file
50
test/Common/AutoFixture/SutProviderExtensions.cs
Normal 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();
|
||||
}
|
||||
}
|
18
test/Core.Test/AutoFixture/SubscriptionInfoCustomization.cs
Normal file
18
test/Core.Test/AutoFixture/SubscriptionInfoCustomization.cs
Normal 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));
|
||||
}
|
||||
}
|
78
test/Core.Test/Entities/OrganizationConnectionTests.cs
Normal file
78
test/Core.Test/Entities/OrganizationConnectionTests.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user