diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index b56ca6e21..ad4961278 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -27,4 +27,4 @@ ] } } -} \ No newline at end of file +} diff --git a/.vscode/launch.json b/.vscode/launch.json index 2fb0e97e6..9fb3456fc 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -40,6 +40,7 @@ "Identity", "Sso", "Icons", + "Billing" ], "presentation": { "hidden": false, @@ -125,6 +126,28 @@ "/Views": "${workspaceFolder}/Views" } }, + { + "name": "Billing", + "presentation": { + "hidden": false, + "group": "cloud", + "order": 10 + }, + "requireExactSource": true, + "type": "coreclr", + "request": "launch", + "preLaunchTask": "buildBilling", + "program": "${workspaceFolder}/src/Billing/bin/Debug/net5.0/Billing.dll", + "args": [], + "cwd": "${workspaceFolder}/src/Billing", + "stopAtEntry": false, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, { "name": "Admin", "presentation": { diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 983789385..02e7ddcac 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -89,6 +89,22 @@ "isDefault": true } }, + { + "label": "buildBilling", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/src/Billing/Billing.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile", + "group": { + "kind": "build", + "isDefault": true + } + }, { "label": "clean", "type": "shell", diff --git a/bitwarden_license/src/Sso/Startup.cs b/bitwarden_license/src/Sso/Startup.cs index 0156ed6d1..0f349846b 100644 --- a/bitwarden_license/src/Sso/Startup.cs +++ b/bitwarden_license/src/Sso/Startup.cs @@ -78,7 +78,7 @@ namespace Bit.Sso services.AddCustomIdentityServices(globalSettings); // Services - services.AddBaseServices(); + services.AddBaseServices(globalSettings); services.AddDefaultServices(globalSettings); services.AddCoreLocalizationServices(); } diff --git a/bitwarden_license/test/CmmCore.Test/packages.lock.json b/bitwarden_license/test/CmmCore.Test/packages.lock.json index c2a98d522..3a013a4bf 100644 --- a/bitwarden_license/test/CmmCore.Test/packages.lock.json +++ b/bitwarden_license/test/CmmCore.Test/packages.lock.json @@ -298,6 +298,15 @@ "IdentityModel": "4.3.0" } }, + "Kralizek.AutoFixture.Extensions.MockHttp": { + "type": "Transitive", + "resolved": "1.2.0", + "contentHash": "6zmks7/5mVczazv910N7V2EdiU6B+rY61lwdgVO0o2iZuTI6KI3T+Hgkrjv0eGOKYucq2OMC+gnAc5Ej2ajoTQ==", + "dependencies": { + "AutoFixture": "4.11.0", + "RichardSzalay.MockHttp": "6.0.0" + } + }, "libsodium": { "type": "Transitive", "resolved": "1.0.18", @@ -1598,6 +1607,11 @@ "System.Diagnostics.DiagnosticSource": "4.7.1" } }, + "RichardSzalay.MockHttp": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "bStGNqIX/MGYtML7K3EzdsE/k5HGVAcg7XgN23TQXGXqxNC9fvYFR94fA0sGM5hAT36R+BBGet6ZDQxXL/IPxg==" + }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", "resolved": "4.3.2", @@ -3547,6 +3561,7 @@ "AutoFixture.AutoNSubstitute": "4.14.0", "AutoFixture.Xunit2": "4.14.0", "Core": "1.47.1", + "Kralizek.AutoFixture.Extensions.MockHttp": "1.2.0", "Microsoft.NET.Test.Sdk": "16.6.1", "NSubstitute": "4.2.2", "xunit": "2.4.1" @@ -3599,6 +3614,7 @@ "AutoFixture.Xunit2": "4.14.0", "Common": "1.47.1", "Core": "1.47.1", + "Kralizek.AutoFixture.Extensions.MockHttp": "1.2.0", "Microsoft.NET.Test.Sdk": "16.6.1", "Moq": "4.16.1", "NSubstitute": "4.2.2", diff --git a/src/Admin/Controllers/OrganizationsController.cs b/src/Admin/Controllers/OrganizationsController.cs index 9ea4cff20..a568f03d2 100644 --- a/src/Admin/Controllers/OrganizationsController.cs +++ b/src/Admin/Controllers/OrganizationsController.cs @@ -1,10 +1,13 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Bit.Admin.Models; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; +using Bit.Core.Models.OrganizationConnectionConfigs; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -19,11 +22,14 @@ namespace Bit.Admin.Controllers { private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IOrganizationConnectionRepository _organizationConnectionRepository; + private readonly ISelfHostedSyncSponsorshipsCommand _syncSponsorshipsCommand; private readonly ICipherRepository _cipherRepository; private readonly ICollectionRepository _collectionRepository; private readonly IGroupRepository _groupRepository; private readonly IPolicyRepository _policyRepository; private readonly IPaymentService _paymentService; + private readonly ILicensingService _licensingService; private readonly IApplicationCacheService _applicationCacheService; private readonly GlobalSettings _globalSettings; private readonly IReferenceEventService _referenceEventService; @@ -32,11 +38,14 @@ namespace Bit.Admin.Controllers public OrganizationsController( IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, + IOrganizationConnectionRepository organizationConnectionRepository, + ISelfHostedSyncSponsorshipsCommand syncSponsorshipsCommand, ICipherRepository cipherRepository, ICollectionRepository collectionRepository, IGroupRepository groupRepository, IPolicyRepository policyRepository, IPaymentService paymentService, + ILicensingService licensingService, IApplicationCacheService applicationCacheService, GlobalSettings globalSettings, IReferenceEventService referenceEventService, @@ -44,11 +53,14 @@ namespace Bit.Admin.Controllers { _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; + _organizationConnectionRepository = organizationConnectionRepository; + _syncSponsorshipsCommand = syncSponsorshipsCommand; _cipherRepository = cipherRepository; _collectionRepository = collectionRepository; _groupRepository = groupRepository; _policyRepository = policyRepository; _paymentService = paymentService; + _licensingService = licensingService; _applicationCacheService = applicationCacheService; _globalSettings = globalSettings; _referenceEventService = referenceEventService; @@ -104,7 +116,8 @@ namespace Bit.Admin.Controllers policies = await _policyRepository.GetManyByOrganizationIdAsync(id); } var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id); - return View(new OrganizationViewModel(organization, users, ciphers, collections, groups, policies)); + var billingSyncConnection = _globalSettings.EnableCloudCommunication ? await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync) : null; + return View(new OrganizationViewModel(organization, billingSyncConnection, users, ciphers, collections, groups, policies)); } [SelfHosted(NotSelfHostedOnly = true)] @@ -130,8 +143,9 @@ namespace Bit.Admin.Controllers } var users = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(id); var billingInfo = await _paymentService.GetBillingAsync(organization); + var billingSyncConnection = _globalSettings.EnableCloudCommunication ? await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync) : null; return View(new OrganizationEditModel(organization, users, ciphers, collections, groups, policies, - billingInfo, _globalSettings)); + billingInfo, billingSyncConnection, _globalSettings)); } [HttpPost] @@ -164,5 +178,40 @@ namespace Bit.Admin.Controllers return RedirectToAction("Index"); } + + public async Task TriggerBillingSync(Guid id) + { + var organization = await _organizationRepository.GetByIdAsync(id); + if (organization == null) + { + return RedirectToAction("Index"); + } + var connection = (await _organizationConnectionRepository.GetEnabledByOrganizationIdTypeAsync(id, OrganizationConnectionType.CloudBillingSync)).FirstOrDefault(); + if (connection != null) + { + try + { + var config = connection.GetConfig(); + await _syncSponsorshipsCommand.SyncOrganization(id, config.CloudOrganizationId, connection); + TempData["ConnectionActivated"] = id; + TempData["ConnectionError"] = null; + } + catch (Exception ex) + { + TempData["ConnectionError"] = ex.Message; + } + + if (_globalSettings.SelfHosted) + { + return RedirectToAction("View", new { id }); + } + else + { + return RedirectToAction("Edit", new { id }); + } + } + return RedirectToAction("Index"); + } + } } diff --git a/src/Admin/Jobs/DatabaseExpiredSponsorshipsJob.cs b/src/Admin/Jobs/DatabaseExpiredSponsorshipsJob.cs new file mode 100644 index 000000000..da682ca82 --- /dev/null +++ b/src/Admin/Jobs/DatabaseExpiredSponsorshipsJob.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; +using Bit.Core; +using Bit.Core.Jobs; +using Bit.Core.Repositories; +using Bit.Core.Settings; +using Microsoft.Extensions.Logging; +using Quartz; + +namespace Bit.Admin.Jobs +{ + public class DatabaseExpiredSponsorshipsJob : BaseJob + { + private GlobalSettings _globalSettings; + private readonly IMaintenanceRepository _maintenanceRepository; + + public DatabaseExpiredSponsorshipsJob( + IMaintenanceRepository maintenanceRepository, + ILogger logger, + GlobalSettings globalSettings) + : base(logger) + { + _maintenanceRepository = maintenanceRepository; + _globalSettings = globalSettings; + } + + protected override async Task ExecuteJobAsync(IJobExecutionContext context) + { + if (_globalSettings.SelfHosted && !_globalSettings.EnableCloudCommunication) + { + return; + } + _logger.LogInformation(Constants.BypassFiltersEventId, "Execute job task: DeleteExpiredSponsorshipsAsync"); + + // allow a 90 day grace period before deleting + var deleteDate = DateTime.UtcNow.AddDays(-90); + + await _maintenanceRepository.DeleteExpiredSponsorshipsAsync(deleteDate); + _logger.LogInformation(Constants.BypassFiltersEventId, "Finished job task: DeleteExpiredSponsorshipsAsync"); + } + } +} diff --git a/src/Admin/Jobs/JobsHostedService.cs b/src/Admin/Jobs/JobsHostedService.cs index 8bef67ff4..fec687eea 100644 --- a/src/Admin/Jobs/JobsHostedService.cs +++ b/src/Admin/Jobs/JobsHostedService.cs @@ -55,6 +55,11 @@ namespace Bit.Admin.Jobs .StartNow() .WithCronSchedule("0 0 0 ? * SUN", x => x.InTimeZone(timeZone)) .Build(); + var everyMondayAtMidnightTrigger = TriggerBuilder.Create() + .WithIdentity("EveryMondayAtMidnightTrigger") + .StartNow() + .WithCronSchedule("0 0 0 ? * MON", x => x.InTimeZone(timeZone)) + .Build(); var everyDayAtMidnightUtc = TriggerBuilder.Create() .WithIdentity("EveryDayAtMidnightUtc") .StartNow() @@ -67,7 +72,8 @@ namespace Bit.Admin.Jobs new Tuple(typeof(DatabaseExpiredGrantsJob), everyFridayAt10pmTrigger), new Tuple(typeof(DatabaseUpdateStatisticsJob), everySaturdayAtMidnightTrigger), new Tuple(typeof(DatabaseRebuildlIndexesJob), everySundayAtMidnightTrigger), - new Tuple(typeof(DeleteCiphersJob), everyDayAtMidnightUtc) + new Tuple(typeof(DeleteCiphersJob), everyDayAtMidnightUtc), + new Tuple(typeof(DatabaseExpiredSponsorshipsJob), everyMondayAtMidnightTrigger) }; if (!_globalSettings.SelfHosted) @@ -88,6 +94,7 @@ namespace Bit.Admin.Jobs services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); services.AddTransient(); services.AddTransient(); } diff --git a/src/Admin/Models/OrganizationEditModel.cs b/src/Admin/Models/OrganizationEditModel.cs index c587d87c4..6d20fe28a 100644 --- a/src/Admin/Models/OrganizationEditModel.cs +++ b/src/Admin/Models/OrganizationEditModel.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; -using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Settings; using Bit.Core.Utilities; @@ -16,8 +16,9 @@ namespace Bit.Admin.Models public OrganizationEditModel(Organization org, IEnumerable orgUsers, IEnumerable ciphers, IEnumerable collections, IEnumerable groups, - IEnumerable policies, BillingInfo billingInfo, GlobalSettings globalSettings) - : base(org, orgUsers, ciphers, collections, groups, policies) + IEnumerable policies, BillingInfo billingInfo, IEnumerable connections, + GlobalSettings globalSettings) + : base(org, connections, orgUsers, ciphers, collections, groups, policies) { BillingInfo = billingInfo; BraintreeMerchantId = globalSettings.Braintree.MerchantId; diff --git a/src/Admin/Models/OrganizationViewModel.cs b/src/Admin/Models/OrganizationViewModel.cs index 49309fcd1..ee38e81f3 100644 --- a/src/Admin/Models/OrganizationViewModel.cs +++ b/src/Admin/Models/OrganizationViewModel.cs @@ -1,9 +1,8 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.Linq; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Admin.Models { @@ -11,11 +10,12 @@ namespace Bit.Admin.Models { public OrganizationViewModel() { } - public OrganizationViewModel(Organization org, IEnumerable orgUsers, - IEnumerable ciphers, IEnumerable collections, IEnumerable groups, - IEnumerable policies) + public OrganizationViewModel(Organization org, IEnumerable connections, + IEnumerable orgUsers, IEnumerable ciphers, IEnumerable collections, + IEnumerable groups, IEnumerable policies) { Organization = org; + Connections = connections ?? Enumerable.Empty(); HasPublicPrivateKeys = org.PublicKey != null && org.PrivateKey != null; UserInvitedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Invited); UserAcceptedCount = orgUsers.Count(u => u.Status == OrganizationUserStatusType.Accepted); @@ -36,6 +36,7 @@ namespace Bit.Admin.Models } public Organization Organization { get; set; } + public IEnumerable Connections { get; set; } public string Owners { get; set; } public string Admins { get; set; } public int UserInvitedCount { get; set; } diff --git a/src/Admin/Startup.cs b/src/Admin/Startup.cs index 61a354251..e964814c4 100644 --- a/src/Admin/Startup.cs +++ b/src/Admin/Startup.cs @@ -69,7 +69,7 @@ namespace Bit.Admin } // Services - services.AddBaseServices(); + services.AddBaseServices(globalSettings); services.AddDefaultServices(globalSettings); #if OSS diff --git a/src/Admin/Views/Organizations/Connections.cshtml b/src/Admin/Views/Organizations/Connections.cshtml new file mode 100644 index 000000000..f1f8f4a8d --- /dev/null +++ b/src/Admin/Views/Organizations/Connections.cshtml @@ -0,0 +1,81 @@ +@model OrganizationViewModel +

Connections

+
+
+
+ + + + + + + + + + @if(!Model.Connections.Any()) + { + + + + } + else + { + @foreach(var connection in Model.Connections) + { + + + + + + } + } + +
TypeStatus
No results to list.
+ @if(connection.Type == OrganizationConnectionType.CloudBillingSync) + { + @:Billing Sync + } + + @if(@TempData["ConnectionError"] != null) + { + + @TempData["ConnectionError"] + + } + else + { + @if(connection.Enabled) + { + @:Enabled + } + else + { + @:Disabled + } + } + + @if(connection.Enabled) + { + @if(@TempData["ConnectionActivated"] != null && @TempData["ConnectionActivated"].ToString() == @Model.Organization.Id.ToString()) + { + @if(connection.Type.Equals(OrganizationConnectionType.CloudBillingSync)) + { + + } + } + else + { + @if(connection.Type.Equals(OrganizationConnectionType.CloudBillingSync)) + { + + Manually Sync + + } + } + } +
+
+
+
\ No newline at end of file diff --git a/src/Admin/Views/Organizations/View.cshtml b/src/Admin/Views/Organizations/View.cshtml index b7d8e00a2..38d58e61a 100644 --- a/src/Admin/Views/Organizations/View.cshtml +++ b/src/Admin/Views/Organizations/View.cshtml @@ -1,4 +1,5 @@ -@model OrganizationViewModel +@inject Bit.Core.Settings.GlobalSettings GlobalSettings +@model OrganizationViewModel @{ ViewData["Title"] = "Organization: " + Model.Organization.Name; } @@ -7,6 +8,10 @@

Information

@await Html.PartialAsync("_ViewInformation", Model) +@if(GlobalSettings.SelfHosted) +{ + @await Html.PartialAsync("Connections", Model) +}
diff --git a/src/Admin/Views/_ViewImports.cshtml b/src/Admin/Views/_ViewImports.cshtml index 194be5ed4..b3dac997e 100644 --- a/src/Admin/Views/_ViewImports.cshtml +++ b/src/Admin/Views/_ViewImports.cshtml @@ -2,5 +2,6 @@ @using Bit.Admin @using Bit.Admin.Models @using Bit.Core.Enums.Provider +@using Bit.Core.Enums @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers @addTagHelper "*, Admin" diff --git a/src/Api/Controllers/OrganizationConnectionsController.cs b/src/Api/Controllers/OrganizationConnectionsController.cs new file mode 100644 index 000000000..ff695252c --- /dev/null +++ b/src/Api/Controllers/OrganizationConnectionsController.cs @@ -0,0 +1,162 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Bit.Api.Models.Request.Organizations; +using Bit.Api.Models.Response.Organizations; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.OrganizationConnectionConfigs; +using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.Controllers +{ + [SelfHosted(SelfHostedOnly = true)] + [Authorize("Application")] + [Route("organizations/connections")] + public class OrganizationConnectionsController : Controller + { + private readonly ICreateOrganizationConnectionCommand _createOrganizationConnectionCommand; + private readonly IUpdateOrganizationConnectionCommand _updateOrganizationConnectionCommand; + private readonly IDeleteOrganizationConnectionCommand _deleteOrganizationConnectionCommand; + private readonly IOrganizationConnectionRepository _organizationConnectionRepository; + private readonly ICurrentContext _currentContext; + private readonly IGlobalSettings _globalSettings; + private readonly ILicensingService _licensingService; + + public OrganizationConnectionsController( + ICreateOrganizationConnectionCommand createOrganizationConnectionCommand, + IUpdateOrganizationConnectionCommand updateOrganizationConnectionCommand, + IDeleteOrganizationConnectionCommand deleteOrganizationConnectionCommand, + IOrganizationConnectionRepository organizationConnectionRepository, + ICurrentContext currentContext, + IGlobalSettings globalSettings, + ILicensingService licensingService) + { + _createOrganizationConnectionCommand = createOrganizationConnectionCommand; + _updateOrganizationConnectionCommand = updateOrganizationConnectionCommand; + _deleteOrganizationConnectionCommand = deleteOrganizationConnectionCommand; + _organizationConnectionRepository = organizationConnectionRepository; + _currentContext = currentContext; + _globalSettings = globalSettings; + _licensingService = licensingService; + } + + [HttpGet("enabled")] + public bool ConnectionsEnabled() + { + return _globalSettings.SelfHosted && _globalSettings.EnableCloudCommunication; + } + + [HttpPost] + public async Task CreateConnection([FromBody] OrganizationConnectionRequestModel model) + { + if (!await HasPermissionAsync(model?.OrganizationId)) + { + throw new BadRequestException("Only the owner of an organization can create a connection."); + } + + if (await HasConnectionTypeAsync(model)) + { + throw new BadRequestException($"The requested organization already has a connection of type {model.Type}. Only one of each connection type may exist per organization."); + } + + switch (model.Type) + { + case OrganizationConnectionType.CloudBillingSync: + var typedModel = new OrganizationConnectionRequestModel(model); + var license = await _licensingService.ReadOrganizationLicenseAsync(model.OrganizationId); + typedModel.ParsedConfig.CloudOrganizationId = license.Id; + var connection = await _createOrganizationConnectionCommand.CreateAsync(typedModel.ToData()); + return new OrganizationConnectionResponseModel(connection, typeof(BillingSyncConfig)); + default: + throw new BadRequestException($"Unknown Organization connection Type: {model.Type}"); + } + } + + [HttpPut("{organizationConnectionId}")] + public async Task UpdateConnection(Guid organizationConnectionId, [FromBody] OrganizationConnectionRequestModel model) + { + if (!await HasPermissionAsync(model?.OrganizationId)) + { + throw new BadRequestException("Only the owner of an organization can update a connection."); + } + + if (await HasConnectionTypeAsync(model, organizationConnectionId)) + { + throw new BadRequestException($"The requested organization already has a connection of type {model.Type}. Only one of each connection type may exist per organization."); + } + + switch (model.Type) + { + case OrganizationConnectionType.CloudBillingSync: + var typedModel = new OrganizationConnectionRequestModel(model); + var connection = await _updateOrganizationConnectionCommand.UpdateAsync(typedModel.ToData(organizationConnectionId)); + return new OrganizationConnectionResponseModel(connection, typeof(BillingSyncConfig)); + default: + throw new BadRequestException($"Unkown Organization connection Type: {model.Type}"); + } + } + + [HttpGet("{organizationId}/{type}")] + public async Task GetConnection(Guid organizationId, OrganizationConnectionType type) + { + if (!await HasPermissionAsync(organizationId)) + { + throw new BadRequestException("Only the owner of an organization can retrieve a connection."); + } + + var connections = await GetConnectionsAsync(organizationId); + var connection = connections.FirstOrDefault(c => c.Type == type); + + switch (type) + { + case OrganizationConnectionType.CloudBillingSync: + return new OrganizationConnectionResponseModel(connection, typeof(BillingSyncConfig)); + default: + throw new BadRequestException($"Unkown Organization connection Type: {type}"); + } + + } + + [HttpDelete("{organizationConnectionId}")] + [HttpPost("{organizationConnectionId}/delete")] + public async Task DeleteConnection(Guid organizationConnectionId) + { + var connection = await _organizationConnectionRepository.GetByIdAsync(organizationConnectionId); + + if (connection == null) + { + throw new NotFoundException(); + } + + if (!await HasPermissionAsync(connection.OrganizationId)) + { + throw new BadRequestException("Only the owner of an organization can remove a connection."); + } + + await _deleteOrganizationConnectionCommand.DeleteAsync(connection); + } + + private async Task> GetConnectionsAsync(Guid organizationId) => + await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organizationId, OrganizationConnectionType.CloudBillingSync); + + private async Task HasConnectionTypeAsync(OrganizationConnectionRequestModel model, Guid? connectionId = null) + { + var existingConnections = await GetConnectionsAsync(model.OrganizationId); + + return existingConnections.Any(c => c.Type == model.Type && (!connectionId.HasValue || c.Id != connectionId.Value)); + } + + private async Task HasPermissionAsync(Guid? organizationId) => + organizationId.HasValue && await _currentContext.OrganizationOwner(organizationId.Value); + } +} diff --git a/src/Api/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Controllers/OrganizationSponsorshipsController.cs index 07c3fd520..9a9732b72 100644 --- a/src/Api/Controllers/OrganizationSponsorshipsController.cs +++ b/src/Api/Controllers/OrganizationSponsorshipsController.cs @@ -1,9 +1,15 @@ using System; using System.Threading.Tasks; using Bit.Api.Models.Request.Organizations; +using Bit.Api.Models.Response.Organizations; using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; +using Bit.Core.Models.Api.Request.OrganizationSponsorships; +using Bit.Core.Models.Api.Request.OrganizationSponsorships; +using Bit.Core.Models.Api.Response.OrganizationSponsorships; +using Bit.Core.Models.Api.Response.OrganizationSponsorships; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; @@ -13,42 +19,65 @@ using Microsoft.AspNetCore.Mvc; namespace Bit.Api.Controllers { [Route("organization/sponsorship")] - [Authorize("Application")] public class OrganizationSponsorshipsController : Controller { - private readonly IOrganizationSponsorshipService _organizationsSponsorshipService; private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository; private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IValidateRedemptionTokenCommand _validateRedemptionTokenCommand; + private readonly IValidateBillingSyncKeyCommand _validateBillingSyncKeyCommand; + private readonly ICreateSponsorshipCommand _createSponsorshipCommand; + private readonly ISendSponsorshipOfferCommand _sendSponsorshipOfferCommand; + private readonly ISetUpSponsorshipCommand _setUpSponsorshipCommand; + private readonly IRevokeSponsorshipCommand _revokeSponsorshipCommand; + private readonly IRemoveSponsorshipCommand _removeSponsorshipCommand; + private readonly ICloudSyncSponsorshipsCommand _syncSponsorshipsCommand; private readonly ICurrentContext _currentContext; private readonly IUserService _userService; - public OrganizationSponsorshipsController(IOrganizationSponsorshipService organizationSponsorshipService, + public OrganizationSponsorshipsController( IOrganizationSponsorshipRepository organizationSponsorshipRepository, IOrganizationRepository organizationRepository, IOrganizationUserRepository organizationUserRepository, + IValidateRedemptionTokenCommand validateRedemptionTokenCommand, + IValidateBillingSyncKeyCommand validateBillingSyncKeyCommand, + ICreateSponsorshipCommand createSponsorshipCommand, + ISendSponsorshipOfferCommand sendSponsorshipOfferCommand, + ISetUpSponsorshipCommand setUpSponsorshipCommand, + IRevokeSponsorshipCommand revokeSponsorshipCommand, + IRemoveSponsorshipCommand removeSponsorshipCommand, + ICloudSyncSponsorshipsCommand syncSponsorshipsCommand, IUserService userService, ICurrentContext currentContext) { - _organizationsSponsorshipService = organizationSponsorshipService; _organizationSponsorshipRepository = organizationSponsorshipRepository; _organizationRepository = organizationRepository; _organizationUserRepository = organizationUserRepository; + _validateRedemptionTokenCommand = validateRedemptionTokenCommand; + _validateBillingSyncKeyCommand = validateBillingSyncKeyCommand; + _createSponsorshipCommand = createSponsorshipCommand; + _sendSponsorshipOfferCommand = sendSponsorshipOfferCommand; + _setUpSponsorshipCommand = setUpSponsorshipCommand; + _revokeSponsorshipCommand = revokeSponsorshipCommand; + _removeSponsorshipCommand = removeSponsorshipCommand; + _syncSponsorshipsCommand = syncSponsorshipsCommand; _userService = userService; _currentContext = currentContext; } + [Authorize("Application")] [HttpPost("{sponsoringOrgId}/families-for-enterprise")] [SelfHosted(NotSelfHostedOnly = true)] - public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] OrganizationSponsorshipRequestModel model) + public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] OrganizationSponsorshipCreateRequestModel model) { - await _organizationsSponsorshipService.OfferSponsorshipAsync( + var sponsorship = await _createSponsorshipCommand.CreateSponsorshipAsync( await _organizationRepository.GetByIdAsync(sponsoringOrgId), await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default), - model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName, - (await CurrentUser).Email); + model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName); + await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, (await CurrentUser).Email); } + [Authorize("Application")] [HttpPost("{sponsoringOrgId}/families-for-enterprise/resend")] [SelfHosted(NotSelfHostedOnly = true)] public async Task ResendSponsorshipOffer(Guid sponsoringOrgId) @@ -56,26 +85,27 @@ namespace Bit.Api.Controllers var sponsoringOrgUser = await _organizationUserRepository .GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default); - await _organizationsSponsorshipService.ResendSponsorshipOfferAsync( + await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync( await _organizationRepository.GetByIdAsync(sponsoringOrgId), sponsoringOrgUser, await _organizationSponsorshipRepository - .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id), - (await CurrentUser).Email); + .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id)); } + [Authorize("Application")] [HttpPost("validate-token")] [SelfHosted(NotSelfHostedOnly = true)] public async Task PreValidateSponsorshipToken([FromQuery] string sponsorshipToken) { - return (await _organizationsSponsorshipService.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email)).valid; + return (await _validateRedemptionTokenCommand.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email)).valid; } + [Authorize("Application")] [HttpPost("redeem")] [SelfHosted(NotSelfHostedOnly = true)] public async Task RedeemSponsorship([FromQuery] string sponsorshipToken, [FromBody] OrganizationSponsorshipRedeemRequestModel model) { - var (valid, sponsorship) = await _organizationsSponsorshipService.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email); + var (valid, sponsorship) = await _validateRedemptionTokenCommand.ValidateRedemptionTokenAsync(sponsorshipToken, (await CurrentUser).Email); if (!valid) { @@ -87,12 +117,27 @@ namespace Bit.Api.Controllers throw new BadRequestException("Can only redeem sponsorship for an organization you own."); } - await _organizationsSponsorshipService.SetUpSponsorshipAsync( + await _setUpSponsorshipCommand.SetUpSponsorshipAsync( sponsorship, - // Check org to sponsor's product type await _organizationRepository.GetByIdAsync(model.SponsoredOrganizationId)); } + [Authorize("Installation")] + [HttpPost("sync")] + public async Task Sync([FromBody] OrganizationSponsorshipSyncRequestModel model) + { + var sponsoringOrg = await _organizationRepository.GetByIdAsync(model.SponsoringOrganizationCloudId); + if (!await _validateBillingSyncKeyCommand.ValidateBillingSyncKeyAsync(sponsoringOrg, model.BillingSyncKey)) + { + throw new BadRequestException("Invalid Billing Sync Key"); + } + + var (syncResponseData, offersToSend) = await _syncSponsorshipsCommand.SyncOrganization(sponsoringOrg, model.ToOrganizationSponsorshipSync().SponsorshipsBatch); + await _sendSponsorshipOfferCommand.BulkSendSponsorshipOfferAsync(sponsoringOrg.Name, offersToSend); + return new OrganizationSponsorshipSyncResponseModel(syncResponseData); + } + + [Authorize("Application")] [HttpDelete("{sponsoringOrganizationId}")] [HttpPost("{sponsoringOrganizationId}/delete")] [SelfHosted(NotSelfHostedOnly = true)] @@ -108,12 +153,10 @@ namespace Bit.Api.Controllers var existingOrgSponsorship = await _organizationSponsorshipRepository .GetBySponsoringOrganizationUserIdAsync(orgUser.Id); - await _organizationsSponsorshipService.RevokeSponsorshipAsync( - await _organizationRepository - .GetByIdAsync(existingOrgSponsorship.SponsoredOrganizationId ?? default), - existingOrgSponsorship); + await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship); } + [Authorize("Application")] [HttpDelete("sponsored/{sponsoredOrgId}")] [HttpPost("sponsored/{sponsoredOrgId}/remove")] [SelfHosted(NotSelfHostedOnly = true)] @@ -128,10 +171,21 @@ namespace Bit.Api.Controllers var existingOrgSponsorship = await _organizationSponsorshipRepository .GetBySponsoredOrganizationIdAsync(sponsoredOrgId); - await _organizationsSponsorshipService.RemoveSponsorshipAsync( - await _organizationRepository - .GetByIdAsync(existingOrgSponsorship.SponsoredOrganizationId.Value), - existingOrgSponsorship); + await _removeSponsorshipCommand.RemoveSponsorshipAsync(existingOrgSponsorship); + } + + [HttpGet("{sponsoringOrgId}/sync-status")] + public async Task GetSyncStatus(Guid sponsoringOrgId) + { + var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgId); + + if (!await _currentContext.OrganizationOwner(sponsoringOrg.Id)) + { + throw new NotFoundException(); + } + + var lastSyncDate = await _organizationSponsorshipRepository.GetLatestSyncDateBySponsoringOrganizationIdAsync(sponsoringOrg.Id); + return new OrganizationSponsorshipSyncStatusResponseModel(lastSyncDate); } private Task CurrentUser => _userService.GetUserByIdAsync(_currentContext.UserId.Value); diff --git a/src/Api/Controllers/OrganizationUsersController.cs b/src/Api/Controllers/OrganizationUsersController.cs index 3fc985dc2..e900fa559 100644 --- a/src/Api/Controllers/OrganizationUsersController.cs +++ b/src/Api/Controllers/OrganizationUsersController.cs @@ -4,11 +4,12 @@ using System.Linq; using System.Threading.Tasks; using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Response; +using Bit.Api.Models.Response.Organizations; using Bit.Core.Context; using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; -using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; using Microsoft.AspNetCore.Authorization; diff --git a/src/Api/Controllers/OrganizationsController.cs b/src/Api/Controllers/OrganizationsController.cs index 41ee259c4..5b00261ae 100644 --- a/src/Api/Controllers/OrganizationsController.cs +++ b/src/Api/Controllers/OrganizationsController.cs @@ -6,12 +6,14 @@ using Bit.Api.Models.Request; using Bit.Api.Models.Request.Accounts; using Bit.Api.Models.Request.Organizations; using Bit.Api.Models.Response; +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.Data; +using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -34,6 +36,9 @@ namespace Bit.Api.Controllers private readonly ICurrentContext _currentContext; private readonly ISsoConfigRepository _ssoConfigRepository; private readonly ISsoConfigService _ssoConfigService; + private readonly IGetOrganizationApiKeyCommand _getOrganizationApiKeyCommand; + private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand; + private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; private readonly GlobalSettings _globalSettings; public OrganizationsController( @@ -46,6 +51,9 @@ namespace Bit.Api.Controllers ICurrentContext currentContext, ISsoConfigRepository ssoConfigRepository, ISsoConfigService ssoConfigService, + IGetOrganizationApiKeyCommand getOrganizationApiKeyCommand, + IRotateOrganizationApiKeyCommand rotateOrganizationApiKeyCommand, + IOrganizationApiKeyRepository organizationApiKeyRepository, GlobalSettings globalSettings) { _organizationRepository = organizationRepository; @@ -57,6 +65,9 @@ namespace Bit.Api.Controllers _currentContext = currentContext; _ssoConfigRepository = ssoConfigRepository; _ssoConfigService = ssoConfigService; + _getOrganizationApiKeyCommand = getOrganizationApiKeyCommand; + _rotateOrganizationApiKeyCommand = rotateOrganizationApiKeyCommand; + _organizationApiKeyRepository = organizationApiKeyRepository; _globalSettings = globalSettings; } @@ -482,7 +493,7 @@ namespace Bit.Api.Controllers } [HttpPost("{id}/api-key")] - public async Task ApiKey(string id, [FromBody] SecretVerificationRequestModel model) + public async Task ApiKey(string id, [FromBody] OrganizationApiKeyRequestModel model) { var orgIdGuid = new Guid(id); if (!await _currentContext.OrganizationOwner(orgIdGuid)) @@ -496,6 +507,19 @@ namespace Bit.Api.Controllers throw new NotFoundException(); } + if (model.Type == OrganizationApiKeyType.BillingSync) + { + // Non-enterprise orgs should not be able to create or view an apikey of billing sync key type + var plan = StaticStore.GetPlan(organization.PlanType); + if (plan.Product != ProductType.Enterprise) + { + throw new NotFoundException(); + } + } + + var organizationApiKey = await _getOrganizationApiKeyCommand + .GetOrganizationApiKeyAsync(organization.Id, model.Type); + var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) { @@ -509,13 +533,27 @@ namespace Bit.Api.Controllers } else { - var response = new ApiKeyResponseModel(organization); + var response = new ApiKeyResponseModel(organizationApiKey); return response; } } + [HttpGet("{id}/api-key-information")] + public async Task> ApiKeyInformation(Guid id) + { + if (!await _currentContext.OrganizationOwner(id)) + { + throw new NotFoundException(); + } + + var apiKeys = await _organizationApiKeyRepository.GetManyByOrganizationIdTypeAsync(id); + + return new ListResponseModel( + apiKeys.Select(k => new OrganizationApiKeyInformation(k))); + } + [HttpPost("{id}/rotate-api-key")] - public async Task RotateApiKey(string id, [FromBody] SecretVerificationRequestModel model) + public async Task RotateApiKey(string id, [FromBody] OrganizationApiKeyRequestModel model) { var orgIdGuid = new Guid(id); if (!await _currentContext.OrganizationOwner(orgIdGuid)) @@ -529,6 +567,9 @@ namespace Bit.Api.Controllers throw new NotFoundException(); } + var organizationApiKey = await _getOrganizationApiKeyCommand + .GetOrganizationApiKeyAsync(organization.Id, model.Type); + var user = await _userService.GetUserByPrincipalAsync(User); if (user == null) { @@ -542,8 +583,8 @@ namespace Bit.Api.Controllers } else { - await _organizationService.RotateApiKeyAsync(organization); - var response = new ApiKeyResponseModel(organization); + await _rotateOrganizationApiKeyCommand.RotateApiKeyAsync(organizationApiKey); + var response = new ApiKeyResponseModel(organizationApiKey); return response; } } diff --git a/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs new file mode 100644 index 000000000..e29afc436 --- /dev/null +++ b/src/Api/Controllers/SelfHosted/SelfHostedOrganizationSponsorshipsController.cs @@ -0,0 +1,69 @@ +using System; +using System.Threading.Tasks; +using Bit.Api.Models.Request.Organizations; +using Bit.Core.Context; +using Bit.Core.Exceptions; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Api.Controllers.SelfHosted +{ + [Route("organization/sponsorship/self-hosted")] + [Authorize("Application")] + [SelfHosted(SelfHostedOnly = true)] + public class SelfHostedOrganizationSponsorshipsController : Controller + { + private readonly IOrganizationRepository _organizationRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository; + private readonly ICreateSponsorshipCommand _offerSponsorshipCommand; + private readonly IRevokeSponsorshipCommand _revokeSponsorshipCommand; + private readonly ICurrentContext _currentContext; + + public SelfHostedOrganizationSponsorshipsController( + ICreateSponsorshipCommand offerSponsorshipCommand, + IRevokeSponsorshipCommand revokeSponsorshipCommand, + IOrganizationRepository organizationRepository, + IOrganizationSponsorshipRepository organizationSponsorshipRepository, + IOrganizationUserRepository organizationUserRepository, + ICurrentContext currentContext + ) + { + _offerSponsorshipCommand = offerSponsorshipCommand; + _revokeSponsorshipCommand = revokeSponsorshipCommand; + _organizationRepository = organizationRepository; + _organizationSponsorshipRepository = organizationSponsorshipRepository; + _organizationUserRepository = organizationUserRepository; + _currentContext = currentContext; + } + + [HttpPost("{sponsoringOrgId}/families-for-enterprise")] + public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] OrganizationSponsorshipCreateRequestModel model) + { + await _offerSponsorshipCommand.CreateSponsorshipAsync( + await _organizationRepository.GetByIdAsync(sponsoringOrgId), + await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default), + model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName); + } + + [HttpDelete("{sponsoringOrgId}")] + [HttpPost("{sponsoringOrgId}/delete")] + public async Task RevokeSponsorship(Guid sponsoringOrgId) + { + var orgUser = await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default); + + if (orgUser == null) + { + throw new BadRequestException("Unknown Organization User"); + } + + var existingOrgSponsorship = await _organizationSponsorshipRepository + .GetBySponsoringOrganizationUserIdAsync(orgUser.Id); + + await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship); + } + } +} diff --git a/src/Api/Jobs/JobsHostedService.cs b/src/Api/Jobs/JobsHostedService.cs index f2fa924ee..eabe02427 100644 --- a/src/Api/Jobs/JobsHostedService.cs +++ b/src/Api/Jobs/JobsHostedService.cs @@ -46,8 +46,15 @@ namespace Bit.Api.Jobs .StartNow() .WithCronSchedule("0 30 */12 * * ?") .Build(); + var randomDailySponsorshipSyncTrigger = TriggerBuilder.Create() + .WithIdentity("RandomDailySponsorshipSyncTrigger") + .StartAt(DateBuilder.FutureDate(new Random().Next(24), IntervalUnit.Hour)) + .WithSimpleSchedule(x => x + .WithIntervalInHours(24) + .RepeatForever()) + .Build(); - Jobs = new List> + var jobs = new List> { new Tuple(typeof(AliveJob), everyTopOfTheHourTrigger), new Tuple(typeof(EmergencyAccessNotificationJob), emergencyAccessNotificationTrigger), @@ -56,11 +63,22 @@ namespace Bit.Api.Jobs new Tuple(typeof(ValidateOrganizationsJob), everyTwelfthHourAndThirtyMinutesTrigger) }; + if (_globalSettings.SelfHosted && _globalSettings.EnableCloudCommunication) + { + jobs.Add(new Tuple(typeof(SelfHostedSponsorshipSyncJob), randomDailySponsorshipSyncTrigger)); + } + + Jobs = jobs; + await base.StartAsync(cancellationToken); } - public static void AddJobsServices(IServiceCollection services) + public static void AddJobsServices(IServiceCollection services, bool selfHosted) { + if (selfHosted) + { + services.AddTransient(); + } services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/src/Api/Jobs/SelfHostedSponsorshipSyncJob.cs b/src/Api/Jobs/SelfHostedSponsorshipSyncJob.cs new file mode 100644 index 000000000..975d68fdb --- /dev/null +++ b/src/Api/Jobs/SelfHostedSponsorshipSyncJob.cs @@ -0,0 +1,73 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Enums; +using Bit.Core.Jobs; +using Bit.Core.Models.OrganizationConnectionConfigs; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Settings; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Quartz; + +namespace Bit.Api.Jobs +{ + public class SelfHostedSponsorshipSyncJob : BaseJob + { + private readonly IServiceProvider _serviceProvider; + private IOrganizationRepository _organizationRepository; + private IOrganizationConnectionRepository _organizationConnectionRepository; + private readonly ILicensingService _licensingService; + private GlobalSettings _globalSettings; + + public SelfHostedSponsorshipSyncJob( + IServiceProvider serviceProvider, + IOrganizationRepository organizationRepository, + IOrganizationConnectionRepository organizationConnectionRepository, + ILicensingService licensingService, + ILogger logger, + GlobalSettings globalSettings) + : base(logger) + { + _serviceProvider = serviceProvider; + _organizationRepository = organizationRepository; + _organizationConnectionRepository = organizationConnectionRepository; + _licensingService = licensingService; + _globalSettings = globalSettings; + } + + protected override async Task ExecuteJobAsync(IJobExecutionContext context) + { + if (!_globalSettings.EnableCloudCommunication) + { + _logger.LogInformation("Skipping Organization sync with cloud - Cloud communication is disabled in global settings"); + return; + } + + var organizations = await _organizationRepository.GetManyByEnabledAsync(); + + using (var scope = _serviceProvider.CreateScope()) + { + var syncCommand = scope.ServiceProvider.GetRequiredService(); + foreach (var org in organizations) + { + var connection = (await _organizationConnectionRepository.GetEnabledByOrganizationIdTypeAsync(org.Id, OrganizationConnectionType.CloudBillingSync)).FirstOrDefault(); + if (connection != null) + { + try + { + var config = connection.GetConfig(); + await syncCommand.SyncOrganization(org.Id, config.CloudOrganizationId, connection); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Sponsorship sync for organization {org.Name} Failed"); + } + } + } + } + } + } +} diff --git a/src/Api/Models/Public/MemberBaseModel.cs b/src/Api/Models/Public/MemberBaseModel.cs index 913f751a2..e3c233bf2 100644 --- a/src/Api/Models/Public/MemberBaseModel.cs +++ b/src/Api/Models/Public/MemberBaseModel.cs @@ -2,7 +2,7 @@ using System.ComponentModel.DataAnnotations; using Bit.Core.Entities; using Bit.Core.Enums; -using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Api.Models.Public { diff --git a/src/Api/Models/Public/Response/MemberResponseModel.cs b/src/Api/Models/Public/Response/MemberResponseModel.cs index 011bebd65..850aaa7ce 100644 --- a/src/Api/Models/Public/Response/MemberResponseModel.cs +++ b/src/Api/Models/Public/Response/MemberResponseModel.cs @@ -5,6 +5,7 @@ using System.Linq; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Api.Models.Public.Response { diff --git a/src/Api/Models/Request/Accounts/OrganizationApiKeyRequestModel.cs b/src/Api/Models/Request/Accounts/OrganizationApiKeyRequestModel.cs new file mode 100644 index 000000000..331cd7045 --- /dev/null +++ b/src/Api/Models/Request/Accounts/OrganizationApiKeyRequestModel.cs @@ -0,0 +1,9 @@ +using Bit.Core.Enums; + +namespace Bit.Api.Models.Request.Accounts +{ + public class OrganizationApiKeyRequestModel : SecretVerificationRequestModel + { + public OrganizationApiKeyType Type { get; set; } + } +} diff --git a/src/Api/Models/Request/Organizations/OrganizationConnectionRequestModel.cs b/src/Api/Models/Request/Organizations/OrganizationConnectionRequestModel.cs new file mode 100644 index 000000000..f26ba9428 --- /dev/null +++ b/src/Api/Models/Request/Organizations/OrganizationConnectionRequestModel.cs @@ -0,0 +1,52 @@ +using System; +using System.Text.Json; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data.Organizations.OrganizationConnections; +using Bit.Core.Utilities; + +namespace Bit.Api.Models.Request.Organizations +{ + public class OrganizationConnectionRequestModel + { + public OrganizationConnectionType Type { get; set; } + public Guid OrganizationId { get; set; } + public bool Enabled { get; set; } + public JsonDocument Config { get; set; } + + public OrganizationConnectionRequestModel() { } + } + + + public class OrganizationConnectionRequestModel : OrganizationConnectionRequestModel where T : new() + { + public T ParsedConfig { get; private set; } + + public OrganizationConnectionRequestModel(OrganizationConnectionRequestModel model) + { + Type = model.Type; + OrganizationId = model.OrganizationId; + Enabled = model.Enabled; + Config = model.Config; + + try + { + ParsedConfig = model.Config.ToObject(JsonHelpers.IgnoreCase); + } + catch (JsonException) + { + throw new BadRequestException("Organization Connection configuration malformed"); + } + } + + public OrganizationConnectionData ToData(Guid? id = null) => + new() + { + Id = id, + Type = Type, + OrganizationId = OrganizationId, + Enabled = Enabled, + Config = ParsedConfig, + }; + } +} diff --git a/src/Api/Models/Request/Organizations/OrganizationSponsorshipRequestModel.cs b/src/Api/Models/Request/Organizations/OrganizationSponsorshipCreateRequestModel.cs similarity index 88% rename from src/Api/Models/Request/Organizations/OrganizationSponsorshipRequestModel.cs rename to src/Api/Models/Request/Organizations/OrganizationSponsorshipCreateRequestModel.cs index 2fe13151b..e3848a4cb 100644 --- a/src/Api/Models/Request/Organizations/OrganizationSponsorshipRequestModel.cs +++ b/src/Api/Models/Request/Organizations/OrganizationSponsorshipCreateRequestModel.cs @@ -4,7 +4,7 @@ using Bit.Core.Utilities; namespace Bit.Api.Models.Request.Organizations { - public class OrganizationSponsorshipRequestModel + public class OrganizationSponsorshipCreateRequestModel { [Required] public PlanSponsorshipType PlanSponsorshipType { get; set; } diff --git a/src/Api/Models/Request/Organizations/OrganizationUserRequestModels.cs b/src/Api/Models/Request/Organizations/OrganizationUserRequestModels.cs index afa39a120..86bb2acb1 100644 --- a/src/Api/Models/Request/Organizations/OrganizationUserRequestModels.cs +++ b/src/Api/Models/Request/Organizations/OrganizationUserRequestModels.cs @@ -6,6 +6,7 @@ using System.Text.Json; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Utilities; namespace Bit.Api.Models.Request.Organizations diff --git a/src/Api/Models/Response/ApiKeyResponseModel.cs b/src/Api/Models/Response/ApiKeyResponseModel.cs index 4bae0882c..84a2b8b2a 100644 --- a/src/Api/Models/Response/ApiKeyResponseModel.cs +++ b/src/Api/Models/Response/ApiKeyResponseModel.cs @@ -6,14 +6,15 @@ namespace Bit.Api.Models.Response { public class ApiKeyResponseModel : ResponseModel { - public ApiKeyResponseModel(Organization organization, string obj = "apiKey") + public ApiKeyResponseModel(OrganizationApiKey organizationApiKey, string obj = "apiKey") : base(obj) { - if (organization == null) + if (organizationApiKey == null) { - throw new ArgumentNullException(nameof(organization)); + throw new ArgumentNullException(nameof(organizationApiKey)); } - ApiKey = organization.ApiKey; + ApiKey = organizationApiKey.ApiKey; + RevisionDate = organizationApiKey.RevisionDate; } public ApiKeyResponseModel(User user, string obj = "apiKey") @@ -24,8 +25,10 @@ namespace Bit.Api.Models.Response throw new ArgumentNullException(nameof(user)); } ApiKey = user.ApiKey; + RevisionDate = user.RevisionDate; } public string ApiKey { get; set; } + public DateTime RevisionDate { get; set; } } } diff --git a/src/Api/Models/Response/Organizations/OrganizationApiKeyInformationResponseModel.cs b/src/Api/Models/Response/Organizations/OrganizationApiKeyInformationResponseModel.cs new file mode 100644 index 000000000..516b73944 --- /dev/null +++ b/src/Api/Models/Response/Organizations/OrganizationApiKeyInformationResponseModel.cs @@ -0,0 +1,19 @@ +using System; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Api; + +namespace Bit.Api.Models.Response.Organizations +{ + public class OrganizationApiKeyInformation : ResponseModel + { + public OrganizationApiKeyInformation(OrganizationApiKey key) : base("keyInformation") + { + KeyType = key.Type; + RevisionDate = key.RevisionDate; + } + + public OrganizationApiKeyType KeyType { get; set; } + public DateTime RevisionDate { get; set; } + } +} diff --git a/src/Api/Models/Response/OrganizationAutoEnrollStatusResponseModel.cs b/src/Api/Models/Response/Organizations/OrganizationAutoEnrollStatusResponseModel.cs similarity index 90% rename from src/Api/Models/Response/OrganizationAutoEnrollStatusResponseModel.cs rename to src/Api/Models/Response/Organizations/OrganizationAutoEnrollStatusResponseModel.cs index 6ba4308d9..7f0168488 100644 --- a/src/Api/Models/Response/OrganizationAutoEnrollStatusResponseModel.cs +++ b/src/Api/Models/Response/Organizations/OrganizationAutoEnrollStatusResponseModel.cs @@ -1,7 +1,7 @@ using System; using Bit.Core.Models.Api; -namespace Bit.Api.Models.Response +namespace Bit.Api.Models.Response.Organizations { public class OrganizationAutoEnrollStatusResponseModel : ResponseModel { diff --git a/src/Api/Models/Response/Organizations/OrganizationConnectionResponseModel.cs b/src/Api/Models/Response/Organizations/OrganizationConnectionResponseModel.cs new file mode 100644 index 000000000..1d3063965 --- /dev/null +++ b/src/Api/Models/Response/Organizations/OrganizationConnectionResponseModel.cs @@ -0,0 +1,30 @@ +using System; +using System.Text.Json; +using Bit.Core.Entities; +using Bit.Core.Enums; + +namespace Bit.Api.Models.Response.Organizations +{ + public class OrganizationConnectionResponseModel + { + public Guid? Id { get; set; } + public OrganizationConnectionType Type { get; set; } + public Guid OrganizationId { get; set; } + public bool Enabled { get; set; } + public JsonDocument Config { get; set; } + + public OrganizationConnectionResponseModel(OrganizationConnection connection, Type configType) + { + if (connection == null) + { + return; + } + + Id = connection.Id; + Type = connection.Type; + OrganizationId = connection.OrganizationId; + Enabled = connection.Enabled; + Config = JsonDocument.Parse(connection.Config); + } + } +} diff --git a/src/Api/Models/Response/OrganizationKeysResponseModel.cs b/src/Api/Models/Response/Organizations/OrganizationKeysResponseModel.cs similarity index 92% rename from src/Api/Models/Response/OrganizationKeysResponseModel.cs rename to src/Api/Models/Response/Organizations/OrganizationKeysResponseModel.cs index e0f8a738e..622af5824 100644 --- a/src/Api/Models/Response/OrganizationKeysResponseModel.cs +++ b/src/Api/Models/Response/Organizations/OrganizationKeysResponseModel.cs @@ -2,7 +2,7 @@ using Bit.Core.Entities; using Bit.Core.Models.Api; -namespace Bit.Api.Models.Response +namespace Bit.Api.Models.Response.Organizations { public class OrganizationKeysResponseModel : ResponseModel { diff --git a/src/Api/Models/Response/OrganizationResponseModel.cs b/src/Api/Models/Response/Organizations/OrganizationResponseModel.cs similarity index 99% rename from src/Api/Models/Response/OrganizationResponseModel.cs rename to src/Api/Models/Response/Organizations/OrganizationResponseModel.cs index f2576b806..32e005215 100644 --- a/src/Api/Models/Response/OrganizationResponseModel.cs +++ b/src/Api/Models/Response/Organizations/OrganizationResponseModel.cs @@ -6,7 +6,7 @@ using Bit.Core.Models.Api; using Bit.Core.Models.Business; using Bit.Core.Utilities; -namespace Bit.Api.Models.Response +namespace Bit.Api.Models.Response.Organizations { public class OrganizationResponseModel : ResponseModel { diff --git a/src/Api/Models/Response/Organizations/OrganizationSponsorshipSyncStatusResponseModel.cs b/src/Api/Models/Response/Organizations/OrganizationSponsorshipSyncStatusResponseModel.cs new file mode 100644 index 000000000..897558f95 --- /dev/null +++ b/src/Api/Models/Response/Organizations/OrganizationSponsorshipSyncStatusResponseModel.cs @@ -0,0 +1,16 @@ +using System; +using Bit.Core.Models.Api; + +namespace Bit.Api.Models.Response.Organizations +{ + public class OrganizationSponsorshipSyncStatusResponseModel : ResponseModel + { + public OrganizationSponsorshipSyncStatusResponseModel(DateTime? lastSyncDate) + : base("syncStatus") + { + LastSyncDate = lastSyncDate; + } + + public DateTime? LastSyncDate { get; set; } + } +} diff --git a/src/Api/Models/Response/OrganizationSsoResponseModel.cs b/src/Api/Models/Response/Organizations/OrganizationSsoResponseModel.cs similarity index 97% rename from src/Api/Models/Response/OrganizationSsoResponseModel.cs rename to src/Api/Models/Response/Organizations/OrganizationSsoResponseModel.cs index 2f1fe9a3d..dd828630b 100644 --- a/src/Api/Models/Response/OrganizationSsoResponseModel.cs +++ b/src/Api/Models/Response/Organizations/OrganizationSsoResponseModel.cs @@ -3,7 +3,7 @@ using Bit.Core.Models.Api; using Bit.Core.Models.Data; using Bit.Core.Settings; -namespace Bit.Api.Models.Response +namespace Bit.Api.Models.Response.Organizations { public class OrganizationSsoResponseModel : ResponseModel { diff --git a/src/Api/Models/Response/OrganizationUserResponseModel.cs b/src/Api/Models/Response/Organizations/OrganizationUserResponseModel.cs similarity index 98% rename from src/Api/Models/Response/OrganizationUserResponseModel.cs rename to src/Api/Models/Response/Organizations/OrganizationUserResponseModel.cs index 03ad0a817..ac2c7593b 100644 --- a/src/Api/Models/Response/OrganizationUserResponseModel.cs +++ b/src/Api/Models/Response/Organizations/OrganizationUserResponseModel.cs @@ -5,9 +5,10 @@ using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Api; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Utilities; -namespace Bit.Api.Models.Response +namespace Bit.Api.Models.Response.Organizations { public class OrganizationUserResponseModel : ResponseModel { diff --git a/src/Api/Models/Response/ProfileOrganizationResponseModel.cs b/src/Api/Models/Response/ProfileOrganizationResponseModel.cs index a66198685..b75ad422a 100644 --- a/src/Api/Models/Response/ProfileOrganizationResponseModel.cs +++ b/src/Api/Models/Response/ProfileOrganizationResponseModel.cs @@ -1,6 +1,8 @@ -using Bit.Core.Enums; +using System; +using Bit.Core.Enums; using Bit.Core.Models.Api; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Utilities; namespace Bit.Api.Models.Response @@ -45,6 +47,9 @@ namespace Bit.Api.Models.Response StaticStore.GetSponsoredPlan(PlanSponsorshipType.FamiliesForEnterprise) .UsersCanSponsor(organization); PlanProductType = StaticStore.GetPlan(organization.PlanType).Product; + FamilySponsorshipLastSyncDate = organization.FamilySponsorshipLastSyncDate; + FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete; + FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil; if (organization.SsoConfig != null) { @@ -88,5 +93,8 @@ namespace Bit.Api.Models.Response public ProductType PlanProductType { get; set; } public bool KeyConnectorEnabled { get; set; } public string KeyConnectorUrl { get; set; } + public DateTime? FamilySponsorshipLastSyncDate { get; set; } + public DateTime? FamilySponsorshipValidUntil { get; set; } + public bool? FamilySponsorshipToDelete { get; set; } } } diff --git a/src/Api/Models/Response/ProfileResponseModel.cs b/src/Api/Models/Response/ProfileResponseModel.cs index 96da68789..59864dc87 100644 --- a/src/Api/Models/Response/ProfileResponseModel.cs +++ b/src/Api/Models/Response/ProfileResponseModel.cs @@ -5,6 +5,7 @@ using Bit.Api.Models.Response.Providers; using Bit.Core.Entities; using Bit.Core.Models.Api; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Api.Models.Response { diff --git a/src/Api/Models/Response/SyncResponseModel.cs b/src/Api/Models/Response/SyncResponseModel.cs index a30736f35..a2aba0428 100644 --- a/src/Api/Models/Response/SyncResponseModel.cs +++ b/src/Api/Models/Response/SyncResponseModel.cs @@ -4,6 +4,7 @@ using System.Linq; using Bit.Core.Entities; using Bit.Core.Models.Api; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Settings; using Core.Models.Data; diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs index db84ec336..d2593e9dd 100644 --- a/src/Api/Startup.cs +++ b/src/Api/Startup.cs @@ -114,12 +114,17 @@ namespace Bit.Api policy.RequireAuthenticatedUser(); policy.RequireClaim(JwtClaimTypes.Scope, "api.organization"); }); + config.AddPolicy("Installation", policy => + { + policy.RequireAuthenticatedUser(); + policy.RequireClaim(JwtClaimTypes.Scope, "api.installation"); + }); }); services.AddScoped(); // Services - services.AddBaseServices(); + services.AddBaseServices(globalSettings); services.AddDefaultServices(globalSettings); services.AddCoreLocalizationServices(); @@ -137,15 +142,9 @@ namespace Bit.Api }); services.AddSwagger(globalSettings); - Jobs.JobsHostedService.AddJobsServices(services); + Jobs.JobsHostedService.AddJobsServices(services, globalSettings.SelfHosted); services.AddHostedService(); - if (globalSettings.SelfHosted) - { - // Jobs service - Jobs.JobsHostedService.AddJobsServices(services); - services.AddHostedService(); - } if (CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ConnectionString) && CoreHelpers.SettingHasValue(globalSettings.ServiceBus.ApplicationCacheTopicName)) { diff --git a/src/Billing/Controllers/StripeController.cs b/src/Billing/Controllers/StripeController.cs index 07a11f716..ac5dce95a 100644 --- a/src/Billing/Controllers/StripeController.cs +++ b/src/Billing/Controllers/StripeController.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Business; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -30,7 +31,8 @@ namespace Bit.Billing.Controllers private readonly BillingSettings _billingSettings; private readonly IWebHostEnvironment _hostingEnvironment; private readonly IOrganizationService _organizationService; - private readonly IOrganizationSponsorshipService _organizationSponsorshipService; + private readonly IValidateSponsorshipCommand _validateSponsorshipCommand; + private readonly IOrganizationSponsorshipRenewCommand _organizationSponsorshipRenewCommand; private readonly IOrganizationRepository _organizationRepository; private readonly ITransactionRepository _transactionRepository; private readonly IUserService _userService; @@ -47,7 +49,8 @@ namespace Bit.Billing.Controllers IOptions billingSettings, IWebHostEnvironment hostingEnvironment, IOrganizationService organizationService, - IOrganizationSponsorshipService organizationSponsorshipService, + IValidateSponsorshipCommand validateSponsorshipCommand, + IOrganizationSponsorshipRenewCommand organizationSponsorshipRenewCommand, IOrganizationRepository organizationRepository, ITransactionRepository transactionRepository, IUserService userService, @@ -61,7 +64,8 @@ namespace Bit.Billing.Controllers _billingSettings = billingSettings?.Value; _hostingEnvironment = hostingEnvironment; _organizationService = organizationService; - _organizationSponsorshipService = organizationSponsorshipService; + _validateSponsorshipCommand = validateSponsorshipCommand; + _organizationSponsorshipRenewCommand = organizationSponsorshipRenewCommand; _organizationRepository = organizationRepository; _transactionRepository = transactionRepository; _userService = userService; @@ -142,6 +146,10 @@ namespace Bit.Billing.Controllers { await _organizationService.UpdateExpirationDateAsync(ids.Item1.Value, subscription.CurrentPeriodEnd); + if (IsSponsoredSubscription(subscription)) + { + await _organizationSponsorshipRenewCommand.UpdateExpirationDateAsync(ids.Item1.Value, subscription.CurrentPeriodEnd); + } } // user else if (ids.Item2.HasValue) @@ -169,10 +177,9 @@ namespace Bit.Billing.Controllers if (ids.Item1.HasValue) { // sponsored org - if (CheckSponsoredSubscription(subscription)) + if (IsSponsoredSubscription(subscription)) { - await _organizationSponsorshipService - .ValidateSponsorshipAsync(ids.Item1.Value); + await _validateSponsorshipCommand.ValidateSponsorshipAsync(ids.Item1.Value); } var org = await _organizationRepository.GetByIdAsync(ids.Item1.Value); @@ -795,7 +802,7 @@ namespace Bit.Billing.Controllers return subscription; } - private static bool CheckSponsoredSubscription(Subscription subscription) => + private static bool IsSponsoredSubscription(Subscription subscription) => StaticStore.SponsoredPlans.Any(p => p.StripePlanId == subscription.Id); } } diff --git a/src/Billing/Startup.cs b/src/Billing/Startup.cs index 7ad1070e6..bfdb1d5bb 100644 --- a/src/Billing/Startup.cs +++ b/src/Billing/Startup.cs @@ -58,7 +58,7 @@ namespace Bit.Billing //services.AddPasswordlessIdentityServices(globalSettings); // Services - services.AddBaseServices(); + services.AddBaseServices(globalSettings); services.AddDefaultServices(globalSettings); services.TryAddSingleton(); diff --git a/src/Core/Entities/Event.cs b/src/Core/Entities/Event.cs index 8c0dc02a4..1288b0438 100644 --- a/src/Core/Entities/Event.cs +++ b/src/Core/Entities/Event.cs @@ -22,6 +22,7 @@ namespace Bit.Core.Entities PolicyId = e.PolicyId; GroupId = e.GroupId; OrganizationUserId = e.OrganizationUserId; + InstallationId = e.InstallationId; ProviderUserId = e.ProviderUserId; ProviderOrganizationId = e.ProviderOrganizationId; DeviceType = e.DeviceType; @@ -34,6 +35,7 @@ namespace Bit.Core.Entities public EventType Type { get; set; } public Guid? UserId { get; set; } public Guid? OrganizationId { get; set; } + public Guid? InstallationId { get; set; } public Guid? ProviderId { get; set; } public Guid? CipherId { get; set; } public Guid? CollectionId { get; set; } diff --git a/src/Core/Entities/Organization.cs b/src/Core/Entities/Organization.cs index 9b9ae40d1..dd19fd008 100644 --- a/src/Core/Entities/Organization.cs +++ b/src/Core/Entities/Organization.cs @@ -60,8 +60,6 @@ namespace Bit.Core.Entities public bool Enabled { get; set; } = true; [MaxLength(100)] public string LicenseKey { get; set; } - [MaxLength(30)] - public string ApiKey { get; set; } public string PublicKey { get; set; } public string PrivateKey { get; set; } public string TwoFactorProviders { get; set; } diff --git a/src/Core/Entities/OrganizationApiKey.cs b/src/Core/Entities/OrganizationApiKey.cs new file mode 100644 index 000000000..0eb5b527a --- /dev/null +++ b/src/Core/Entities/OrganizationApiKey.cs @@ -0,0 +1,22 @@ +using System; +using System.ComponentModel.DataAnnotations; +using Bit.Core.Enums; +using Bit.Core.Utilities; + +namespace Bit.Core.Entities +{ + public class OrganizationApiKey : ITableObject + { + public Guid Id { get; set; } + public Guid OrganizationId { get; set; } + public OrganizationApiKeyType Type { get; set; } + [MaxLength(30)] + public string ApiKey { get; set; } + public DateTime RevisionDate { get; set; } + + public void SetNewId() + { + Id = CoreHelpers.GenerateComb(); + } + } +} diff --git a/src/Core/Entities/OrganizationConnection.cs b/src/Core/Entities/OrganizationConnection.cs new file mode 100644 index 000000000..9e79e82de --- /dev/null +++ b/src/Core/Entities/OrganizationConnection.cs @@ -0,0 +1,47 @@ +using System; +using System.Text.Json; +using Bit.Core.Enums; +using Bit.Core.Utilities; + +namespace Bit.Core.Entities +{ + public class OrganizationConnection : OrganizationConnection where T : new() + { + public new T Config + { + get => base.GetConfig(); + set => base.SetConfig(value); + } + } + + public class OrganizationConnection : ITableObject + { + public Guid Id { get; set; } + public OrganizationConnectionType Type { get; set; } + public Guid OrganizationId { get; set; } + public bool Enabled { get; set; } + public string Config { get; set; } + + public void SetNewId() + { + Id = CoreHelpers.GenerateComb(); + } + + public T GetConfig() where T : new() + { + try + { + return JsonSerializer.Deserialize(Config); + } + catch (JsonException) + { + return default; + } + } + + public void SetConfig(T config) where T : new() + { + Config = JsonSerializer.Serialize(config); + } + } +} diff --git a/src/Core/Entities/OrganizationSponsorship.cs b/src/Core/Entities/OrganizationSponsorship.cs index 2dc67fba7..c975feaf6 100644 --- a/src/Core/Entities/OrganizationSponsorship.cs +++ b/src/Core/Entities/OrganizationSponsorship.cs @@ -8,20 +8,17 @@ namespace Bit.Core.Entities public class OrganizationSponsorship : ITableObject { public Guid Id { get; set; } - public Guid? InstallationId { get; set; } public Guid? SponsoringOrganizationId { get; set; } - public Guid? SponsoringOrganizationUserId { get; set; } + public Guid SponsoringOrganizationUserId { get; set; } public Guid? SponsoredOrganizationId { get; set; } [MaxLength(256)] public string FriendlyName { get; set; } [MaxLength(256)] public string OfferedToEmail { get; set; } public PlanSponsorshipType? PlanSponsorshipType { get; set; } - [Required] - public bool CloudSponsor { get; set; } public DateTime? LastSyncDate { get; set; } - public byte TimesRenewedWithoutValidation { get; set; } - public DateTime? SponsorshipLapsedDate { get; set; } + public DateTime? ValidUntil { get; set; } + public bool ToDelete { get; set; } public void SetNewId() { diff --git a/src/Core/Enums/EventType.cs b/src/Core/Enums/EventType.cs index 840229038..a873772fe 100644 --- a/src/Core/Enums/EventType.cs +++ b/src/Core/Enums/EventType.cs @@ -60,6 +60,7 @@ Organization_DisabledSso = 1605, Organization_EnabledKeyConnector = 1606, Organization_DisabledKeyConnector = 1607, + Organization_SponsorshipsSynced = 1608, Policy_Updated = 1700, diff --git a/src/Core/Enums/OrganizationApiKeyType.cs b/src/Core/Enums/OrganizationApiKeyType.cs new file mode 100644 index 000000000..fe294a7d2 --- /dev/null +++ b/src/Core/Enums/OrganizationApiKeyType.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Enums +{ + public enum OrganizationApiKeyType : byte + { + Default, + BillingSync, + } +} diff --git a/src/Core/Enums/OrganizationConnectionType.cs b/src/Core/Enums/OrganizationConnectionType.cs new file mode 100644 index 000000000..bfa9a2215 --- /dev/null +++ b/src/Core/Enums/OrganizationConnectionType.cs @@ -0,0 +1,7 @@ +namespace Bit.Core.Enums +{ + public enum OrganizationConnectionType : byte + { + CloudBillingSync = 1, + } +} diff --git a/src/Core/IdentityServer/ApiResources.cs b/src/Core/IdentityServer/ApiResources.cs index 8c38104d0..669f6bbb0 100644 --- a/src/Core/IdentityServer/ApiResources.cs +++ b/src/Core/IdentityServer/ApiResources.cs @@ -30,6 +30,7 @@ namespace Bit.Core.IdentityServer new ApiResource("api.licensing", new string[] { JwtClaimTypes.Subject }), new ApiResource("api.organization", new string[] { JwtClaimTypes.Subject }), new ApiResource("api.provider", new string[] { JwtClaimTypes.Subject }), + new ApiResource("api.installation", new string[] { JwtClaimTypes.Subject }), }; } } diff --git a/src/Core/IdentityServer/ApiScopes.cs b/src/Core/IdentityServer/ApiScopes.cs index cea3023ca..1faadb00a 100644 --- a/src/Core/IdentityServer/ApiScopes.cs +++ b/src/Core/IdentityServer/ApiScopes.cs @@ -13,6 +13,7 @@ namespace Bit.Core.IdentityServer new ApiScope("api.push", "API Push Access"), new ApiScope("api.licensing", "API Licensing Access"), new ApiScope("api.organization", "API Organization Access"), + new ApiScope("api.installation", "API Installation Access"), new ApiScope("internal", "Internal Access") }; } diff --git a/src/Core/IdentityServer/BaseRequestValidator.cs b/src/Core/IdentityServer/BaseRequestValidator.cs index d9d544c11..3daa7f987 100644 --- a/src/Core/IdentityServer/BaseRequestValidator.cs +++ b/src/Core/IdentityServer/BaseRequestValidator.cs @@ -12,8 +12,7 @@ using Bit.Core.Enums; using Bit.Core.Identity; using Bit.Core.Models; using Bit.Core.Models.Api; -using Bit.Core.Models.Business; -using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; diff --git a/src/Core/IdentityServer/ClientStore.cs b/src/Core/IdentityServer/ClientStore.cs index 5b3ae8c40..7796ea46d 100644 --- a/src/Core/IdentityServer/ClientStore.cs +++ b/src/Core/IdentityServer/ClientStore.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Security.Claims; using System.Threading.Tasks; using Bit.Core.Context; +using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -27,6 +28,7 @@ namespace Bit.Core.IdentityServer private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IProviderUserRepository _providerUserRepository; private readonly IProviderOrganizationRepository _providerOrganizationRepository; + private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; public ClientStore( IInstallationRepository installationRepository, @@ -38,7 +40,8 @@ namespace Bit.Core.IdentityServer ICurrentContext currentContext, IOrganizationUserRepository organizationUserRepository, IProviderUserRepository providerUserRepository, - IProviderOrganizationRepository providerOrganizationRepository) + IProviderOrganizationRepository providerOrganizationRepository, + IOrganizationApiKeyRepository organizationApiKeyRepository) { _installationRepository = installationRepository; _organizationRepository = organizationRepository; @@ -50,6 +53,7 @@ namespace Bit.Core.IdentityServer _organizationUserRepository = organizationUserRepository; _providerUserRepository = providerUserRepository; _providerOrganizationRepository = providerOrganizationRepository; + _organizationApiKeyRepository = organizationApiKeyRepository; } public async Task FindClientByIdAsync(string clientId) @@ -67,7 +71,7 @@ namespace Bit.Core.IdentityServer ClientId = $"installation.{installation.Id}", RequireClientSecret = true, ClientSecrets = { new Secret(installation.Key.Sha256()) }, - AllowedScopes = new string[] { "api.push", "api.licensing" }, + AllowedScopes = new string[] { "api.push", "api.licensing", "api.installation" }, AllowedGrantTypes = GrantTypes.ClientCredentials, AccessTokenLifetime = 3600 * 24, Enabled = installation.Enabled, @@ -113,11 +117,14 @@ namespace Bit.Core.IdentityServer var org = await _organizationRepository.GetByIdAsync(id); if (org != null) { + var orgApiKey = (await _organizationApiKeyRepository + .GetManyByOrganizationIdTypeAsync(org.Id, OrganizationApiKeyType.Default)) + .First(); return new Client { ClientId = $"organization.{org.Id}", RequireClientSecret = true, - ClientSecrets = { new Secret(org.ApiKey.Sha256()) }, + ClientSecrets = { new Secret(orgApiKey.ApiKey.Sha256()) }, AllowedScopes = new string[] { "api.organization" }, AllowedGrantTypes = GrantTypes.ClientCredentials, AccessTokenLifetime = 3600 * 1, diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferExistingAccount.html.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferExistingAccount.html.hbs index 2af8fd42a..17f94c1a8 100644 --- a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferExistingAccount.html.hbs +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferExistingAccount.html.hbs @@ -2,7 +2,7 @@ diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferExistingAccount.text.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferExistingAccount.text.hbs index f11b31596..f457927b2 100644 --- a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferExistingAccount.text.hbs +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferExistingAccount.text.hbs @@ -1,5 +1,5 @@ {{#>BasicTextLayout}} -A Bitwarden account, {{SponsorEmail}}, has sponsored a free Families subscription for you! To activate your complimentary subscription, click the link below. +A Bitwarden organization, {{SponsorOrgName}}, has sponsored a free Families subscription for you! To activate your complimentary subscription, click the link below. {{Url}} {{/BasicTextLayout}} diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferNewAccount.html.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferNewAccount.html.hbs index 78bfdb742..ac2c23305 100644 --- a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferNewAccount.html.hbs +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferNewAccount.html.hbs @@ -2,7 +2,7 @@
- A Bitwarden account, {{SponsorEmail}}, has sponsored a free Families subscription for you! To activate your complimentary subscription, click the link below. + A Bitwarden organization, {{SponsorOrgName}}, has sponsored a free Families subscription for you! To activate your complimentary subscription, click the link below.
diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferNewAccount.text.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferNewAccount.text.hbs index 512c0c13f..20058bc41 100644 --- a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferNewAccount.text.hbs +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseOfferNewAccount.text.hbs @@ -1,5 +1,5 @@ {{#>BasicTextLayout}} -A Bitwarden account, {{SponsorEmail}}, has sponsored a free Families subscription for you! To accept your complimentary subscription, you will need to create an account with this email address. Click the link below. +A Bitwarden organization, {{SponsorOrgName}}, has sponsored a free Families subscription for you! To accept your complimentary subscription, you will need to create an account with this email address. Click the link below. {{Url}} diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipReverting.html.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipReverting.html.hbs index 51741015c..6c8ae3b26 100644 --- a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipReverting.html.hbs +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipReverting.html.hbs @@ -2,7 +2,7 @@
- A Bitwarden account, {{SponsorEmail}}, has sponsored a free Families subscription for you! To accept your complimentary subscription, you will need to create an account with this email address. + A Bitwarden organization, {{SponsorOrgName}}, has sponsored a free Families subscription for you! To accept your complimentary subscription, you will need to create an account with this email address.
- Your Families for Enterprise sponsorship will revert back to your existing payment method at the end of the current billing cycle. + Your Families subscription will remain sponsored until {{ExpirationDate}}. To continue your plan, make sure you have a current payment method for the subscription. Review or update your payment method under Settings in your Families Organization.
diff --git a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipReverting.text.hbs b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipReverting.text.hbs index d711cf9e3..5b3384070 100644 --- a/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipReverting.text.hbs +++ b/src/Core/MailTemplates/Handlebars/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipReverting.text.hbs @@ -1,3 +1,3 @@ {{#>BasicTextLayout}} -Your Families for Enterprise sponsorship will revert back to your existing payment method at the end of the current billing cycle. +Your Families subscription will remain sponsored until {{Date}}. To continue your plan, make sure you have a current payment method for the subscription. Review or update your payment method under Settings in your Families Organization. {{/BasicTextLayout}} diff --git a/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.html.hbs b/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.html.hbs index 14ba3cc1b..99c1ae93e 100644 --- a/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.html.hbs +++ b/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.html.hbs @@ -15,16 +15,6 @@ If you do not wish to join this organization, you can safely ignore this email. - {{#jsonIf OrganizationCanSponsor}} -

- - Did you know? - - Members of {{OrganizationName}} receive a complimentary Families subscription. Learn more at the - following link: https://bitwarden.com/help/article/families-for-enterprise/ -

- {{/jsonIf}} diff --git a/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.text.hbs b/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.text.hbs index 9fbfff10a..ed0ec4261 100644 --- a/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.text.hbs +++ b/src/Core/MailTemplates/Handlebars/OrganizationUserInvited.text.hbs @@ -6,7 +6,4 @@ You have been invited to join the {{OrganizationName}} organization. To accept t This link expires on {{ExpirationDate}}. If you do not wish to join this organization, you can safely ignore this email. -{{#jsonIf OrganizationCanSponsor}} -Did you know? Members of {{OrganizationName}} receive a complimentary Families subscription. Learn more here: https://bitwarden.com/help/article/families-for-enterprise/ -{{/jsonIf}} {{/BasicTextLayout}} diff --git a/src/Core/Models/Api/Request/OrganizationSponsorships/OrganizationSponsorshipRequestModel.cs b/src/Core/Models/Api/Request/OrganizationSponsorships/OrganizationSponsorshipRequestModel.cs new file mode 100644 index 000000000..05a9be10c --- /dev/null +++ b/src/Core/Models/Api/Request/OrganizationSponsorships/OrganizationSponsorshipRequestModel.cs @@ -0,0 +1,57 @@ +using System; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Data.Organizations.OrganizationSponsorships; + +namespace Bit.Core.Models.Api.Request.OrganizationSponsorships +{ + public class OrganizationSponsorshipRequestModel + { + public Guid SponsoringOrganizationUserId { get; set; } + public string FriendlyName { get; set; } + public string OfferedToEmail { get; set; } + public PlanSponsorshipType PlanSponsorshipType { get; set; } + public DateTime? LastSyncDate { get; set; } + public DateTime? ValidUntil { get; set; } + public bool ToDelete { get; set; } + + public OrganizationSponsorshipRequestModel() { } + + public OrganizationSponsorshipRequestModel(OrganizationSponsorshipData sponsorshipData) + { + SponsoringOrganizationUserId = sponsorshipData.SponsoringOrganizationUserId; + FriendlyName = sponsorshipData.FriendlyName; + OfferedToEmail = sponsorshipData.OfferedToEmail; + PlanSponsorshipType = sponsorshipData.PlanSponsorshipType; + LastSyncDate = sponsorshipData.LastSyncDate; + ValidUntil = sponsorshipData.ValidUntil; + ToDelete = sponsorshipData.ToDelete; + } + + public OrganizationSponsorshipRequestModel(OrganizationSponsorship sponsorship) + { + SponsoringOrganizationUserId = sponsorship.SponsoringOrganizationUserId; + FriendlyName = sponsorship.FriendlyName; + OfferedToEmail = sponsorship.OfferedToEmail; + PlanSponsorshipType = sponsorship.PlanSponsorshipType.GetValueOrDefault(); + LastSyncDate = sponsorship.LastSyncDate; + ValidUntil = sponsorship.ValidUntil; + ToDelete = sponsorship.ToDelete; + } + + public OrganizationSponsorshipData ToOrganizationSponsorship() + { + return new OrganizationSponsorshipData + { + SponsoringOrganizationUserId = SponsoringOrganizationUserId, + FriendlyName = FriendlyName, + OfferedToEmail = OfferedToEmail, + PlanSponsorshipType = PlanSponsorshipType, + LastSyncDate = LastSyncDate, + ValidUntil = ValidUntil, + ToDelete = ToDelete, + }; + + } + } +} diff --git a/src/Core/Models/Api/Request/OrganizationSponsorships/OrganizationSponsorshipSyncRequestModel.cs b/src/Core/Models/Api/Request/OrganizationSponsorships/OrganizationSponsorshipSyncRequestModel.cs new file mode 100644 index 000000000..faa649841 --- /dev/null +++ b/src/Core/Models/Api/Request/OrganizationSponsorships/OrganizationSponsorshipSyncRequestModel.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Bit.Core.Models.Data.Organizations.OrganizationSponsorships; + +namespace Bit.Core.Models.Api.Request.OrganizationSponsorships +{ + public class OrganizationSponsorshipSyncRequestModel + { + public string BillingSyncKey { get; set; } + public Guid SponsoringOrganizationCloudId { get; set; } + public IEnumerable SponsorshipsBatch { get; set; } + + public OrganizationSponsorshipSyncRequestModel() { } + + public OrganizationSponsorshipSyncRequestModel(IEnumerable sponsorshipsBatch) + { + SponsorshipsBatch = sponsorshipsBatch; + } + + public OrganizationSponsorshipSyncRequestModel(OrganizationSponsorshipSyncData syncData) + { + if (syncData == null) + { + return; + } + BillingSyncKey = syncData.BillingSyncKey; + SponsoringOrganizationCloudId = syncData.SponsoringOrganizationCloudId; + SponsorshipsBatch = syncData.SponsorshipsBatch.Select(o => new OrganizationSponsorshipRequestModel(o)); + } + + public OrganizationSponsorshipSyncData ToOrganizationSponsorshipSync() + { + return new OrganizationSponsorshipSyncData() + { + BillingSyncKey = BillingSyncKey, + SponsoringOrganizationCloudId = SponsoringOrganizationCloudId, + SponsorshipsBatch = SponsorshipsBatch.Select(o => o.ToOrganizationSponsorship()) + }; + } + + } +} diff --git a/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipResponseModel.cs b/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipResponseModel.cs new file mode 100644 index 000000000..c14a4cd80 --- /dev/null +++ b/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipResponseModel.cs @@ -0,0 +1,50 @@ +using System; +using System.Text.Json.Serialization; +using Bit.Core.Enums; +using Bit.Core.Models.Data.Organizations.OrganizationSponsorships; + +namespace Bit.Core.Models.Api.Response.OrganizationSponsorships +{ + public class OrganizationSponsorshipResponseModel + { + public Guid SponsoringOrganizationUserId { get; set; } + public string FriendlyName { get; set; } + public string OfferedToEmail { get; set; } + public PlanSponsorshipType PlanSponsorshipType { get; set; } + public DateTime? LastSyncDate { get; set; } + public DateTime? ValidUntil { get; set; } + public bool ToDelete { get; set; } + + public bool CloudSponsorshipRemoved { get; set; } + + public OrganizationSponsorshipResponseModel() { } + + public OrganizationSponsorshipResponseModel(OrganizationSponsorshipData sponsorshipData) + { + SponsoringOrganizationUserId = sponsorshipData.SponsoringOrganizationUserId; + FriendlyName = sponsorshipData.FriendlyName; + OfferedToEmail = sponsorshipData.OfferedToEmail; + PlanSponsorshipType = sponsorshipData.PlanSponsorshipType; + LastSyncDate = sponsorshipData.LastSyncDate; + ValidUntil = sponsorshipData.ValidUntil; + ToDelete = sponsorshipData.ToDelete; + CloudSponsorshipRemoved = sponsorshipData.CloudSponsorshipRemoved; + } + + public OrganizationSponsorshipData ToOrganizationSponsorship() + { + return new OrganizationSponsorshipData + { + SponsoringOrganizationUserId = SponsoringOrganizationUserId, + FriendlyName = FriendlyName, + OfferedToEmail = OfferedToEmail, + PlanSponsorshipType = PlanSponsorshipType, + LastSyncDate = LastSyncDate, + ValidUntil = ValidUntil, + ToDelete = ToDelete, + CloudSponsorshipRemoved = CloudSponsorshipRemoved + }; + + } + } +} diff --git a/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipSyncResponseModel.cs b/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipSyncResponseModel.cs new file mode 100644 index 000000000..8878731f2 --- /dev/null +++ b/src/Core/Models/Api/Response/OrganizationSponsorships/OrganizationSponsorshipSyncResponseModel.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; +using System.Linq; +using System.Text.Json.Serialization; +using Bit.Core.Models.Data.Organizations.OrganizationSponsorships; + +namespace Bit.Core.Models.Api.Response.OrganizationSponsorships +{ + public class OrganizationSponsorshipSyncResponseModel + { + public IEnumerable SponsorshipsBatch { get; set; } + + public OrganizationSponsorshipSyncResponseModel() { } + + public OrganizationSponsorshipSyncResponseModel(OrganizationSponsorshipSyncData syncData) + { + if (syncData == null) + { + return; + } + SponsorshipsBatch = syncData.SponsorshipsBatch.Select(o => new OrganizationSponsorshipResponseModel(o)); + + } + + public OrganizationSponsorshipSyncData ToOrganizationSponsorshipSync() + { + return new OrganizationSponsorshipSyncData() + { + SponsorshipsBatch = SponsorshipsBatch.Select(o => o.ToOrganizationSponsorship()) + }; + } + + } +} diff --git a/src/Core/Models/Business/OrganizationLicense.cs b/src/Core/Models/Business/OrganizationLicense.cs index 58424ebdc..2cf830b70 100644 --- a/src/Core/Models/Business/OrganizationLicense.cs +++ b/src/Core/Models/Business/OrganizationLicense.cs @@ -210,7 +210,7 @@ namespace Bit.Core.Models.Business } } - public bool VerifyData(Organization organization, GlobalSettings globalSettings) + public bool VerifyData(Organization organization, IGlobalSettings globalSettings) { if (Issued > DateTime.UtcNow || Expires < DateTime.UtcNow) { diff --git a/src/Core/Models/Business/OrganizationUserInvite.cs b/src/Core/Models/Business/OrganizationUserInvite.cs index 425a2ef76..8ea491157 100644 --- a/src/Core/Models/Business/OrganizationUserInvite.cs +++ b/src/Core/Models/Business/OrganizationUserInvite.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Core.Models.Business { diff --git a/src/Core/Models/Business/Tokenables/OrganizationSponsorshipOfferTokenable.cs b/src/Core/Models/Business/Tokenables/OrganizationSponsorshipOfferTokenable.cs new file mode 100644 index 000000000..206471402 --- /dev/null +++ b/src/Core/Models/Business/Tokenables/OrganizationSponsorshipOfferTokenable.cs @@ -0,0 +1,58 @@ +using System; +using System.Text.Json.Serialization; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Tokens; + +namespace Bit.Core.Models.Business.Tokenables +{ + public class OrganizationSponsorshipOfferTokenable : Tokenable + { + public const string ClearTextPrefix = "BWOrganizationSponsorship_"; + public const string DataProtectorPurpose = "OrganizationSponsorshipDataProtector"; + public const string TokenIdentifier = "OrganizationSponsorshipOfferToken"; + public string Identifier { get; set; } = TokenIdentifier; + public Guid Id { get; set; } + public PlanSponsorshipType SponsorshipType { get; set; } + public string Email { get; set; } + + public override bool Valid => !string.IsNullOrWhiteSpace(Email) && + Identifier == TokenIdentifier && + Id != default; + + + [JsonConstructor] + public OrganizationSponsorshipOfferTokenable() { } + + public OrganizationSponsorshipOfferTokenable(OrganizationSponsorship sponsorship) + { + if (string.IsNullOrWhiteSpace(sponsorship.OfferedToEmail)) + { + throw new ArgumentException("Invalid OrganizationSponsorship to create a token, OfferedToEmail is required", nameof(sponsorship)); + } + Email = sponsorship.OfferedToEmail; + + if (!sponsorship.PlanSponsorshipType.HasValue) + { + throw new ArgumentException("Invalid OrganizationSponsorship to create a token, PlanSponsorshipType is required", nameof(sponsorship)); + } + SponsorshipType = sponsorship.PlanSponsorshipType.Value; + + if (sponsorship.Id == default) + { + throw new ArgumentException("Invalid OrganizationSponsorship to create a token, Id is required", nameof(sponsorship)); + } + Id = sponsorship.Id; + } + + public bool IsValid(OrganizationSponsorship sponsorship, string currentUserEmail) => + sponsorship != null && + sponsorship.PlanSponsorshipType.HasValue && + SponsorshipType == sponsorship.PlanSponsorshipType.Value && + Id == sponsorship.Id && + !string.IsNullOrWhiteSpace(sponsorship.OfferedToEmail) && + Email.Equals(currentUserEmail, StringComparison.InvariantCultureIgnoreCase) && + Email.Equals(sponsorship.OfferedToEmail, StringComparison.InvariantCultureIgnoreCase); + + } +} diff --git a/src/Core/Models/Data/EventMessage.cs b/src/Core/Models/Data/EventMessage.cs index eaae3dfbb..31508a865 100644 --- a/src/Core/Models/Data/EventMessage.cs +++ b/src/Core/Models/Data/EventMessage.cs @@ -1,7 +1,6 @@ using System; using Bit.Core.Context; using Bit.Core.Enums; -using Bit.Core.Settings; namespace Bit.Core.Models.Data { @@ -20,6 +19,7 @@ namespace Bit.Core.Models.Data public EventType Type { get; set; } public Guid? UserId { get; set; } public Guid? OrganizationId { get; set; } + public Guid? InstallationId { get; set; } public Guid? ProviderId { get; set; } public Guid? CipherId { get; set; } public Guid? CollectionId { get; set; } diff --git a/src/Core/Models/Data/EventTableEntity.cs b/src/Core/Models/Data/EventTableEntity.cs index 7fcff2df4..a6a3541a4 100644 --- a/src/Core/Models/Data/EventTableEntity.cs +++ b/src/Core/Models/Data/EventTableEntity.cs @@ -16,6 +16,7 @@ namespace Bit.Core.Models.Data Type = e.Type; UserId = e.UserId; OrganizationId = e.OrganizationId; + InstallationId = e.InstallationId; ProviderId = e.ProviderId; CipherId = e.CipherId; CollectionId = e.CollectionId; @@ -33,6 +34,7 @@ namespace Bit.Core.Models.Data public EventType Type { get; set; } public Guid? UserId { get; set; } public Guid? OrganizationId { get; set; } + public Guid? InstallationId { get; set; } public Guid? ProviderId { get; set; } public Guid? CipherId { get; set; } public Guid? CollectionId { get; set; } diff --git a/src/Core/Models/Data/IEvent.cs b/src/Core/Models/Data/IEvent.cs index d0fc0cab3..400a3bfcc 100644 --- a/src/Core/Models/Data/IEvent.cs +++ b/src/Core/Models/Data/IEvent.cs @@ -8,6 +8,7 @@ namespace Bit.Core.Models.Data EventType Type { get; set; } Guid? UserId { get; set; } Guid? OrganizationId { get; set; } + Guid? InstallationId { get; set; } Guid? ProviderId { get; set; } Guid? CipherId { get; set; } Guid? CollectionId { get; set; } diff --git a/src/Core/Models/Data/OrganizationAbility.cs b/src/Core/Models/Data/Organizations/OrganizationAbility.cs similarity index 96% rename from src/Core/Models/Data/OrganizationAbility.cs rename to src/Core/Models/Data/Organizations/OrganizationAbility.cs index d7423fdc7..d2d12f04c 100644 --- a/src/Core/Models/Data/OrganizationAbility.cs +++ b/src/Core/Models/Data/Organizations/OrganizationAbility.cs @@ -1,7 +1,7 @@ using System; using Bit.Core.Entities; -namespace Bit.Core.Models.Data +namespace Bit.Core.Models.Data.Organizations { public class OrganizationAbility { diff --git a/src/Core/Models/Data/Organizations/OrganizationConnections/OrganizationConnectionData.cs b/src/Core/Models/Data/Organizations/OrganizationConnections/OrganizationConnectionData.cs new file mode 100644 index 000000000..c71f3c1f1 --- /dev/null +++ b/src/Core/Models/Data/Organizations/OrganizationConnections/OrganizationConnectionData.cs @@ -0,0 +1,35 @@ + + +using System; +using Bit.Core.Entities; +using Bit.Core.Enums; + +namespace Bit.Core.Models.Data.Organizations.OrganizationConnections +{ + public class OrganizationConnectionData where T : new() + { + public Guid? Id { get; set; } + public OrganizationConnectionType Type { get; set; } + public Guid OrganizationId { get; set; } + public bool Enabled { get; set; } + public T Config { get; set; } + + public OrganizationConnection ToEntity() + { + var result = new OrganizationConnection() + { + Type = Type, + OrganizationId = OrganizationId, + Enabled = Enabled, + }; + result.SetConfig(Config); + + if (Id.HasValue) + { + result.Id = Id.Value; + } + + return result; + } + } +} diff --git a/src/Core/Models/Data/Organizations/OrganizationSponsorships/OrganizationSponsorshipData.cs b/src/Core/Models/Data/Organizations/OrganizationSponsorships/OrganizationSponsorshipData.cs new file mode 100644 index 000000000..a2236fa30 --- /dev/null +++ b/src/Core/Models/Data/Organizations/OrganizationSponsorships/OrganizationSponsorshipData.cs @@ -0,0 +1,32 @@ +using System; +using Bit.Core.Entities; +using Bit.Core.Enums; + +namespace Bit.Core.Models.Data.Organizations.OrganizationSponsorships +{ + public class OrganizationSponsorshipData + { + public OrganizationSponsorshipData() { } + public OrganizationSponsorshipData(OrganizationSponsorship sponsorship) + { + SponsoringOrganizationUserId = sponsorship.SponsoringOrganizationUserId; + SponsoredOrganizationId = sponsorship.SponsoredOrganizationId; + FriendlyName = sponsorship.FriendlyName; + OfferedToEmail = sponsorship.OfferedToEmail; + PlanSponsorshipType = sponsorship.PlanSponsorshipType.GetValueOrDefault(); + LastSyncDate = sponsorship.LastSyncDate; + ValidUntil = sponsorship.ValidUntil; + ToDelete = sponsorship.ToDelete; + } + public Guid SponsoringOrganizationUserId { get; set; } + public Guid? SponsoredOrganizationId { get; set; } + public string FriendlyName { get; set; } + public string OfferedToEmail { get; set; } + public PlanSponsorshipType PlanSponsorshipType { get; set; } + public DateTime? LastSyncDate { get; set; } + public DateTime? ValidUntil { get; set; } + public bool ToDelete { get; set; } + + public bool CloudSponsorshipRemoved { get; set; } + } +} diff --git a/src/Core/Models/Data/Organizations/OrganizationSponsorships/OrganizationSponsorshipSyncData.cs b/src/Core/Models/Data/Organizations/OrganizationSponsorships/OrganizationSponsorshipSyncData.cs new file mode 100644 index 000000000..3b4570133 --- /dev/null +++ b/src/Core/Models/Data/Organizations/OrganizationSponsorships/OrganizationSponsorshipSyncData.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace Bit.Core.Models.Data.Organizations.OrganizationSponsorships +{ + public class OrganizationSponsorshipSyncData + { + public string BillingSyncKey { get; set; } + public Guid SponsoringOrganizationCloudId { get; set; } + public IEnumerable SponsorshipsBatch { get; set; } + } +} diff --git a/src/Core/Models/Data/OrganizationUserInviteData.cs b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserInviteData.cs similarity index 86% rename from src/Core/Models/Data/OrganizationUserInviteData.cs rename to src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserInviteData.cs index eb66c3088..892c12951 100644 --- a/src/Core/Models/Data/OrganizationUserInviteData.cs +++ b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserInviteData.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using Bit.Core.Enums; -namespace Bit.Core.Models.Data +namespace Bit.Core.Models.Data.Organizations.OrganizationUsers { public class OrganizationUserInviteData { diff --git a/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs similarity index 86% rename from src/Core/Models/Data/OrganizationUserOrganizationDetails.cs rename to src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs index 7adbc274f..2d06a0d2b 100644 --- a/src/Core/Models/Data/OrganizationUserOrganizationDetails.cs +++ b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserOrganizationDetails.cs @@ -1,6 +1,6 @@ using System; -namespace Bit.Core.Models.Data +namespace Bit.Core.Models.Data.Organizations.OrganizationUsers { public class OrganizationUserOrganizationDetails { @@ -37,5 +37,8 @@ namespace Bit.Core.Models.Data public string ProviderName { get; set; } public string FamilySponsorshipFriendlyName { get; set; } public string SsoConfig { get; set; } + public DateTime? FamilySponsorshipLastSyncDate { get; set; } + public DateTime? FamilySponsorshipValidUntil { get; set; } + public bool? FamilySponsorshipToDelete { get; set; } } } diff --git a/src/Core/Models/Data/OrganizationUserPublicKey.cs b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserPublicKey.cs similarity index 76% rename from src/Core/Models/Data/OrganizationUserPublicKey.cs rename to src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserPublicKey.cs index 835784555..f5f5a870d 100644 --- a/src/Core/Models/Data/OrganizationUserPublicKey.cs +++ b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserPublicKey.cs @@ -1,6 +1,6 @@ using System; -namespace Bit.Core.Models.Data +namespace Bit.Core.Models.Data.Organizations.OrganizationUsers { public class OrganizationUserPublicKey { diff --git a/src/Core/Models/Data/OrganizationUserResetPasswordDetails.cs b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserResetPasswordDetails.cs similarity index 94% rename from src/Core/Models/Data/OrganizationUserResetPasswordDetails.cs rename to src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserResetPasswordDetails.cs index ea17ad8bc..5bd0eae17 100644 --- a/src/Core/Models/Data/OrganizationUserResetPasswordDetails.cs +++ b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserResetPasswordDetails.cs @@ -2,7 +2,7 @@ using Bit.Core.Entities; using Bit.Core.Enums; -namespace Bit.Core.Models.Data +namespace Bit.Core.Models.Data.Organizations.OrganizationUsers { public class OrganizationUserResetPasswordDetails { diff --git a/src/Core/Models/Data/OrganizationUserUserDetails.cs b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserUserDetails.cs similarity index 96% rename from src/Core/Models/Data/OrganizationUserUserDetails.cs rename to src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserUserDetails.cs index 49dbe36cb..4e9bf5beb 100644 --- a/src/Core/Models/Data/OrganizationUserUserDetails.cs +++ b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserUserDetails.cs @@ -1,10 +1,9 @@ using System; using System.Collections.Generic; -using System.Text.Json; using Bit.Core.Enums; using Bit.Core.Utilities; -namespace Bit.Core.Models.Data +namespace Bit.Core.Models.Data.Organizations.OrganizationUsers { public class OrganizationUserUserDetails : IExternal, ITwoFactorProvidersUser { diff --git a/src/Core/Models/Data/OrganizationUserWithCollections.cs b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserWithCollections.cs similarity index 74% rename from src/Core/Models/Data/OrganizationUserWithCollections.cs rename to src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserWithCollections.cs index b655953e8..c96a49f56 100644 --- a/src/Core/Models/Data/OrganizationUserWithCollections.cs +++ b/src/Core/Models/Data/Organizations/OrganizationUsers/OrganizationUserWithCollections.cs @@ -1,7 +1,7 @@ using System.Data; using Bit.Core.Entities; -namespace Bit.Core.Models.Data +namespace Bit.Core.Models.Data.Organizations.OrganizationUsers { public class OrganizationUserWithCollections : OrganizationUser { diff --git a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseOfferViewModel.cs b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseOfferViewModel.cs index 9a30d98d1..97f028253 100644 --- a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseOfferViewModel.cs +++ b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseOfferViewModel.cs @@ -2,7 +2,7 @@ { public class FamiliesForEnterpriseOfferViewModel : BaseMailModel { - public string SponsorEmail { get; set; } + public string SponsorOrgName { get; set; } public string SponsoredEmail { get; set; } public string SponsorshipToken { get; set; } public bool ExistingAccount { get; set; } diff --git a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipRevertingViewModel.cs b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipRevertingViewModel.cs index b9de49db8..69d94cee9 100644 --- a/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipRevertingViewModel.cs +++ b/src/Core/Models/Mail/FamiliesForEnterprise/FamiliesForEnterpriseSponsorshipRevertingViewModel.cs @@ -1,7 +1,9 @@ -namespace Bit.Core.Models.Mail.FamiliesForEnterprise +using System; + +namespace Bit.Core.Models.Mail.FamiliesForEnterprise { public class FamiliesForEnterpriseSponsorshipRevertingViewModel : BaseMailModel { - public string OrganizationName { get; set; } + public DateTime ExpirationDate { get; set; } } } diff --git a/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs b/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs index b281e96d3..36509f5bb 100644 --- a/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs +++ b/src/Core/Models/Mail/OrganizationUserInvitedViewModel.cs @@ -11,7 +11,6 @@ namespace Bit.Core.Models.Mail public string OrganizationNameUrlEncoded { get; set; } public string Token { get; set; } public string ExpirationDate { get; set; } - public bool OrganizationCanSponsor { get; set; } public string Url => string.Format("{0}/accept-organization?organizationId={1}&" + "organizationUserId={2}&email={3}&organizationName={4}&token={5}", WebVaultUrl, diff --git a/src/Core/Models/OrganizationConnectionConfigs/BillingSyncConfig.cs b/src/Core/Models/OrganizationConnectionConfigs/BillingSyncConfig.cs new file mode 100644 index 000000000..761bbb8db --- /dev/null +++ b/src/Core/Models/OrganizationConnectionConfigs/BillingSyncConfig.cs @@ -0,0 +1,10 @@ +using System; + +namespace Bit.Core.Models.OrganizationConnectionConfigs +{ + public class BillingSyncConfig + { + public string BillingSyncKey { get; set; } + public Guid CloudOrganizationId { get; set; } + } +} diff --git a/src/Core/Models/StaticStore/SponsoredPlan.cs b/src/Core/Models/StaticStore/SponsoredPlan.cs index 7624db1e3..92a7bfd15 100644 --- a/src/Core/Models/StaticStore/SponsoredPlan.cs +++ b/src/Core/Models/StaticStore/SponsoredPlan.cs @@ -1,6 +1,6 @@ using System; using Bit.Core.Enums; -using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Core.Models.StaticStore { diff --git a/src/Core/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyCommand.cs b/src/Core/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyCommand.cs new file mode 100644 index 000000000..a45aeb429 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyCommand.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Utilities; + +namespace Bit.Core.OrganizationFeatures.OrganizationApiKeys +{ + public class GetOrganizationApiKeyCommand : IGetOrganizationApiKeyCommand + { + private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; + + public GetOrganizationApiKeyCommand(IOrganizationApiKeyRepository organizationApiKeyRepository) + { + _organizationApiKeyRepository = organizationApiKeyRepository; + } + + public async Task GetOrganizationApiKeyAsync(Guid organizationId, OrganizationApiKeyType organizationApiKeyType) + { + if (!Enum.IsDefined(organizationApiKeyType)) + { + throw new ArgumentOutOfRangeException(nameof(organizationApiKeyType), $"Invalid value for enum {nameof(OrganizationApiKeyType)}"); + } + + var apiKeys = await _organizationApiKeyRepository + .GetManyByOrganizationIdTypeAsync(organizationId, organizationApiKeyType); + + if (apiKeys == null || !apiKeys.Any()) + { + var apiKey = new OrganizationApiKey + { + OrganizationId = organizationId, + Type = organizationApiKeyType, + ApiKey = CoreHelpers.SecureRandomString(30), + RevisionDate = DateTime.UtcNow, + }; + + await _organizationApiKeyRepository.CreateAsync(apiKey); + return apiKey; + } + + // NOTE: Currently we only allow one type of api key per organization + return apiKeys.Single(); + } + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationApiKeys/Interfaces/IGetOrganizationApiKeyCommand.cs b/src/Core/OrganizationFeatures/OrganizationApiKeys/Interfaces/IGetOrganizationApiKeyCommand.cs new file mode 100644 index 000000000..9ba0eab3d --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationApiKeys/Interfaces/IGetOrganizationApiKeyCommand.cs @@ -0,0 +1,12 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Enums; + +namespace Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces +{ + public interface IGetOrganizationApiKeyCommand + { + Task GetOrganizationApiKeyAsync(Guid organizationId, OrganizationApiKeyType organizationApiKeyType); + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationApiKeys/Interfaces/IRotateOrganizationApiKeyCommand.cs b/src/Core/OrganizationFeatures/OrganizationApiKeys/Interfaces/IRotateOrganizationApiKeyCommand.cs new file mode 100644 index 000000000..1667b05c9 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationApiKeys/Interfaces/IRotateOrganizationApiKeyCommand.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Bit.Core.Entities; + +namespace Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces +{ + public interface IRotateOrganizationApiKeyCommand + { + Task RotateApiKeyAsync(OrganizationApiKey organizationApiKey); + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationApiKeys/RotateOrganizationApiKeyCommand.cs b/src/Core/OrganizationFeatures/OrganizationApiKeys/RotateOrganizationApiKeyCommand.cs new file mode 100644 index 000000000..de1ae62e6 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationApiKeys/RotateOrganizationApiKeyCommand.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Utilities; + +namespace Bit.Core.OrganizationFeatures.OrganizationApiKeys +{ + public class RotateOrganizationApiKeyCommand : IRotateOrganizationApiKeyCommand + { + private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; + + public RotateOrganizationApiKeyCommand(IOrganizationApiKeyRepository organizationApiKeyRepository) + { + _organizationApiKeyRepository = organizationApiKeyRepository; + } + + public async Task RotateApiKeyAsync(OrganizationApiKey organizationApiKey) + { + organizationApiKey.ApiKey = CoreHelpers.SecureRandomString(30); + organizationApiKey.RevisionDate = DateTime.UtcNow; + await _organizationApiKeyRepository.UpsertAsync(organizationApiKey); + return organizationApiKey; + } + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationConnections/CreateOrganizationConnectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationConnections/CreateOrganizationConnectionCommand.cs new file mode 100644 index 000000000..655df2007 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationConnections/CreateOrganizationConnectionCommand.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Models.Data.Organizations.OrganizationConnections; +using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces; +using Bit.Core.Repositories; + +namespace Bit.Core.OrganizationFeatures.OrganizationConnections +{ + public class CreateOrganizationConnectionCommand : ICreateOrganizationConnectionCommand + { + private readonly IOrganizationConnectionRepository _organizationConnectionRepository; + + public CreateOrganizationConnectionCommand(IOrganizationConnectionRepository organizationConnectionRepository) + { + _organizationConnectionRepository = organizationConnectionRepository; + } + + public async Task CreateAsync(OrganizationConnectionData connectionData) where T : new() + { + return await _organizationConnectionRepository.CreateAsync(connectionData.ToEntity()); + } + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationConnections/DeleteOrganizationConnectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationConnections/DeleteOrganizationConnectionCommand.cs new file mode 100644 index 000000000..86196987d --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationConnections/DeleteOrganizationConnectionCommand.cs @@ -0,0 +1,22 @@ +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces; +using Bit.Core.Repositories; + +namespace Bit.Core.OrganizationFeatures.OrganizationConnections +{ + public class DeleteOrganizationConnectionCommand : IDeleteOrganizationConnectionCommand + { + private readonly IOrganizationConnectionRepository _organizationConnectionRepository; + + public DeleteOrganizationConnectionCommand(IOrganizationConnectionRepository organizationConnectionRepository) + { + _organizationConnectionRepository = organizationConnectionRepository; + } + + public async Task DeleteAsync(OrganizationConnection connection) + { + await _organizationConnectionRepository.DeleteAsync(connection); + } + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/ICreateOrganizationConnectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/ICreateOrganizationConnectionCommand.cs new file mode 100644 index 000000000..42f81049b --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/ICreateOrganizationConnectionCommand.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Models.Data.Organizations.OrganizationConnections; + +namespace Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces +{ + public interface ICreateOrganizationConnectionCommand + { + Task CreateAsync(OrganizationConnectionData connectionData) where T : new(); + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/IDeleteOrganizationConnectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/IDeleteOrganizationConnectionCommand.cs new file mode 100644 index 000000000..47d85bed4 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/IDeleteOrganizationConnectionCommand.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Bit.Core.Entities; + +namespace Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces +{ + public interface IDeleteOrganizationConnectionCommand + { + Task DeleteAsync(OrganizationConnection connection); + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/IUpdateOrganizationConnectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/IUpdateOrganizationConnectionCommand.cs new file mode 100644 index 000000000..0f94c1e7a --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationConnections/Interfaces/IUpdateOrganizationConnectionCommand.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Models.Data.Organizations.OrganizationConnections; + +namespace Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces +{ + public interface IUpdateOrganizationConnectionCommand + { + Task UpdateAsync(OrganizationConnectionData connectionData) where T : new(); + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationConnections/UpdateOrganizationConnectionCommand.cs b/src/Core/OrganizationFeatures/OrganizationConnections/UpdateOrganizationConnectionCommand.cs new file mode 100644 index 000000000..9a13cd55c --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationConnections/UpdateOrganizationConnectionCommand.cs @@ -0,0 +1,39 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data.Organizations.OrganizationConnections; +using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces; +using Bit.Core.Repositories; + +namespace Bit.Core.OrganizationFeatures.OrganizationConnections +{ + public class UpdateOrganizationConnectionCommand : IUpdateOrganizationConnectionCommand + { + private readonly IOrganizationConnectionRepository _organizationConnectionRepository; + + public UpdateOrganizationConnectionCommand(IOrganizationConnectionRepository organizationConnectionRepository) + { + _organizationConnectionRepository = organizationConnectionRepository; + } + + public async Task UpdateAsync(OrganizationConnectionData connectionData) where T : new() + { + if (!connectionData.Id.HasValue) + { + throw new Exception("Cannot update connection, Connection does not exist."); + } + + var connection = await _organizationConnectionRepository.GetByIdAsync(connectionData.Id.Value); + + if (connection == null) + { + throw new NotFoundException(); + } + + var entity = connectionData.ToEntity(); + await _organizationConnectionRepository.UpsertAsync(entity); + return entity; + } + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs new file mode 100644 index 000000000..94e59ab31 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationServiceCollectionExtensions.cs @@ -0,0 +1,77 @@ +using Bit.Core.Models.Business.Tokenables; +using Bit.Core.OrganizationFeatures.OrganizationApiKeys; +using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces; +using Bit.Core.OrganizationFeatures.OrganizationConnections; +using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Core.Tokens; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Core.OrganizationFeatures +{ + public static class OrganizationServiceCollectionExtensions + { + public static void AddOrganizationServices(this IServiceCollection services, IGlobalSettings globalSettings) + { + services.AddScoped(); + services.AddTokenizers(); + services.AddOrganizationConnectionCommands(); + services.AddOrganizationSponsorshipCommands(globalSettings); + services.AddOrganizationApiKeyCommands(); + } + + private static void AddOrganizationConnectionCommands(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + + private static void AddOrganizationSponsorshipCommands(this IServiceCollection services, IGlobalSettings globalSettings) + { + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + if (globalSettings.SelfHosted) + { + services.AddScoped(); + } + else + { + services.AddScoped(); + } + } + + private static void AddOrganizationApiKeyCommands(this IServiceCollection services) + { + services.AddScoped(); + services.AddScoped(); + } + + private static void AddTokenizers(this IServiceCollection services) + { + services.AddSingleton>(serviceProvider => + new DataProtectorTokenFactory( + OrganizationSponsorshipOfferTokenable.ClearTextPrefix, + OrganizationSponsorshipOfferTokenable.DataProtectorPurpose, + serviceProvider.GetDataProtectionProvider()) + ); + } + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CancelSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CancelSponsorshipCommand.cs new file mode 100644 index 000000000..4900fd088 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CancelSponsorshipCommand.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Repositories; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise +{ + public abstract class CancelSponsorshipCommand + { + protected readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository; + protected readonly IOrganizationRepository _organizationRepository; + + public CancelSponsorshipCommand(IOrganizationSponsorshipRepository organizationSponsorshipRepository, + IOrganizationRepository organizationRepository) + { + _organizationSponsorshipRepository = organizationSponsorshipRepository; + _organizationRepository = organizationRepository; + } + + protected virtual async Task DeleteSponsorshipAsync(OrganizationSponsorship sponsorship = null) + { + if (sponsorship == null) + { + return; + } + + await _organizationSponsorshipRepository.DeleteAsync(sponsorship); + } + + protected async Task MarkToDeleteSponsorshipAsync(OrganizationSponsorship sponsorship) + { + if (sponsorship == null) + { + throw new BadRequestException("The sponsorship you are trying to cancel does not exist"); + } + + sponsorship.ToDelete = true; + await _organizationSponsorshipRepository.UpsertAsync(sponsorship); + } + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudRevokeSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudRevokeSponsorshipCommand.cs new file mode 100644 index 000000000..73ad3876f --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudRevokeSponsorshipCommand.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; +using Bit.Core.Repositories; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud +{ + public class CloudRevokeSponsorshipCommand : CancelSponsorshipCommand, IRevokeSponsorshipCommand + { + public CloudRevokeSponsorshipCommand( + IOrganizationSponsorshipRepository organizationSponsorshipRepository, + IOrganizationRepository organizationRepository) : base(organizationSponsorshipRepository, organizationRepository) + { + } + + public async Task RevokeSponsorshipAsync(OrganizationSponsorship sponsorship) + { + if (sponsorship == null) + { + throw new BadRequestException("You are not currently sponsoring an organization."); + } + + if (sponsorship.SponsoredOrganizationId == null) + { + await base.DeleteSponsorshipAsync(sponsorship); + } + else + { + await MarkToDeleteSponsorshipAsync(sponsorship); + } + } + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudSyncSponsorshipsCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudSyncSponsorshipsCommand.cs new file mode 100644 index 000000000..252ed2803 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudSyncSponsorshipsCommand.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data.Organizations.OrganizationSponsorships; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Utilities; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud +{ + public class CloudSyncSponsorshipsCommand : ICloudSyncSponsorshipsCommand + { + private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository; + private readonly IEventService _eventService; + + public CloudSyncSponsorshipsCommand( + IOrganizationSponsorshipRepository organizationSponsorshipRepository, + IEventService eventService) + { + _organizationSponsorshipRepository = organizationSponsorshipRepository; + _eventService = eventService; + } + + public async Task<(OrganizationSponsorshipSyncData, IEnumerable)> SyncOrganization(Organization sponsoringOrg, IEnumerable sponsorshipsData) + { + if (sponsoringOrg == null) + { + throw new BadRequestException("Failed to sync sponsorship - missing organization."); + } + + var (processedSponsorshipsData, sponsorshipsToEmailOffer) = sponsorshipsData.Any() ? + await DoSyncAsync(sponsoringOrg, sponsorshipsData) : + (sponsorshipsData, Array.Empty()); + + await RecordEvent(sponsoringOrg); + + return (new OrganizationSponsorshipSyncData + { + SponsorshipsBatch = processedSponsorshipsData + }, sponsorshipsToEmailOffer); + } + + private async Task<(IEnumerable data, IEnumerable toOffer)> DoSyncAsync(Organization sponsoringOrg, IEnumerable sponsorshipsData) + { + var existingSponsorshipsDict = (await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(sponsoringOrg.Id)) + .ToDictionary(i => i.SponsoringOrganizationUserId); + + var sponsorshipsToUpsert = new List(); + var sponsorshipIdsToDelete = new List(); + var sponsorshipsToReturn = new List(); + + foreach (var selfHostedSponsorship in sponsorshipsData) + { + var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(selfHostedSponsorship.PlanSponsorshipType)?.SponsoringProductType; + if (requiredSponsoringProductType == null + || StaticStore.GetPlan(sponsoringOrg.PlanType).Product != requiredSponsoringProductType.Value) + { + continue; // prevent unsupported sponsorships + } + + if (!existingSponsorshipsDict.TryGetValue(selfHostedSponsorship.SponsoringOrganizationUserId, out var cloudSponsorship)) + { + if (selfHostedSponsorship.ToDelete && selfHostedSponsorship.LastSyncDate == null) + { + continue; // prevent invalid sponsorships in cloud. These should have been deleted by self hosted + } + if (OrgDisabledForMoreThanGracePeriod(sponsoringOrg)) + { + continue; // prevent new sponsorships from disabled orgs + } + cloudSponsorship = new OrganizationSponsorship + { + SponsoringOrganizationId = sponsoringOrg.Id, + SponsoringOrganizationUserId = selfHostedSponsorship.SponsoringOrganizationUserId, + FriendlyName = selfHostedSponsorship.FriendlyName, + OfferedToEmail = selfHostedSponsorship.OfferedToEmail, + PlanSponsorshipType = selfHostedSponsorship.PlanSponsorshipType, + LastSyncDate = DateTime.UtcNow, + }; + } + else + { + cloudSponsorship.LastSyncDate = DateTime.UtcNow; + } + + if (selfHostedSponsorship.ToDelete) + { + if (cloudSponsorship.SponsoredOrganizationId == null) + { + sponsorshipIdsToDelete.Add(cloudSponsorship.Id); + selfHostedSponsorship.CloudSponsorshipRemoved = true; + } + else + { + cloudSponsorship.ToDelete = true; + } + } + sponsorshipsToUpsert.Add(cloudSponsorship); + + selfHostedSponsorship.ValidUntil = cloudSponsorship.ValidUntil; + selfHostedSponsorship.LastSyncDate = DateTime.UtcNow; + sponsorshipsToReturn.Add(selfHostedSponsorship); + } + var sponsorshipsToEmailOffer = sponsorshipsToUpsert.Where(s => s.Id == default).ToArray(); + if (sponsorshipsToUpsert.Any()) + { + await _organizationSponsorshipRepository.UpsertManyAsync(sponsorshipsToUpsert); + } + if (sponsorshipIdsToDelete.Any()) + { + await _organizationSponsorshipRepository.DeleteManyAsync(sponsorshipIdsToDelete); + } + + return (sponsorshipsToReturn, sponsorshipsToEmailOffer); + } + + /// + /// True if Organization is disabled and the expiration date is more than three months ago + /// + /// + private bool OrgDisabledForMoreThanGracePeriod(Organization organization) => + !organization.Enabled && + ( + !organization.ExpirationDate.HasValue || + DateTime.UtcNow.Subtract(organization.ExpirationDate.Value).TotalDays > 93 + ); + + private async Task RecordEvent(Organization organization) + { + await _eventService.LogOrganizationEventAsync(organization, EventType.Organization_SponsorshipsSynced); + } + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/OrganizationSponsorshipRenewCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/OrganizationSponsorshipRenewCommand.cs new file mode 100644 index 000000000..0e9dc94ad --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/OrganizationSponsorshipRenewCommand.cs @@ -0,0 +1,30 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; +using Bit.Core.Repositories; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud +{ + public class OrganizationSponsorshipRenewCommand : IOrganizationSponsorshipRenewCommand + { + private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository; + + public OrganizationSponsorshipRenewCommand(IOrganizationSponsorshipRepository organizationSponsorshipRepository) + { + _organizationSponsorshipRepository = organizationSponsorshipRepository; + } + + public async Task UpdateExpirationDateAsync(Guid organizationId, DateTime expireDate) + { + var sponsorship = await _organizationSponsorshipRepository.GetBySponsoredOrganizationIdAsync(organizationId); + + if (sponsorship == null) + { + return; + } + + sponsorship.ValidUntil = expireDate; + await _organizationSponsorshipRepository.UpsertAsync(sponsorship); + } + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/RemoveSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/RemoveSponsorshipCommand.cs new file mode 100644 index 000000000..3cf01ad73 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/RemoveSponsorshipCommand.cs @@ -0,0 +1,27 @@ +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; +using Bit.Core.Repositories; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud +{ + public class RemoveSponsorshipCommand : CancelSponsorshipCommand, IRemoveSponsorshipCommand + { + public RemoveSponsorshipCommand( + IOrganizationSponsorshipRepository organizationSponsorshipRepository, + IOrganizationRepository organizationRepository) : base(organizationSponsorshipRepository, organizationRepository) + { + } + + public async Task RemoveSponsorshipAsync(OrganizationSponsorship sponsorship) + { + if (sponsorship == null || sponsorship.SponsoredOrganizationId == null) + { + throw new BadRequestException("The requested organization is not currently being sponsored."); + } + + await MarkToDeleteSponsorshipAsync(sponsorship); + } + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SendSponsorshipOfferCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SendSponsorshipOfferCommand.cs new file mode 100644 index 000000000..6607bf578 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SendSponsorshipOfferCommand.cs @@ -0,0 +1,72 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Business.Tokenables; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Tokens; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud +{ + public class SendSponsorshipOfferCommand : ISendSponsorshipOfferCommand + { + private readonly IUserRepository _userRepository; + private readonly IMailService _mailService; + private readonly IDataProtectorTokenFactory _tokenFactory; + + public SendSponsorshipOfferCommand(IUserRepository userRepository, + IMailService mailService, + IDataProtectorTokenFactory tokenFactory) + { + _userRepository = userRepository; + _mailService = mailService; + _tokenFactory = tokenFactory; + } + + public async Task BulkSendSponsorshipOfferAsync(string sponsoringOrgName, IEnumerable sponsorships) + { + var invites = new List<(string, bool, string)>(); + foreach (var sponsorship in sponsorships) + { + var user = await _userRepository.GetByEmailAsync(sponsorship.OfferedToEmail); + var isExistingAccount = user != null; + invites.Add((sponsorship.OfferedToEmail, user != null, _tokenFactory.Protect(new OrganizationSponsorshipOfferTokenable(sponsorship)))); + } + + await _mailService.BulkSendFamiliesForEnterpriseOfferEmailAsync(sponsoringOrgName, invites); + } + + public async Task SendSponsorshipOfferAsync(OrganizationSponsorship sponsorship, string sponsoringOrgName) + { + var user = await _userRepository.GetByEmailAsync(sponsorship.OfferedToEmail); + var isExistingAccount = user != null; + + await _mailService.SendFamiliesForEnterpriseOfferEmailAsync(sponsoringOrgName, sponsorship.OfferedToEmail, + isExistingAccount, _tokenFactory.Protect(new OrganizationSponsorshipOfferTokenable(sponsorship))); + } + + public async Task SendSponsorshipOfferAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, + OrganizationSponsorship sponsorship) + { + if (sponsoringOrg == null) + { + throw new BadRequestException("Cannot find the requested sponsoring organization."); + } + + if (sponsoringOrgUser == null || sponsoringOrgUser.Status != OrganizationUserStatusType.Confirmed) + { + throw new BadRequestException("Only confirmed users can sponsor other organizations."); + } + + if (sponsorship == null || sponsorship.OfferedToEmail == null) + { + throw new BadRequestException("Cannot find an outstanding sponsorship offer for this organization."); + } + + await SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name); + } + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SetUpSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SetUpSponsorshipCommand.cs new file mode 100644 index 000000000..4391db605 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SetUpSponsorshipCommand.cs @@ -0,0 +1,69 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Utilities; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud +{ + public class SetUpSponsorshipCommand : ISetUpSponsorshipCommand + { + private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository; + private readonly IOrganizationRepository _organizationRepository; + private readonly IPaymentService _paymentService; + + public SetUpSponsorshipCommand(IOrganizationSponsorshipRepository organizationSponsorshipRepository, IOrganizationRepository organizationRepository, IPaymentService paymentService) + { + _organizationSponsorshipRepository = organizationSponsorshipRepository; + _organizationRepository = organizationRepository; + _paymentService = paymentService; + } + + public async Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship, + Organization sponsoredOrganization) + { + if (sponsorship == null) + { + throw new BadRequestException("No unredeemed sponsorship offer exists for you."); + } + + var existingOrgSponsorship = await _organizationSponsorshipRepository + .GetBySponsoredOrganizationIdAsync(sponsoredOrganization.Id); + if (existingOrgSponsorship != null) + { + throw new BadRequestException("Cannot redeem a sponsorship offer for an organization that is already sponsored. Revoke existing sponsorship first."); + } + + if (sponsorship.PlanSponsorshipType == null) + { + throw new BadRequestException("Cannot set up sponsorship without a known sponsorship type."); + } + + // Do not allow self-hosted sponsorships that haven't been synced for > 0.5 year + if (sponsorship.LastSyncDate != null && DateTime.UtcNow.Subtract(sponsorship.LastSyncDate.Value).TotalDays > 182.5) + { + await _organizationSponsorshipRepository.DeleteAsync(sponsorship); + throw new BadRequestException("This sponsorship offer is more than 6 months old and has expired."); + } + + // Check org to sponsor's product type + var requiredSponsoredProductType = StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value)?.SponsoredProductType; + if (requiredSponsoredProductType == null || + sponsoredOrganization == null || + StaticStore.GetPlan(sponsoredOrganization.PlanType).Product != requiredSponsoredProductType.Value) + { + throw new BadRequestException("Can only redeem sponsorship offer on families organizations."); + } + + await _paymentService.SponsorOrganizationAsync(sponsoredOrganization, sponsorship); + await _organizationRepository.UpsertAsync(sponsoredOrganization); + + sponsorship.SponsoredOrganizationId = sponsoredOrganization.Id; + sponsorship.OfferedToEmail = null; + await _organizationSponsorshipRepository.UpsertAsync(sponsorship); + } + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateBillingSyncKeyCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateBillingSyncKeyCommand.cs new file mode 100644 index 000000000..9c0a9e37c --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateBillingSyncKeyCommand.cs @@ -0,0 +1,43 @@ +using System; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; +using Bit.Core.Repositories; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud +{ + public class ValidateBillingSyncKeyCommand : IValidateBillingSyncKeyCommand + { + private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository; + private readonly IOrganizationApiKeyRepository _apiKeyRepository; + + public ValidateBillingSyncKeyCommand( + IOrganizationSponsorshipRepository organizationSponsorshipRepository, + IOrganizationApiKeyRepository organizationApiKeyRepository) + { + _organizationSponsorshipRepository = organizationSponsorshipRepository; + _apiKeyRepository = organizationApiKeyRepository; + } + + public async Task ValidateBillingSyncKeyAsync(Organization organization, string billingSyncKey) + { + if (organization == null) + { + throw new BadRequestException("Invalid organization"); + } + if (string.IsNullOrWhiteSpace(billingSyncKey)) + { + return false; + } + + var orgApiKey = (await _apiKeyRepository.GetManyByOrganizationIdTypeAsync(organization.Id, Enums.OrganizationApiKeyType.BillingSync)).FirstOrDefault(); + if (string.Equals(orgApiKey.ApiKey, billingSyncKey)) + { + return true; + } + return false; + } + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateRedemptionTokenCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateRedemptionTokenCommand.cs new file mode 100644 index 000000000..91d990e36 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateRedemptionTokenCommand.cs @@ -0,0 +1,38 @@ +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Models.Business.Tokenables; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Tokens; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud +{ + public class ValidateRedemptionTokenCommand : IValidateRedemptionTokenCommand + { + private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository; + private readonly IDataProtectorTokenFactory _dataProtectorTokenFactory; + + public ValidateRedemptionTokenCommand(IOrganizationSponsorshipRepository organizationSponsorshipRepository, + IDataProtectorTokenFactory dataProtectorTokenFactory) + { + _organizationSponsorshipRepository = organizationSponsorshipRepository; + _dataProtectorTokenFactory = dataProtectorTokenFactory; + } + + public async Task<(bool valid, OrganizationSponsorship sponsorship)> ValidateRedemptionTokenAsync(string encryptedToken, string sponsoredUserEmail) + { + + if (!_dataProtectorTokenFactory.TryUnprotect(encryptedToken, out var tokenable)) + { + return (false, null); + } + + var sponsorship = await _organizationSponsorshipRepository.GetByIdAsync(tokenable.Id); + if (!tokenable.IsValid(sponsorship, sponsoredUserEmail)) + { + return (false, sponsorship); + } + return (true, sponsorship); + } + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommand.cs new file mode 100644 index 000000000..f4cb6b4df --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommand.cs @@ -0,0 +1,117 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud +{ + public class ValidateSponsorshipCommand : CancelSponsorshipCommand, IValidateSponsorshipCommand + { + private readonly IPaymentService _paymentService; + private readonly IMailService _mailService; + private readonly ILogger _logger; + + public ValidateSponsorshipCommand( + IOrganizationSponsorshipRepository organizationSponsorshipRepository, + IOrganizationRepository organizationRepository, + IPaymentService paymentService, + IMailService mailService, + ILogger logger) : base(organizationSponsorshipRepository, organizationRepository) + { + _paymentService = paymentService; + _mailService = mailService; + _logger = logger; + } + + public async Task ValidateSponsorshipAsync(Guid sponsoredOrganizationId) + { + var sponsoredOrganization = await _organizationRepository.GetByIdAsync(sponsoredOrganizationId); + if (sponsoredOrganization == null) + { + return false; + } + + var existingSponsorship = await _organizationSponsorshipRepository + .GetBySponsoredOrganizationIdAsync(sponsoredOrganizationId); + + if (existingSponsorship == null) + { + await CancelSponsorshipAsync(sponsoredOrganization, null); + return false; + } + + if (existingSponsorship.SponsoringOrganizationId == null || existingSponsorship.SponsoringOrganizationUserId == default || existingSponsorship.PlanSponsorshipType == null) + { + await CancelSponsorshipAsync(sponsoredOrganization, existingSponsorship); + return false; + } + var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(existingSponsorship.PlanSponsorshipType.Value); + + var sponsoringOrganization = await _organizationRepository + .GetByIdAsync(existingSponsorship.SponsoringOrganizationId.Value); + if (sponsoringOrganization == null) + { + await CancelSponsorshipAsync(sponsoredOrganization, existingSponsorship); + return false; + } + + var sponsoringOrgPlan = Utilities.StaticStore.GetPlan(sponsoringOrganization.PlanType); + if (OrgDisabledForMoreThanGracePeriod(sponsoringOrganization) || + sponsoredPlan.SponsoringProductType != sponsoringOrgPlan.Product || + existingSponsorship.ToDelete || + SponsorshipIsSelfHostedOutOfSync(existingSponsorship)) + { + await CancelSponsorshipAsync(sponsoredOrganization, existingSponsorship); + return false; + } + + return true; + } + + protected async Task CancelSponsorshipAsync(Organization sponsoredOrganization, OrganizationSponsorship sponsorship = null) + { + if (sponsoredOrganization != null) + { + await _paymentService.RemoveOrganizationSponsorshipAsync(sponsoredOrganization, sponsorship); + await _organizationRepository.UpsertAsync(sponsoredOrganization); + + try + { + if (sponsorship != null) + { + await _mailService.SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync( + sponsoredOrganization.BillingEmailAddress(), + sponsorship.ValidUntil ?? DateTime.UtcNow.AddDays(15)); + } + } + catch (Exception e) + { + _logger.LogError("Error sending Family sponsorship removed email.", e); + } + } + await base.DeleteSponsorshipAsync(sponsorship); + } + + /// + /// True if Sponsorship is from a self-hosted instance that has failed to sync for more than 6 months + /// + /// + private bool SponsorshipIsSelfHostedOutOfSync(OrganizationSponsorship sponsorship) => + sponsorship.LastSyncDate.HasValue && + DateTime.UtcNow.Subtract(sponsorship.LastSyncDate.Value).TotalDays > 182.5; + + /// + /// True if Organization is disabled and the expiration date is more than three months ago + /// + /// + private bool OrgDisabledForMoreThanGracePeriod(Organization organization) => + !organization.Enabled && + ( + !organization.ExpirationDate.HasValue || + DateTime.UtcNow.Subtract(organization.ExpirationDate.Value).TotalDays > 93 + ); + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs new file mode 100644 index 000000000..01bb68584 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommand.cs @@ -0,0 +1,84 @@ +using System.Threading.Tasks; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Utilities; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise +{ + public class CreateSponsorshipCommand : ICreateSponsorshipCommand + { + private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository; + private readonly IUserService _userService; + + public CreateSponsorshipCommand(IOrganizationSponsorshipRepository organizationSponsorshipRepository, + IUserService userService) + { + _organizationSponsorshipRepository = organizationSponsorshipRepository; + _userService = userService; + } + + public async Task CreateSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, + PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName) + { + var sponsoringUser = await _userService.GetUserByIdAsync(sponsoringOrgUser.UserId.Value); + if (sponsoringUser == null || string.Equals(sponsoringUser.Email, sponsoredEmail, System.StringComparison.InvariantCultureIgnoreCase)) + { + throw new BadRequestException("Cannot offer a Families Organization Sponsorship to yourself. Choose a different email."); + } + + var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(sponsorshipType)?.SponsoringProductType; + if (requiredSponsoringProductType == null || + sponsoringOrg == null || + StaticStore.GetPlan(sponsoringOrg.PlanType).Product != requiredSponsoringProductType.Value) + { + throw new BadRequestException("Specified Organization cannot sponsor other organizations."); + } + + if (sponsoringOrgUser == null || sponsoringOrgUser.Status != OrganizationUserStatusType.Confirmed) + { + throw new BadRequestException("Only confirmed users can sponsor other organizations."); + } + + var existingOrgSponsorship = await _organizationSponsorshipRepository + .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id); + if (existingOrgSponsorship?.SponsoredOrganizationId != null) + { + throw new BadRequestException("Can only sponsor one organization per Organization User."); + } + + var sponsorship = new OrganizationSponsorship + { + SponsoringOrganizationId = sponsoringOrg.Id, + SponsoringOrganizationUserId = sponsoringOrgUser.Id, + FriendlyName = friendlyName, + OfferedToEmail = sponsoredEmail, + PlanSponsorshipType = sponsorshipType, + }; + + if (existingOrgSponsorship != null) + { + // Replace existing invalid offer with our new sponsorship offer + sponsorship.Id = existingOrgSponsorship.Id; + } + + try + { + await _organizationSponsorshipRepository.UpsertAsync(sponsorship); + return sponsorship; + } + catch + { + if (sponsorship.Id != default) + { + await _organizationSponsorshipRepository.DeleteAsync(sponsorship); + } + throw; + } + } + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/ICreateSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/ICreateSponsorshipCommand.cs new file mode 100644 index 000000000..135e8b6c3 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/ICreateSponsorshipCommand.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Enums; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces +{ + public interface ICreateSponsorshipCommand + { + Task CreateSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, + PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName); + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IOrganizationSponsorshipRenewCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IOrganizationSponsorshipRenewCommand.cs new file mode 100644 index 000000000..b91a3698e --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IOrganizationSponsorshipRenewCommand.cs @@ -0,0 +1,10 @@ +using System; +using System.Threading.Tasks; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces +{ + public interface IOrganizationSponsorshipRenewCommand + { + Task UpdateExpirationDateAsync(Guid organizationId, DateTime expireDate); + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IRemoveSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IRemoveSponsorshipCommand.cs new file mode 100644 index 000000000..3e75d2756 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IRemoveSponsorshipCommand.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Bit.Core.Entities; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces +{ + public interface IRemoveSponsorshipCommand + { + Task RemoveSponsorshipAsync(OrganizationSponsorship sponsorship); + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IRevokeSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IRevokeSponsorshipCommand.cs new file mode 100644 index 000000000..dc97edb32 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IRevokeSponsorshipCommand.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Bit.Core.Entities; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces +{ + public interface IRevokeSponsorshipCommand + { + Task RevokeSponsorshipAsync(OrganizationSponsorship sponsorship); + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/ISendSponsorshipOfferCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/ISendSponsorshipOfferCommand.cs new file mode 100644 index 000000000..4a3f5ce30 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/ISendSponsorshipOfferCommand.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Entities; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces +{ + public interface ISendSponsorshipOfferCommand + { + Task BulkSendSponsorshipOfferAsync(string sponsoringOrgName, IEnumerable invites); + Task SendSponsorshipOfferAsync(OrganizationSponsorship sponsorship, string sponsoringOrgName); + Task SendSponsorshipOfferAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, + OrganizationSponsorship sponsorship); + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/ISetUpSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/ISetUpSponsorshipCommand.cs new file mode 100644 index 000000000..93d55e330 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/ISetUpSponsorshipCommand.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; +using Bit.Core.Entities; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces +{ + public interface ISetUpSponsorshipCommand + { + Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship, + Organization sponsoredOrganization); + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/ISyncOrganizationSponsorshipsCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/ISyncOrganizationSponsorshipsCommand.cs new file mode 100644 index 000000000..77033c546 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/ISyncOrganizationSponsorshipsCommand.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Models.Data.Organizations.OrganizationSponsorships; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces +{ + public interface ISelfHostedSyncSponsorshipsCommand + { + Task SyncOrganization(Guid organizationId, Guid cloudOrganizationId, OrganizationConnection billingSyncConnection); + } + + public interface ICloudSyncSponsorshipsCommand + { + Task<(OrganizationSponsorshipSyncData, IEnumerable)> SyncOrganization(Organization sponsoringOrg, IEnumerable sponsorshipsData); + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IValidateBillingSyncKeyCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IValidateBillingSyncKeyCommand.cs new file mode 100644 index 000000000..f411dc1c2 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IValidateBillingSyncKeyCommand.cs @@ -0,0 +1,11 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Entities; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces +{ + public interface IValidateBillingSyncKeyCommand + { + Task ValidateBillingSyncKeyAsync(Organization organization, string billingSyncKey); + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IValidateRedemptionTokenCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IValidateRedemptionTokenCommand.cs new file mode 100644 index 000000000..b8b369efc --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IValidateRedemptionTokenCommand.cs @@ -0,0 +1,10 @@ +using System.Threading.Tasks; +using Bit.Core.Entities; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces +{ + public interface IValidateRedemptionTokenCommand + { + Task<(bool valid, OrganizationSponsorship sponsorship)> ValidateRedemptionTokenAsync(string encryptedToken, string sponsoredUserEmail); + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IValidateSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IValidateSponsorshipCommand.cs new file mode 100644 index 000000000..fd63e679e --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Interfaces/IValidateSponsorshipCommand.cs @@ -0,0 +1,10 @@ +using System; +using System.Threading.Tasks; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces +{ + public interface IValidateSponsorshipCommand + { + Task ValidateSponsorshipAsync(Guid sponsoredOrganizationId); + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedRevokeSponsorshipCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedRevokeSponsorshipCommand.cs new file mode 100644 index 000000000..56703bdc4 --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedRevokeSponsorshipCommand.cs @@ -0,0 +1,34 @@ +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; +using Bit.Core.Repositories; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted +{ + public class SelfHostedRevokeSponsorshipCommand : CancelSponsorshipCommand, IRevokeSponsorshipCommand + { + public SelfHostedRevokeSponsorshipCommand( + IOrganizationSponsorshipRepository organizationSponsorshipRepository, + IOrganizationRepository organizationRepository) : base(organizationSponsorshipRepository, organizationRepository) + { + } + + public async Task RevokeSponsorshipAsync(OrganizationSponsorship sponsorship) + { + if (sponsorship == null) + { + throw new BadRequestException("You are not currently sponsoring an organization."); + } + + if (sponsorship.LastSyncDate == null) + { + await base.DeleteSponsorshipAsync(sponsorship); + } + else + { + await MarkToDeleteSponsorshipAsync(sponsorship); + } + } + } +} diff --git a/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs new file mode 100644 index 000000000..f89c490ca --- /dev/null +++ b/src/Core/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommand.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +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.Models.Data.Organizations.OrganizationSponsorships; +using Bit.Core.Models.OrganizationConnectionConfigs; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Core.Utilities; +using Microsoft.Extensions.Logging; + +namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted +{ + public class SelfHostedSyncSponsorshipsCommand : BaseIdentityClientService, ISelfHostedSyncSponsorshipsCommand + { + private readonly IGlobalSettings _globalSettings; + private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository; + private readonly IOrganizationUserRepository _organizationUserRepository; + private readonly IOrganizationConnectionRepository _organizationConnectionRepository; + + public SelfHostedSyncSponsorshipsCommand( + IHttpClientFactory httpFactory, + IOrganizationSponsorshipRepository organizationSponsorshipRepository, + IOrganizationUserRepository organizationUserRepository, + IOrganizationConnectionRepository organizationConnectionRepository, + IGlobalSettings globalSettings, + ILogger logger) + : base( + httpFactory, + globalSettings.Installation.ApiUri, + globalSettings.Installation.IdentityUri, + "api.installation", + $"installation.{globalSettings.Installation.Id}", + globalSettings.Installation.Key, + logger) + { + _globalSettings = globalSettings; + _organizationUserRepository = organizationUserRepository; + _organizationSponsorshipRepository = organizationSponsorshipRepository; + _organizationConnectionRepository = organizationConnectionRepository; + } + + public async Task SyncOrganization(Guid organizationId, Guid cloudOrganizationId, OrganizationConnection billingSyncConnection) + { + if (!_globalSettings.EnableCloudCommunication) + { + throw new BadRequestException("Failed to sync instance with cloud - Cloud communication is disabled in global settings"); + } + if (!billingSyncConnection.Enabled) + { + 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(); + if (billingSyncConfig == null || string.IsNullOrWhiteSpace(billingSyncConfig.BillingSyncKey)) + { + throw new BadRequestException($"Failed to get Billing Sync Key for organization {organizationId}"); + } + + var organizationSponsorshipsDict = (await _organizationSponsorshipRepository.GetManyBySponsoringOrganizationAsync(organizationId)) + .ToDictionary(i => i.SponsoringOrganizationUserId); + if (!organizationSponsorshipsDict.Any()) + { + _logger.LogInformation($"No existing sponsorships to sync for organization {organizationId}"); + return; + } + var syncedSponsorships = new List(); + + foreach (var orgSponsorshipsBatch in CoreHelpers.Batch(organizationSponsorshipsDict.Values, 1000)) + { + var response = await SendAsync(HttpMethod.Post, "organization/sponsorship/sync", new OrganizationSponsorshipSyncRequestModel + { + BillingSyncKey = billingSyncConfig.BillingSyncKey, + SponsoringOrganizationCloudId = cloudOrganizationId, + SponsorshipsBatch = orgSponsorshipsBatch.Select(s => new OrganizationSponsorshipRequestModel(s)) + }); + + if (response == null) + { + throw new BadRequestException("Organization sync failed"); + } + + syncedSponsorships.AddRange(response.ToOrganizationSponsorshipSync().SponsorshipsBatch); + } + + var sponsorshipsToDelete = syncedSponsorships.Where(s => s.CloudSponsorshipRemoved).Select(i => organizationSponsorshipsDict[i.SponsoringOrganizationUserId].Id); + var sponsorshipsToUpsert = syncedSponsorships.Where(s => !s.CloudSponsorshipRemoved).Select(i => + { + var existingSponsorship = organizationSponsorshipsDict[i.SponsoringOrganizationUserId]; + if (existingSponsorship != null) + { + existingSponsorship.LastSyncDate = i.LastSyncDate; + existingSponsorship.ValidUntil = i.ValidUntil; + existingSponsorship.ToDelete = i.ToDelete; + } + else + { + // shouldn't occur, added in case self hosted loses a sponsorship + existingSponsorship = new OrganizationSponsorship + { + SponsoringOrganizationId = organizationId, + SponsoringOrganizationUserId = i.SponsoringOrganizationUserId, + FriendlyName = i.FriendlyName, + OfferedToEmail = i.OfferedToEmail, + PlanSponsorshipType = i.PlanSponsorshipType, + LastSyncDate = i.LastSyncDate, + ValidUntil = i.ValidUntil, + ToDelete = i.ToDelete + }; + } + return existingSponsorship; + }); + + if (sponsorshipsToDelete.Any()) + { + await _organizationSponsorshipRepository.DeleteManyAsync(sponsorshipsToDelete); + } + if (sponsorshipsToUpsert.Any()) + { + await _organizationSponsorshipRepository.UpsertManyAsync(sponsorshipsToUpsert); + } + } + + } +} diff --git a/src/Core/Repositories/IMaintenanceRepository.cs b/src/Core/Repositories/IMaintenanceRepository.cs index 27f1b5cfc..eb53ff7d7 100644 --- a/src/Core/Repositories/IMaintenanceRepository.cs +++ b/src/Core/Repositories/IMaintenanceRepository.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; namespace Bit.Core.Repositories { @@ -8,5 +9,6 @@ namespace Bit.Core.Repositories Task DisableCipherAutoStatsAsync(); Task RebuildIndexesAsync(); Task DeleteExpiredGrantsAsync(); + Task DeleteExpiredSponsorshipsAsync(DateTime validUntilBeforeDate); } } diff --git a/src/Core/Repositories/IOrganizationApiKeyRepository.cs b/src/Core/Repositories/IOrganizationApiKeyRepository.cs new file mode 100644 index 000000000..23727014d --- /dev/null +++ b/src/Core/Repositories/IOrganizationApiKeyRepository.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Enums; + +namespace Bit.Core.Repositories +{ + public interface IOrganizationApiKeyRepository : IRepository + { + Task> GetManyByOrganizationIdTypeAsync(Guid organizationId, OrganizationApiKeyType? type = null); + } +} diff --git a/src/Core/Repositories/IOrganizationConnectionRepository.cs b/src/Core/Repositories/IOrganizationConnectionRepository.cs new file mode 100644 index 000000000..86fc2868c --- /dev/null +++ b/src/Core/Repositories/IOrganizationConnectionRepository.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Enums; + +namespace Bit.Core.Repositories +{ + public interface IOrganizationConnectionRepository : IRepository + { + Task> GetByOrganizationIdTypeAsync(Guid organizationId, OrganizationConnectionType type); + Task> GetEnabledByOrganizationIdTypeAsync(Guid organizationId, OrganizationConnectionType type); + } +} diff --git a/src/Core/Repositories/IOrganizationRepository.cs b/src/Core/Repositories/IOrganizationRepository.cs index 3535b39f4..298b32d5b 100644 --- a/src/Core/Repositories/IOrganizationRepository.cs +++ b/src/Core/Repositories/IOrganizationRepository.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Bit.Core.Entities; -using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations; namespace Bit.Core.Repositories { diff --git a/src/Core/Repositories/IOrganizationSponsorshipRepository.cs b/src/Core/Repositories/IOrganizationSponsorshipRepository.cs index 3ff151e8b..3d9c4f0ec 100644 --- a/src/Core/Repositories/IOrganizationSponsorshipRepository.cs +++ b/src/Core/Repositories/IOrganizationSponsorshipRepository.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Threading.Tasks; using Bit.Core.Entities; @@ -8,7 +7,13 @@ namespace Bit.Core.Repositories { public interface IOrganizationSponsorshipRepository : IRepository { + Task> CreateManyAsync(IEnumerable organizationSponsorships); + Task ReplaceManyAsync(IEnumerable organizationSponsorships); + Task UpsertManyAsync(IEnumerable organizationSponsorships); + Task DeleteManyAsync(IEnumerable organizationSponsorshipIds); + Task> GetManyBySponsoringOrganizationAsync(Guid sponsoringOrganizationId); Task GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId); Task GetBySponsoredOrganizationIdAsync(Guid sponsoredOrganizationId); + Task GetLatestSyncDateBySponsoringOrganizationIdAsync(Guid sponsoringOrganizationId); } } diff --git a/src/Core/Repositories/IOrganizationUserRepository.cs b/src/Core/Repositories/IOrganizationUserRepository.cs index 12dbc4e96..053440282 100644 --- a/src/Core/Repositories/IOrganizationUserRepository.cs +++ b/src/Core/Repositories/IOrganizationUserRepository.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Core.Repositories { diff --git a/src/Core/Services/IApplicationCacheService.cs b/src/Core/Services/IApplicationCacheService.cs index 72e913e2f..2dc8b64ec 100644 --- a/src/Core/Services/IApplicationCacheService.cs +++ b/src/Core/Services/IApplicationCacheService.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Bit.Core.Entities; using Bit.Core.Entities.Provider; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations; namespace Bit.Core.Services { diff --git a/src/Core/Services/ILicensingService.cs b/src/Core/Services/ILicensingService.cs index 15d6a2fe9..a3c3055d4 100644 --- a/src/Core/Services/ILicensingService.cs +++ b/src/Core/Services/ILicensingService.cs @@ -1,4 +1,5 @@ -using System.Threading.Tasks; +using System; +using System.Threading.Tasks; using Bit.Core.Entities; using Bit.Core.Models.Business; @@ -11,5 +12,8 @@ namespace Bit.Core.Services Task ValidateUserPremiumAsync(User user); bool VerifyLicense(ILicense license); byte[] SignLicense(ILicense license); + Task ReadOrganizationLicenseAsync(Organization organization); + Task ReadOrganizationLicenseAsync(Guid organizationId); + } } diff --git a/src/Core/Services/IMailService.cs b/src/Core/Services/IMailService.cs index 875df10ca..87e2a7403 100644 --- a/src/Core/Services/IMailService.cs +++ b/src/Core/Services/IMailService.cs @@ -19,8 +19,8 @@ namespace Bit.Core.Services Task SendNewDeviceLoginTwoFactorEmailAsync(string email, string token); Task SendNoMasterPasswordHintEmailAsync(string email); Task SendMasterPasswordHintEmailAsync(string email, string hint); - Task SendOrganizationInviteEmailAsync(string organizationName, bool orgCanSponsor, OrganizationUser orgUser, ExpiringToken token); - Task BulkSendOrganizationInviteEmailAsync(string organizationName, bool orgCanSponsor, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites); + Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token); + Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites); Task SendOrganizationMaxSeatLimitReachedEmailAsync(Organization organization, int maxSeatCount, IEnumerable ownerEmails); Task SendOrganizationAutoscaledEmailAsync(Organization organization, int initialSeatCount, IEnumerable ownerEmails); Task SendOrganizationAcceptedEmailAsync(Organization organization, string userIdentifier, IEnumerable adminEmails); @@ -50,9 +50,10 @@ namespace Bit.Core.Services Task SendProviderConfirmedEmailAsync(string providerName, string email); Task SendProviderUserRemoved(string providerName, string email); Task SendUpdatedTempPasswordEmailAsync(string email, string userName); - Task SendFamiliesForEnterpriseOfferEmailAsync(string email, string sponsorEmail, bool existingAccount, string token); + Task SendFamiliesForEnterpriseOfferEmailAsync(string sponsorOrgName, string email, bool existingAccount, string token); + Task BulkSendFamiliesForEnterpriseOfferEmailAsync(string SponsorOrgName, IEnumerable<(string Email, bool ExistingAccount, string Token)> invites); Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail); - Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, string familyOrgName); + Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, DateTime expirationDate); Task SendOTPEmailAsync(string email, string token); Task SendFailedLoginAttemptsEmailAsync(string email, DateTime utcNow, string ip); Task SendFailedTwoFactorAttemptsEmailAsync(string email, DateTime utcNow, string ip); diff --git a/src/Core/Services/IOrganizationService.cs b/src/Core/Services/IOrganizationService.cs index e41b1d011..7d7704924 100644 --- a/src/Core/Services/IOrganizationService.cs +++ b/src/Core/Services/IOrganizationService.cs @@ -58,7 +58,6 @@ namespace Bit.Core.Services Task ImportAsync(Guid organizationId, Guid? importingUserId, IEnumerable groups, IEnumerable newUsers, IEnumerable removeUserExternalIds, bool overwriteExisting); - Task RotateApiKeyAsync(Organization organization); Task DeleteSsoUserAsync(Guid userId, Guid? organizationId); Task UpdateOrganizationKeysAsync(Guid orgId, string publicKey, string privateKey); Task HasConfirmedOwnersExceptAsync(Guid organizationId, IEnumerable organizationUsersId, bool includeProvider = true); diff --git a/src/Core/Services/IOrganizationSponsorshipService.cs b/src/Core/Services/IOrganizationSponsorshipService.cs deleted file mode 100644 index f6244d553..000000000 --- a/src/Core/Services/IOrganizationSponsorshipService.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Threading.Tasks; -using Bit.Core.Entities; -using Bit.Core.Enums; - -namespace Bit.Core.Services -{ - public interface IOrganizationSponsorshipService - { - Task<(bool valid, OrganizationSponsorship sponsorship)> ValidateRedemptionTokenAsync(string encryptedToken, string currentUserEmail); - Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, - PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName, string sponsoringUserEmail); - Task ResendSponsorshipOfferAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, - OrganizationSponsorship sponsorship, string sponsoringUserEmail); - Task SendSponsorshipOfferAsync(OrganizationSponsorship sponsorship, string sponsoringOrgUserEmail); - Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship, - Organization sponsoredOrganization); - Task ValidateSponsorshipAsync(Guid sponsoredOrganizationId); - Task RevokeSponsorshipAsync(Organization sponsoredOrganization, OrganizationSponsorship sponsorship); - Task RemoveSponsorshipAsync(Organization sponsoredOrganization, OrganizationSponsorship sponsorship); - } -} diff --git a/src/Core/Services/Implementations/BaseIdentityClientService.cs b/src/Core/Services/Implementations/BaseIdentityClientService.cs index 34527f2f3..61ed13d01 100644 --- a/src/Core/Services/Implementations/BaseIdentityClientService.cs +++ b/src/Core/Services/Implementations/BaseIdentityClientService.cs @@ -4,7 +4,6 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Net.Http.Json; -using System.Text; using System.Text.Json; using System.Threading.Tasks; using Bit.Core.Utilities; @@ -14,15 +13,17 @@ namespace Bit.Core.Services { public abstract class BaseIdentityClientService : IDisposable { + private readonly IHttpClientFactory _httpFactory; private readonly string _identityScope; private readonly string _identityClientId; private readonly string _identityClientSecret; - private readonly ILogger _logger; + protected readonly ILogger _logger; private JsonDocument _decodedToken; private DateTime? _nextAuthAttempt = null; public BaseIdentityClientService( + IHttpClientFactory httpFactory, string baseClientServerUri, string baseIdentityServerUri, string identityScope, @@ -30,21 +31,18 @@ namespace Bit.Core.Services string identityClientSecret, ILogger logger) { + _httpFactory = httpFactory; _identityScope = identityScope; _identityClientId = identityClientId; _identityClientSecret = identityClientSecret; _logger = logger; - Client = new HttpClient - { - BaseAddress = new Uri(baseClientServerUri) - }; + Client = _httpFactory.CreateClient("client"); + Client.BaseAddress = new Uri(baseClientServerUri); Client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - IdentityClient = new HttpClient - { - BaseAddress = new Uri(baseIdentityServerUri) - }; + IdentityClient = _httpFactory.CreateClient("identity"); + IdentityClient.BaseAddress = new Uri(baseIdentityServerUri); IdentityClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); } @@ -52,12 +50,18 @@ namespace Bit.Core.Services protected HttpClient IdentityClient { get; private set; } protected string AccessToken { get; private set; } - protected async Task SendAsync(HttpMethod method, string path, object requestModel = null) + protected Task SendAsync(HttpMethod method, string path) => + SendAsync(method, path, null); + + protected Task SendAsync(HttpMethod method, string path, TRequest body) => + SendAsync(method, path, body); + + protected async Task SendAsync(HttpMethod method, string path, TRequest requestModel) { var tokenStateResponse = await HandleTokenStateAsync(); if (!tokenStateResponse) { - return; + return default; } var message = new TokenHttpRequestMessage(requestModel, AccessToken) @@ -65,14 +69,15 @@ namespace Bit.Core.Services Method = method, RequestUri = new Uri(string.Concat(Client.BaseAddress, path)) }; - try { var response = await Client.SendAsync(message); + return await response.Content.ReadFromJsonAsync(); } catch (Exception e) { _logger.LogError(12334, e, "Failed to send to {0}.", message.RequestUri.ToString()); + return default; } } @@ -192,7 +197,7 @@ namespace Bit.Core.Services public void Dispose() { - _decodedToken.Dispose(); + _decodedToken?.Dispose(); } } } diff --git a/src/Core/Services/Implementations/EventService.cs b/src/Core/Services/Implementations/EventService.cs index 50ff0b692..bd7f979ea 100644 --- a/src/Core/Services/Implementations/EventService.cs +++ b/src/Core/Services/Implementations/EventService.cs @@ -7,6 +7,7 @@ using Bit.Core.Entities; using Bit.Core.Entities.Provider; using Bit.Core.Enums; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations; using Bit.Core.Repositories; using Bit.Core.Settings; @@ -241,7 +242,8 @@ namespace Bit.Core.Services ProviderId = await GetProviderIdAsync(organization.Id), Type = type, ActingUserId = _currentContext?.UserId, - Date = date.GetValueOrDefault(DateTime.UtcNow) + Date = date.GetValueOrDefault(DateTime.UtcNow), + InstallationId = GetInstallationId(), }; await _eventWriteService.CreateAsync(e); } @@ -305,6 +307,16 @@ namespace Bit.Core.Services return await _currentContext.ProviderIdForOrg(orgId.Value); } + private Guid? GetInstallationId() + { + if (_currentContext == null) + { + return null; + } + + return _currentContext.InstallationId; + } + private bool CanUseEvents(IDictionary orgAbilities, Guid orgId) { return orgAbilities != null && orgAbilities.ContainsKey(orgId) && diff --git a/src/Core/Services/Implementations/HandlebarsMailService.cs b/src/Core/Services/Implementations/HandlebarsMailService.cs index 461bf283a..def82ce61 100644 --- a/src/Core/Services/Implementations/HandlebarsMailService.cs +++ b/src/Core/Services/Implementations/HandlebarsMailService.cs @@ -221,10 +221,10 @@ namespace Bit.Core.Services await _mailDeliveryService.SendEmailAsync(message); } - public Task SendOrganizationInviteEmailAsync(string organizationName, bool orgCanSponsor, OrganizationUser orgUser, ExpiringToken token) => - BulkSendOrganizationInviteEmailAsync(organizationName, orgCanSponsor, new[] { (orgUser, token) }); + public Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token) => + BulkSendOrganizationInviteEmailAsync(organizationName, new[] { (orgUser, token) }); - public async Task BulkSendOrganizationInviteEmailAsync(string organizationName, bool organizationCanSponsor, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites) + public async Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites) { MailQueueMessage CreateMessage(string email, object model) { @@ -244,7 +244,6 @@ namespace Bit.Core.Services OrganizationNameUrlEncoded = WebUtility.UrlEncode(organizationName), WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, SiteName = _globalSettings.SiteName, - OrganizationCanSponsor = organizationCanSponsor, } )); @@ -581,35 +580,6 @@ namespace Bit.Core.Services var clickTrackingText = (clickTrackingOff ? "clicktracking=off" : string.Empty); writer.WriteSafeString($"{text}"); }); - - Handlebars.RegisterHelper("jsonIf", (output, options, context, arguments) => - { - // Special case for JsonElement - if (arguments[0] is JsonElement jsonElement - && (jsonElement.ValueKind == JsonValueKind.True || jsonElement.ValueKind == JsonValueKind.False)) - { - if (jsonElement.GetBoolean()) - { - options.Template(output, context); - } - else - { - options.Inverse(output, context); - } - - return; - } - - // Fallback to normal - if (HandlebarsUtils.IsTruthy(arguments[0])) - { - options.Template(output, context); - } - else - { - options.Inverse(output, context); - } - }); } public async Task SendEmergencyAccessInviteEmailAsync(EmergencyAccess emergencyAccess, string name, string token) @@ -803,27 +773,32 @@ namespace Bit.Core.Services await _mailDeliveryService.SendEmailAsync(message); } - public async Task SendFamiliesForEnterpriseOfferEmailAsync(string email, string sponsorEmail, bool existingAccount, string token) + public async Task SendFamiliesForEnterpriseOfferEmailAsync(string sponsorOrgName, string email, bool existingAccount, string token) => + await BulkSendFamiliesForEnterpriseOfferEmailAsync(sponsorOrgName, new[] { (email, existingAccount, token) }); + + public async Task BulkSendFamiliesForEnterpriseOfferEmailAsync(string sponsorOrgName, IEnumerable<(string Email, bool ExistingAccount, string Token)> invites) { - var message = CreateDefaultMessage("Accept Your Free Families Subscription", email); - - var model = new FamiliesForEnterpriseOfferViewModel + MailQueueMessage CreateMessage((string Email, bool ExistingAccount, string Token) invite) { - SponsorEmail = CoreHelpers.ObfuscateEmail(sponsorEmail), - SponsoredEmail = WebUtility.UrlEncode(email), - ExistingAccount = existingAccount, - WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, - SiteName = _globalSettings.SiteName, - SponsorshipToken = token, - }; - var templateName = existingAccount ? - "FamiliesForEnterprise.FamiliesForEnterpriseOfferExistingAccount" : - "FamiliesForEnterprise.FamiliesForEnterpriseOfferNewAccount"; + var message = CreateDefaultMessage("Accept Your Free Families Subscription", invite.Email); + message.Category = "FamiliesForEnterpriseOffer"; + var model = new FamiliesForEnterpriseOfferViewModel + { + SponsorOrgName = sponsorOrgName, + SponsoredEmail = WebUtility.UrlEncode(invite.Email), + ExistingAccount = invite.ExistingAccount, + WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash, + SiteName = _globalSettings.SiteName, + SponsorshipToken = invite.Token, + }; + var templateName = invite.ExistingAccount ? + "FamiliesForEnterprise.FamiliesForEnterpriseOfferExistingAccount" : + "FamiliesForEnterprise.FamiliesForEnterpriseOfferNewAccount"; - await AddMessageContentAsync(message, templateName, model); - - message.Category = "FamiliesForEnterpriseOffer"; - await _mailDeliveryService.SendEmailAsync(message); + return new MailQueueMessage(message, templateName, model); + } + var messageModels = invites.Select(invite => CreateMessage(invite)); + await EnqueueMailAsync(messageModels); } public async Task SendFamiliesForEnterpriseRedeemedEmailsAsync(string familyUserEmail, string sponsorEmail) @@ -851,12 +826,12 @@ namespace Bit.Core.Services await _mailDeliveryService.SendEmailAsync(message); } - public async Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, string familyOrgName) + public async Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, DateTime expirationDate) { - var message = CreateDefaultMessage($"{familyOrgName} Organization Sponsorship Is No Longer Valid", email); + var message = CreateDefaultMessage("Your Families Sponsorship was Removed", email); var model = new FamiliesForEnterpriseSponsorshipRevertingViewModel { - OrganizationName = CoreHelpers.SanitizeForEmail(familyOrgName, false), + ExpirationDate = expirationDate, }; await AddMessageContentAsync(message, "FamiliesForEnterprise.FamiliesForEnterpriseSponsorshipReverting", model); message.Category = "FamiliesForEnterpriseSponsorshipReverting"; diff --git a/src/Core/Services/Implementations/InMemoryApplicationCacheService.cs b/src/Core/Services/Implementations/InMemoryApplicationCacheService.cs index fc9320e85..b7c50288a 100644 --- a/src/Core/Services/Implementations/InMemoryApplicationCacheService.cs +++ b/src/Core/Services/Implementations/InMemoryApplicationCacheService.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Bit.Core.Entities; using Bit.Core.Entities.Provider; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations; using Bit.Core.Repositories; namespace Bit.Core.Services diff --git a/src/Core/Services/Implementations/LicensingService.cs b/src/Core/Services/Implementations/LicensingService.cs index 6eb17e22c..f085b042a 100644 --- a/src/Core/Services/Implementations/LicensingService.cs +++ b/src/Core/Services/Implementations/LicensingService.cs @@ -20,7 +20,7 @@ namespace Bit.Core.Services public class LicensingService : ILicensingService { private readonly X509Certificate2 _certificate; - private readonly GlobalSettings _globalSettings; + private readonly IGlobalSettings _globalSettings; private readonly IUserRepository _userRepository; private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationUserRepository _organizationUserRepository; @@ -36,7 +36,7 @@ namespace Bit.Core.Services IMailService mailService, IWebHostEnvironment environment, ILogger logger, - GlobalSettings globalSettings) + IGlobalSettings globalSettings) { _userRepository = userRepository; _organizationRepository = organizationRepository; @@ -90,7 +90,7 @@ namespace Bit.Core.Services foreach (var org in enabledOrgs) { - var license = ReadOrganizationLicense(org); + var license = await ReadOrganizationLicenseAsync(org); if (license == null) { await DisableOrganizationAsync(org, null, "No license file."); @@ -249,16 +249,18 @@ namespace Bit.Core.Services return JsonSerializer.Deserialize(data); } - private OrganizationLicense ReadOrganizationLicense(Organization organization) + public Task ReadOrganizationLicenseAsync(Organization organization) => + ReadOrganizationLicenseAsync(organization.Id); + public async Task ReadOrganizationLicenseAsync(Guid organizationId) { - var filePath = $"{_globalSettings.LicenseDirectory}/organization/{organization.Id}.json"; + var filePath = Path.Combine(_globalSettings.LicenseDirectory, "organization", $"{organizationId}.json"); if (!File.Exists(filePath)) { return null; } - var data = File.ReadAllText(filePath, Encoding.UTF8); - return JsonSerializer.Deserialize(data); + using var fs = File.OpenRead(filePath); + return await JsonSerializer.DeserializeAsync(fs); } } } diff --git a/src/Core/Services/Implementations/MultiServicePushNotificationService.cs b/src/Core/Services/Implementations/MultiServicePushNotificationService.cs index 8320e516c..3288b5237 100644 --- a/src/Core/Services/Implementations/MultiServicePushNotificationService.cs +++ b/src/Core/Services/Implementations/MultiServicePushNotificationService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Net.Http; using System.Threading.Tasks; using Bit.Core.Entities; using Bit.Core.Enums; @@ -17,6 +18,7 @@ namespace Bit.Core.Services private readonly ILogger _logger; public MultiServicePushNotificationService( + IHttpClientFactory httpFactory, IDeviceRepository deviceRepository, IInstallationDeviceRepository installationDeviceRepository, GlobalSettings globalSettings, @@ -31,14 +33,14 @@ namespace Bit.Core.Services globalSettings.Installation?.Id != null && CoreHelpers.SettingHasValue(globalSettings.Installation?.Key)) { - _services.Add(new RelayPushNotificationService(deviceRepository, globalSettings, + _services.Add(new RelayPushNotificationService(httpFactory, deviceRepository, globalSettings, httpContextAccessor, relayLogger)); } if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) && CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications)) { _services.Add(new NotificationsApiPushNotificationService( - globalSettings, httpContextAccessor, hubLogger)); + httpFactory, globalSettings, httpContextAccessor, hubLogger)); } } else diff --git a/src/Core/Services/Implementations/NotificationsApiPushNotificationService.cs b/src/Core/Services/Implementations/NotificationsApiPushNotificationService.cs index 518404247..c88f3da97 100644 --- a/src/Core/Services/Implementations/NotificationsApiPushNotificationService.cs +++ b/src/Core/Services/Implementations/NotificationsApiPushNotificationService.cs @@ -18,16 +18,18 @@ namespace Bit.Core.Services private readonly IHttpContextAccessor _httpContextAccessor; public NotificationsApiPushNotificationService( + IHttpClientFactory httpFactory, GlobalSettings globalSettings, IHttpContextAccessor httpContextAccessor, ILogger logger) : base( - globalSettings.BaseServiceUri.InternalNotifications, - globalSettings.BaseServiceUri.InternalIdentity, - "internal", - $"internal.{globalSettings.ProjectName}", - globalSettings.InternalIdentityKey, - logger) + httpFactory, + globalSettings.BaseServiceUri.InternalNotifications, + globalSettings.BaseServiceUri.InternalIdentity, + "internal", + $"internal.{globalSettings.ProjectName}", + globalSettings.InternalIdentityKey, + logger) { _globalSettings = globalSettings; _httpContextAccessor = httpContextAccessor; diff --git a/src/Core/Services/Implementations/OrganizationService.cs b/src/Core/Services/Implementations/OrganizationService.cs index b72c604a2..0d5b8aab2 100644 --- a/src/Core/Services/Implementations/OrganizationService.cs +++ b/src/Core/Services/Implementations/OrganizationService.cs @@ -42,6 +42,7 @@ namespace Bit.Core.Services private readonly IReferenceEventService _referenceEventService; private readonly IGlobalSettings _globalSettings; private readonly ITaxRateRepository _taxRateRepository; + private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; private readonly ICurrentContext _currentContext; private readonly ILogger _logger; @@ -68,6 +69,7 @@ namespace Bit.Core.Services IReferenceEventService referenceEventService, IGlobalSettings globalSettings, ITaxRateRepository taxRateRepository, + IOrganizationApiKeyRepository organizationApiKeyRepository, ICurrentContext currentContext, ILogger logger) { @@ -92,6 +94,7 @@ namespace Bit.Core.Services _referenceEventService = referenceEventService; _globalSettings = globalSettings; _taxRateRepository = taxRateRepository; + _organizationApiKeyRepository = organizationApiKeyRepository; _currentContext = currentContext; _logger = logger; } @@ -611,7 +614,6 @@ namespace Bit.Core.Services ReferenceData = signup.Owner.ReferenceData, Enabled = true, LicenseKey = CoreHelpers.SecureRandomString(20), - ApiKey = CoreHelpers.SecureRandomString(30), PublicKey = signup.PublicKey, PrivateKey = signup.PrivateKey, CreationDate = DateTime.UtcNow, @@ -721,7 +723,6 @@ namespace Bit.Core.Services Enabled = license.Enabled, ExpirationDate = license.Expires, LicenseKey = license.LicenseKey, - ApiKey = CoreHelpers.SecureRandomString(30), PublicKey = publicKey, PrivateKey = privateKey, CreationDate = DateTime.UtcNow, @@ -743,6 +744,13 @@ namespace Bit.Core.Services try { await _organizationRepository.CreateAsync(organization); + await _organizationApiKeyRepository.CreateAsync(new OrganizationApiKey + { + OrganizationId = organization.Id, + ApiKey = CoreHelpers.SecureRandomString(30), + Type = OrganizationApiKeyType.Default, + RevisionDate = DateTime.UtcNow, + }); await _applicationCacheService.UpsertOrganizationAbilityAsync(organization); if (!string.IsNullOrWhiteSpace(collectionName)) @@ -1271,7 +1279,7 @@ namespace Bit.Core.Services string MakeToken(OrganizationUser orgUser) => _dataProtector.Protect($"OrganizationUserInvite {orgUser.Id} {orgUser.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}"); - await _mailService.BulkSendOrganizationInviteEmailAsync(organization.Name, CheckOrganizationCanSponsor(organization), + await _mailService.BulkSendOrganizationInviteEmailAsync(organization.Name, orgUsers.Select(o => (o, new ExpiringToken(MakeToken(o), DateTime.UtcNow.AddDays(5))))); } @@ -1282,14 +1290,7 @@ namespace Bit.Core.Services var token = _dataProtector.Protect( $"OrganizationUserInvite {orgUser.Id} {orgUser.Email} {nowMillis}"); - await _mailService.SendOrganizationInviteEmailAsync(organization.Name, CheckOrganizationCanSponsor(organization), orgUser, new ExpiringToken(token, now.AddDays(5))); - } - - - private bool CheckOrganizationCanSponsor(Organization organization) - { - return StaticStore.GetPlan(organization.PlanType).Product == ProductType.Enterprise - && !_globalSettings.SelfHosted; + await _mailService.SendOrganizationInviteEmailAsync(organization.Name, orgUser, new ExpiringToken(token, now.AddDays(5))); } public async Task AcceptUserAsync(Guid organizationUserId, User user, string token, @@ -2016,13 +2017,6 @@ namespace Bit.Core.Services new ReferenceEvent(ReferenceEventType.DirectorySynced, organization)); } - public async Task RotateApiKeyAsync(Organization organization) - { - organization.ApiKey = CoreHelpers.SecureRandomString(30); - organization.RevisionDate = DateTime.UtcNow; - await ReplaceAndUpdateCache(organization); - } - public async Task DeleteSsoUserAsync(Guid userId, Guid? organizationId) { await _ssoUserRepository.DeleteAsync(userId, organizationId); diff --git a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs b/src/Core/Services/Implementations/OrganizationSponsorshipService.cs deleted file mode 100644 index 65fecc797..000000000 --- a/src/Core/Services/Implementations/OrganizationSponsorshipService.cs +++ /dev/null @@ -1,318 +0,0 @@ -using System; -using System.Threading.Tasks; -using Bit.Core.Entities; -using Bit.Core.Enums; -using Bit.Core.Exceptions; -using Bit.Core.Repositories; -using Bit.Core.Utilities; -using Microsoft.AspNetCore.DataProtection; - -namespace Bit.Core.Services -{ - public class OrganizationSponsorshipService : IOrganizationSponsorshipService - { - private const string FamiliesForEnterpriseTokenName = "FamiliesForEnterpriseToken"; - private const string TokenClearTextPrefix = "BWOrganizationSponsorship_"; - - private readonly IOrganizationSponsorshipRepository _organizationSponsorshipRepository; - private readonly IOrganizationRepository _organizationRepository; - private readonly IUserRepository _userRepository; - private readonly IPaymentService _paymentService; - private readonly IMailService _mailService; - - private readonly IDataProtector _dataProtector; - - public OrganizationSponsorshipService(IOrganizationSponsorshipRepository organizationSponsorshipRepository, - IOrganizationRepository organizationRepository, - IUserRepository userRepository, - IPaymentService paymentService, - IMailService mailService, - IDataProtectionProvider dataProtectionProvider) - { - _organizationSponsorshipRepository = organizationSponsorshipRepository; - _organizationRepository = organizationRepository; - _userRepository = userRepository; - _paymentService = paymentService; - _mailService = mailService; - _dataProtector = dataProtectionProvider.CreateProtector("OrganizationSponsorshipServiceDataProtector"); - } - - public async Task<(bool valid, OrganizationSponsorship sponsorship)> ValidateRedemptionTokenAsync(string encryptedToken, string sponsoredUserEmail) - { - if (!encryptedToken.StartsWith(TokenClearTextPrefix) || sponsoredUserEmail == null) - { - return (false, null); - } - - var decryptedToken = _dataProtector.Unprotect(encryptedToken[TokenClearTextPrefix.Length..]); - var dataParts = decryptedToken.Split(' '); - - if (dataParts.Length != 3) - { - return (false, null); - } - - if (dataParts[0].Equals(FamiliesForEnterpriseTokenName)) - { - if (!Guid.TryParse(dataParts[1], out Guid sponsorshipId) || - !Enum.TryParse(dataParts[2], true, out var sponsorshipType)) - { - return (false, null); - } - - var sponsorship = await _organizationSponsorshipRepository.GetByIdAsync(sponsorshipId); - if (sponsorship == null || - sponsorship.PlanSponsorshipType != sponsorshipType || - sponsorship.OfferedToEmail != sponsoredUserEmail) - { - return (false, sponsorship); - } - - return (true, sponsorship); - } - - return (false, null); - } - - private string RedemptionToken(Guid sponsorshipId, PlanSponsorshipType sponsorshipType) => - string.Concat( - TokenClearTextPrefix, - _dataProtector.Protect($"{FamiliesForEnterpriseTokenName} {sponsorshipId} {sponsorshipType}") - ); - - public async Task OfferSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, - PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName, string sponsoringUserEmail) - { - var requiredSponsoringProductType = StaticStore.GetSponsoredPlan(sponsorshipType)?.SponsoringProductType; - if (requiredSponsoringProductType == null || - sponsoringOrg == null || - StaticStore.GetPlan(sponsoringOrg.PlanType).Product != requiredSponsoringProductType.Value) - { - throw new BadRequestException("Specified Organization cannot sponsor other organizations."); - } - - if (sponsoringOrgUser == null || sponsoringOrgUser.Status != OrganizationUserStatusType.Confirmed) - { - throw new BadRequestException("Only confirmed users can sponsor other organizations."); - } - - var existingOrgSponsorship = await _organizationSponsorshipRepository - .GetBySponsoringOrganizationUserIdAsync(sponsoringOrgUser.Id); - if (existingOrgSponsorship?.SponsoredOrganizationId != null) - { - throw new BadRequestException("Can only sponsor one organization per Organization User."); - } - - var sponsorship = new OrganizationSponsorship - { - SponsoringOrganizationId = sponsoringOrg.Id, - SponsoringOrganizationUserId = sponsoringOrgUser.Id, - FriendlyName = friendlyName, - OfferedToEmail = sponsoredEmail, - PlanSponsorshipType = sponsorshipType, - CloudSponsor = true, - }; - - if (existingOrgSponsorship != null) - { - // Replace existing invalid offer with our new sponsorship offer - sponsorship.Id = existingOrgSponsorship.Id; - } - - try - { - await _organizationSponsorshipRepository.UpsertAsync(sponsorship); - - await SendSponsorshipOfferAsync(sponsorship, sponsoringUserEmail); - } - catch - { - if (sponsorship.Id != default) - { - await _organizationSponsorshipRepository.DeleteAsync(sponsorship); - } - throw; - } - } - - public async Task ResendSponsorshipOfferAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, - OrganizationSponsorship sponsorship, string sponsoringUserEmail) - { - if (sponsoringOrg == null) - { - throw new BadRequestException("Cannot find the requested sponsoring organization."); - } - - if (sponsoringOrgUser == null || sponsoringOrgUser.Status != OrganizationUserStatusType.Confirmed) - { - throw new BadRequestException("Only confirmed users can sponsor other organizations."); - } - - if (sponsorship == null || sponsorship.OfferedToEmail == null) - { - throw new BadRequestException("Cannot find an outstanding sponsorship offer for this organization."); - } - - await SendSponsorshipOfferAsync(sponsorship, sponsoringUserEmail); - } - - public async Task SendSponsorshipOfferAsync(OrganizationSponsorship sponsorship, string sponsoringEmail) - { - var user = await _userRepository.GetByEmailAsync(sponsorship.OfferedToEmail); - var isExistingAccount = user != null; - - await _mailService.SendFamiliesForEnterpriseOfferEmailAsync(sponsorship.OfferedToEmail, sponsoringEmail, - isExistingAccount, RedemptionToken(sponsorship.Id, sponsorship.PlanSponsorshipType.Value)); - } - - public async Task SetUpSponsorshipAsync(OrganizationSponsorship sponsorship, - Organization sponsoredOrganization) - { - if (sponsorship == null) - { - throw new BadRequestException("No unredeemed sponsorship offer exists for you."); - } - - var existingOrgSponsorship = await _organizationSponsorshipRepository - .GetBySponsoredOrganizationIdAsync(sponsoredOrganization.Id); - if (existingOrgSponsorship != null) - { - throw new BadRequestException("Cannot redeem a sponsorship offer for an organization that is already sponsored. Revoke existing sponsorship first."); - } - - if (sponsorship.PlanSponsorshipType == null) - { - throw new BadRequestException("Cannot set up sponsorship without a known sponsorship type."); - } - - // Check org to sponsor's product type - var requiredSponsoredProductType = StaticStore.GetSponsoredPlan(sponsorship.PlanSponsorshipType.Value)?.SponsoredProductType; - if (requiredSponsoredProductType == null || - sponsoredOrganization == null || - StaticStore.GetPlan(sponsoredOrganization.PlanType).Product != requiredSponsoredProductType.Value) - { - throw new BadRequestException("Can only redeem sponsorship offer on families organizations."); - } - - await _paymentService.SponsorOrganizationAsync(sponsoredOrganization, sponsorship); - await _organizationRepository.UpsertAsync(sponsoredOrganization); - - sponsorship.SponsoredOrganizationId = sponsoredOrganization.Id; - sponsorship.OfferedToEmail = null; - await _organizationSponsorshipRepository.UpsertAsync(sponsorship); - } - - public async Task ValidateSponsorshipAsync(Guid sponsoredOrganizationId) - { - var sponsoredOrganization = await _organizationRepository.GetByIdAsync(sponsoredOrganizationId); - if (sponsoredOrganization == null) - { - return false; - } - - var existingSponsorship = await _organizationSponsorshipRepository - .GetBySponsoredOrganizationIdAsync(sponsoredOrganizationId); - - if (existingSponsorship == null) - { - await DoRemoveSponsorshipAsync(sponsoredOrganization, null); - return false; - } - - if (existingSponsorship.SponsoringOrganizationId == null || existingSponsorship.SponsoringOrganizationUserId == null || existingSponsorship.PlanSponsorshipType == null) - { - await DoRemoveSponsorshipAsync(sponsoredOrganization, existingSponsorship); - return false; - } - var sponsoredPlan = Utilities.StaticStore.GetSponsoredPlan(existingSponsorship.PlanSponsorshipType.Value); - - var sponsoringOrganization = await _organizationRepository - .GetByIdAsync(existingSponsorship.SponsoringOrganizationId.Value); - if (sponsoringOrganization == null) - { - await DoRemoveSponsorshipAsync(sponsoredOrganization, existingSponsorship); - return false; - } - - var sponsoringOrgPlan = Utilities.StaticStore.GetPlan(sponsoringOrganization.PlanType); - if (!sponsoringOrganization.Enabled || sponsoredPlan.SponsoringProductType != sponsoringOrgPlan.Product) - { - await DoRemoveSponsorshipAsync(sponsoredOrganization, existingSponsorship); - return false; - } - - return true; - } - - public async Task RevokeSponsorshipAsync(Organization sponsoredOrg, OrganizationSponsorship sponsorship) - { - if (sponsorship == null) - { - throw new BadRequestException("You are not currently sponsoring an organization."); - } - - if (sponsorship.SponsoredOrganizationId == null) - { - await DoRemoveSponsorshipAsync(null, sponsorship); - return; - } - - if (sponsoredOrg == null) - { - throw new BadRequestException("Unable to find the sponsored Organization."); - } - - await DoRemoveSponsorshipAsync(sponsoredOrg, sponsorship); - } - - public async Task RemoveSponsorshipAsync(Organization sponsoredOrg, OrganizationSponsorship sponsorship) - { - if (sponsorship == null || sponsorship.SponsoredOrganizationId == null) - { - throw new BadRequestException("The requested organization is not currently being sponsored."); - } - - if (sponsoredOrg == null) - { - throw new BadRequestException("Unable to find the sponsored Organization."); - } - - await DoRemoveSponsorshipAsync(sponsoredOrg, sponsorship); - } - - internal async Task DoRemoveSponsorshipAsync(Organization sponsoredOrganization, OrganizationSponsorship sponsorship = null) - { - if (sponsoredOrganization != null) - { - await _paymentService.RemoveOrganizationSponsorshipAsync(sponsoredOrganization, sponsorship); - await _organizationRepository.UpsertAsync(sponsoredOrganization); - - await _mailService.SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync( - sponsoredOrganization.BillingEmailAddress(), - sponsoredOrganization.Name); - } - - if (sponsorship == null) - { - return; - } - - // Initialize the record as available - sponsorship.SponsoredOrganizationId = null; - sponsorship.FriendlyName = null; - sponsorship.OfferedToEmail = null; - sponsorship.PlanSponsorshipType = null; - sponsorship.TimesRenewedWithoutValidation = 0; - sponsorship.SponsorshipLapsedDate = null; - - if (sponsorship.CloudSponsor || sponsorship.SponsorshipLapsedDate.HasValue) - { - await _organizationSponsorshipRepository.DeleteAsync(sponsorship); - } - else - { - await _organizationSponsorshipRepository.UpsertAsync(sponsorship); - } - } - } -} diff --git a/src/Core/Services/Implementations/RelayPushNotificationService.cs b/src/Core/Services/Implementations/RelayPushNotificationService.cs index 6b930a60f..f3cddee65 100644 --- a/src/Core/Services/Implementations/RelayPushNotificationService.cs +++ b/src/Core/Services/Implementations/RelayPushNotificationService.cs @@ -18,24 +18,24 @@ namespace Bit.Core.Services { private readonly IDeviceRepository _deviceRepository; private readonly IHttpContextAccessor _httpContextAccessor; - private readonly ILogger _logger; public RelayPushNotificationService( + IHttpClientFactory httpFactory, IDeviceRepository deviceRepository, GlobalSettings globalSettings, IHttpContextAccessor httpContextAccessor, ILogger logger) : base( - globalSettings.PushRelayBaseUri, - globalSettings.Installation.IdentityUri, - "api.push", - $"installation.{globalSettings.Installation.Id}", - globalSettings.Installation.Key, - logger) + httpFactory, + globalSettings.PushRelayBaseUri, + globalSettings.Installation.IdentityUri, + "api.push", + $"installation.{globalSettings.Installation.Id}", + globalSettings.Installation.Key, + logger) { _deviceRepository = deviceRepository; _httpContextAccessor = httpContextAccessor; - _logger = logger; } public async Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable collectionIds) diff --git a/src/Core/Services/Implementations/RelayPushRegistrationService.cs b/src/Core/Services/Implementations/RelayPushRegistrationService.cs index 9a77885bc..837df3a60 100644 --- a/src/Core/Services/Implementations/RelayPushRegistrationService.cs +++ b/src/Core/Services/Implementations/RelayPushRegistrationService.cs @@ -11,20 +11,20 @@ namespace Bit.Core.Services { public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegistrationService { - private readonly ILogger _logger; public RelayPushRegistrationService( + IHttpClientFactory httpFactory, GlobalSettings globalSettings, ILogger logger) : base( - globalSettings.PushRelayBaseUri, - globalSettings.Installation.IdentityUri, - "api.push", - $"installation.{globalSettings.Installation.Id}", - globalSettings.Installation.Key, - logger) + httpFactory, + globalSettings.PushRelayBaseUri, + globalSettings.Installation.IdentityUri, + "api.push", + $"installation.{globalSettings.Installation.Id}", + globalSettings.Installation.Key, + logger) { - _logger = logger; } public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, diff --git a/src/Core/Services/Implementations/StripePaymentService.cs b/src/Core/Services/Implementations/StripePaymentService.cs index 329b2738e..32fc6017a 100644 --- a/src/Core/Services/Implementations/StripePaymentService.cs +++ b/src/Core/Services/Implementations/StripePaymentService.cs @@ -204,6 +204,7 @@ namespace Bit.Core.Services var sub = await _stripeAdapter.SubscriptionGetAsync(org.GatewaySubscriptionId); org.ExpirationDate = sub.CurrentPeriodEnd; + sponsorship.ValidUntil = sub.CurrentPeriodEnd; } diff --git a/src/Core/Services/NoopImplementations/NoopLicensingService.cs b/src/Core/Services/NoopImplementations/NoopLicensingService.cs index 84c2599e9..8524b1aec 100644 --- a/src/Core/Services/NoopImplementations/NoopLicensingService.cs +++ b/src/Core/Services/NoopImplementations/NoopLicensingService.cs @@ -44,5 +44,15 @@ namespace Bit.Core.Services { return new byte[0]; } + + public Task ReadOrganizationLicenseAsync(Organization organization) + { + return Task.FromResult(null); + } + + public Task ReadOrganizationLicenseAsync(Guid organizationId) + { + return Task.FromResult(null); + } } } diff --git a/src/Core/Services/NoopImplementations/NoopMailService.cs b/src/Core/Services/NoopImplementations/NoopMailService.cs index 61d99b6a7..ba8d79069 100644 --- a/src/Core/Services/NoopImplementations/NoopMailService.cs +++ b/src/Core/Services/NoopImplementations/NoopMailService.cs @@ -55,12 +55,12 @@ namespace Bit.Core.Services return Task.FromResult(0); } - public Task SendOrganizationInviteEmailAsync(string organizationName, bool orgCanSponsor, OrganizationUser orgUser, ExpiringToken token) + public Task SendOrganizationInviteEmailAsync(string organizationName, OrganizationUser orgUser, ExpiringToken token) { return Task.FromResult(0); } - public Task BulkSendOrganizationInviteEmailAsync(string organizationName, bool orgCanSponsor, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites) + public Task BulkSendOrganizationInviteEmailAsync(string organizationName, IEnumerable<(OrganizationUser orgUser, ExpiringToken token)> invites) { return Task.FromResult(0); } @@ -206,7 +206,12 @@ namespace Bit.Core.Services return Task.FromResult(0); } - public Task SendFamiliesForEnterpriseOfferEmailAsync(string email, string sponsorEmail, bool existingAccount, string token) + public Task SendFamiliesForEnterpriseOfferEmailAsync(string SponsorOrgName, string email, bool existingAccount, string token) + { + return Task.FromResult(0); + } + + public Task BulkSendFamiliesForEnterpriseOfferEmailAsync(string SponsorOrgName, IEnumerable<(string Email, bool ExistingAccount, string Token)> invites) { return Task.FromResult(0); } @@ -216,7 +221,7 @@ namespace Bit.Core.Services return Task.FromResult(0); } - public Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, string familyOrgName) + public Task SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(string email, DateTime expirationDate) { return Task.FromResult(0); } diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs index 342a3e57b..0778ed88f 100644 --- a/src/Core/Settings/GlobalSettings.cs +++ b/src/Core/Settings/GlobalSettings.cs @@ -37,18 +37,19 @@ namespace Bit.Core.Settings public virtual string HibpApiKey { get; set; } public virtual bool DisableUserRegistration { get; set; } public virtual bool DisableEmailNewDevice { get; set; } + public virtual bool EnableCloudCommunication { get; set; } = false; public virtual int OrganizationInviteExpirationHours { get; set; } = 120; // 5 days public virtual string EventGridKey { get; set; } public virtual CaptchaSettings Captcha { get; set; } = new CaptchaSettings(); - public virtual InstallationSettings Installation { get; set; } = new InstallationSettings(); - public virtual BaseServiceUriSettings BaseServiceUri { get; set; } + public virtual IInstallationSettings Installation { get; set; } = new InstallationSettings(); + public virtual IBaseServiceUriSettings BaseServiceUri { get; set; } public virtual string DatabaseProvider { get; set; } public virtual SqlSettings SqlServer { get; set; } = new SqlSettings(); public virtual SqlSettings PostgreSql { get; set; } = new SqlSettings(); public virtual SqlSettings MySql { get; set; } = new SqlSettings(); public virtual SqlSettings Sqlite { get; set; } = new SqlSettings(); public virtual MailSettings Mail { get; set; } = new MailSettings(); - public virtual ConnectionStringSettings Storage { get; set; } = new ConnectionStringSettings(); + public virtual IConnectionStringSettings Storage { get; set; } = new ConnectionStringSettings(); public virtual ConnectionStringSettings Events { get; set; } = new ConnectionStringSettings(); public virtual NotificationsSettings Notifications { get; set; } = new NotificationsSettings(); public virtual IFileStorageSettings Attachment { get; set; } @@ -108,7 +109,7 @@ namespace Bit.Core.Settings return string.Concat("/etc/bitwarden", appendedPath); } - public class BaseServiceUriSettings + public class BaseServiceUriSettings : IBaseServiceUriSettings { private readonly GlobalSettings _globalSettings; @@ -216,7 +217,7 @@ namespace Bit.Core.Settings } } - public class ConnectionStringSettings + public class ConnectionStringSettings : IConnectionStringSettings { private string _connectionString; @@ -421,7 +422,7 @@ namespace Bit.Core.Settings public string NotificationUrl { get; set; } } - public class InstallationSettings + public class InstallationSettings : IInstallationSettings { private string _identityUri; private string _apiUri; @@ -436,6 +437,7 @@ namespace Bit.Core.Settings public string ApiUri { get => string.IsNullOrWhiteSpace(_apiUri) ? "https://api.biwarden.com" : _apiUri; + set => _apiUri = value; } } diff --git a/src/Core/Settings/IBaseServiceUriSettings.cs b/src/Core/Settings/IBaseServiceUriSettings.cs new file mode 100644 index 000000000..9a4373e91 --- /dev/null +++ b/src/Core/Settings/IBaseServiceUriSettings.cs @@ -0,0 +1,20 @@ + +namespace Bit.Core.Settings +{ + public interface IBaseServiceUriSettings + { + string Vault { get; set; } + string VaultWithHash { get; } + string Api { get; set; } + public string Identity { get; set; } + public string Admin { get; set; } + public string Notifications { get; set; } + public string Sso { get; set; } + public string InternalNotifications { get; set; } + public string InternalAdmin { get; set; } + public string InternalIdentity { get; set; } + public string InternalApi { get; set; } + public string InternalVault { get; set; } + public string InternalSso { get; set; } + } +} diff --git a/src/Core/Settings/IConnectionStringSettings.cs b/src/Core/Settings/IConnectionStringSettings.cs new file mode 100644 index 000000000..aff2b0627 --- /dev/null +++ b/src/Core/Settings/IConnectionStringSettings.cs @@ -0,0 +1,8 @@ +namespace Bit.Core.Settings +{ + + public interface IConnectionStringSettings + { + string ConnectionString { get; set; } + } +} diff --git a/src/Core/Settings/IGlobalSettings.cs b/src/Core/Settings/IGlobalSettings.cs index e286d7c2b..0d5710b10 100644 --- a/src/Core/Settings/IGlobalSettings.cs +++ b/src/Core/Settings/IGlobalSettings.cs @@ -6,9 +6,13 @@ namespace Bit.Core.Settings { // This interface exists for testing. Add settings here as needed for testing bool SelfHosted { get; set; } + bool EnableCloudCommunication { get; set; } string LicenseDirectory { get; set; } + string LicenseCertificatePassword { get; set; } int OrganizationInviteExpirationHours { get; set; } - InstallationSettings Installation { get; set; } + IInstallationSettings Installation { get; set; } IFileStorageSettings Attachment { get; set; } + IConnectionStringSettings Storage { get; set; } + IBaseServiceUriSettings BaseServiceUri { get; set; } } } diff --git a/src/Core/Settings/IInstallationSettings.cs b/src/Core/Settings/IInstallationSettings.cs new file mode 100644 index 000000000..f78b13db2 --- /dev/null +++ b/src/Core/Settings/IInstallationSettings.cs @@ -0,0 +1,12 @@ +using System; + +namespace Bit.Core.Settings +{ + public interface IInstallationSettings + { + public Guid Id { get; set; } + public string Key { get; set; } + public string IdentityUri { get; set; } + public string ApiUri { get; } + } +} diff --git a/src/Core/Tokens/DataProtectorTokenFactory.cs b/src/Core/Tokens/DataProtectorTokenFactory.cs index 17f7873ae..8029b3554 100644 --- a/src/Core/Tokens/DataProtectorTokenFactory.cs +++ b/src/Core/Tokens/DataProtectorTokenFactory.cs @@ -16,6 +16,13 @@ namespace Bit.Core.Tokens public string Protect(T data) => data.ToToken().ProtectWith(_dataProtector).WithPrefix(_clearTextPrefix).ToString(); + /// + /// Unprotect token + /// + /// The token to parse + /// The tokenable type to parse to + /// The parsed tokenable + /// Throws CryptographicException if fails to unprotect public T Unprotect(string token) => Tokenable.FromToken(new Token(token).RemovePrefix(_clearTextPrefix).UnprotectWith(_dataProtector).ToString()); diff --git a/src/Core/Tokens/IBillingSyncTokenable.cs b/src/Core/Tokens/IBillingSyncTokenable.cs new file mode 100644 index 000000000..f73940b8b --- /dev/null +++ b/src/Core/Tokens/IBillingSyncTokenable.cs @@ -0,0 +1,10 @@ +using System; + +namespace Bit.Core.Tokens +{ + public interface IBillingSyncTokenable + { + public Guid OrganizationId { get; set; } + public string BillingSyncKey { get; set; } + } +} diff --git a/src/Core/Utilities/CoreHelpers.cs b/src/Core/Utilities/CoreHelpers.cs index e6ef1ad46..3731e518f 100644 --- a/src/Core/Utilities/CoreHelpers.cs +++ b/src/Core/Utilities/CoreHelpers.cs @@ -48,14 +48,22 @@ namespace Bit.Core.Utilities /// /// A comb Guid. public static Guid GenerateComb() - { - var guidArray = Guid.NewGuid().ToByteArray(); + => GenerateComb(Guid.NewGuid(), DateTime.UtcNow); - var now = DateTime.UtcNow; + /// + /// Implementation of with input parameters to remove randomness. + /// This should NOT be used outside of testing. + /// + /// + /// You probably don't want to use this method and instead want to use with no parameters + /// + internal static Guid GenerateComb(Guid startingGuid, DateTime time) + { + var guidArray = startingGuid.ToByteArray(); // Get the days and milliseconds which will be used to build the byte string - var days = new TimeSpan(now.Ticks - _baseDateTicks); - var msecs = now.TimeOfDay; + var days = new TimeSpan(time.Ticks - _baseDateTicks); + var msecs = time.TimeOfDay; // Convert to a byte array // Note that SQL Server is accurate to 1/300th of a millisecond so we divide by 3.333333 diff --git a/src/Core/Utilities/JsonHelpers.cs b/src/Core/Utilities/JsonHelpers.cs index 954a9a14d..76eb97548 100644 --- a/src/Core/Utilities/JsonHelpers.cs +++ b/src/Core/Utilities/JsonHelpers.cs @@ -1,7 +1,5 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; using System.Text.Json; using System.Text.Json.Serialization; using NS = Newtonsoft.Json; @@ -48,12 +46,18 @@ namespace Bit.Core.Utilities }; } - // NOTE: This is built into .NET 6, it SHOULD be removed when we upgrade + [Obsolete("This is built into .NET 6, it SHOULD be removed when we upgrade")] public static T ToObject(this JsonElement element, JsonSerializerOptions options = null) { return JsonSerializer.Deserialize(element.GetRawText(), options ?? Default); } + [Obsolete("This is built into .NET 6, it SHOULD be removed when we upgrade")] + public static T ToObject(this JsonDocument document, JsonSerializerOptions options = null) + { + return JsonSerializer.Deserialize(document.RootElement.GetRawText(), options ?? default); + } + public static T DeserializeOrNew(string json, JsonSerializerOptions options = null) where T : new() { diff --git a/src/Core/Utilities/StaticStore.cs b/src/Core/Utilities/StaticStore.cs index 2b595fce7..2d03b6d2c 100644 --- a/src/Core/Utilities/StaticStore.cs +++ b/src/Core/Utilities/StaticStore.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; using Bit.Core.Enums; -using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.StaticStore; namespace Bit.Core.Utilities diff --git a/src/Identity/Startup.cs b/src/Identity/Startup.cs index 35c6db250..2b3d4afff 100644 --- a/src/Identity/Startup.cs +++ b/src/Identity/Startup.cs @@ -126,7 +126,7 @@ namespace Bit.Identity services.AddCustomIdentityServices(globalSettings); // Services - services.AddBaseServices(); + services.AddBaseServices(globalSettings); services.AddDefaultServices(globalSettings); services.AddCoreLocalizationServices(); diff --git a/src/Infrastructure.Dapper/DapperHelpers.cs b/src/Infrastructure.Dapper/DapperHelpers.cs index 8316375f1..0042ff4ef 100644 --- a/src/Infrastructure.Dapper/DapperHelpers.cs +++ b/src/Infrastructure.Dapper/DapperHelpers.cs @@ -80,18 +80,45 @@ namespace Bit.Infrastructure.Dapper (nameof(OrganizationUser.ResetPasswordKey), typeof(string), ou => ou.ResetPasswordKey), }; + return orgUsers.BuildTable(table, columnData); + } + + public static DataTable ToTvp(this IEnumerable organizationSponsorships) + { + var table = new DataTable(); + table.SetTypeName("[dbo].[OrganizationSponsorshipType]"); + + var columnData = new List<(string name, Type type, Func getter)> + { + (nameof(OrganizationSponsorship.Id), typeof(Guid), ou => ou.Id), + (nameof(OrganizationSponsorship.SponsoringOrganizationId), typeof(Guid), ou => ou.SponsoringOrganizationId), + (nameof(OrganizationSponsorship.SponsoringOrganizationUserId), typeof(Guid), ou => ou.SponsoringOrganizationUserId), + (nameof(OrganizationSponsorship.SponsoredOrganizationId), typeof(Guid), ou => ou.SponsoredOrganizationId), + (nameof(OrganizationSponsorship.FriendlyName), typeof(string), ou => ou.FriendlyName), + (nameof(OrganizationSponsorship.OfferedToEmail), typeof(string), ou => ou.OfferedToEmail), + (nameof(OrganizationSponsorship.PlanSponsorshipType), typeof(byte), ou => ou.PlanSponsorshipType), + (nameof(OrganizationSponsorship.LastSyncDate), typeof(DateTime), ou => ou.LastSyncDate), + (nameof(OrganizationSponsorship.ValidUntil), typeof(DateTime), ou => ou.ValidUntil), + (nameof(OrganizationSponsorship.ToDelete), typeof(bool), ou => ou.ToDelete), + }; + + return organizationSponsorships.BuildTable(table, columnData); + } + + private static DataTable BuildTable(this IEnumerable entities, DataTable table, List<(string name, Type type, Func getter)> columnData) + { foreach (var (name, type, getter) in columnData) { var column = new DataColumn(name, type); table.Columns.Add(column); } - foreach (var orgUser in orgUsers ?? new OrganizationUser[] { }) + foreach (var entity in entities ?? new T[] { }) { var row = table.NewRow(); foreach (var (name, type, getter) in columnData) { - var val = getter(orgUser); + var val = getter(entity); if (val == null) { row[name] = DBNull.Value; diff --git a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs index 9ce997fae..a75a05fcc 100644 --- a/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs +++ b/src/Infrastructure.Dapper/DapperServiceCollectionExtensions.cs @@ -32,6 +32,8 @@ namespace Bit.Infrastructure.Dapper services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); if (selfHosted) { diff --git a/src/Infrastructure.Dapper/Repositories/MaintenanceRepository.cs b/src/Infrastructure.Dapper/Repositories/MaintenanceRepository.cs index 12d942248..e5c0850ed 100644 --- a/src/Infrastructure.Dapper/Repositories/MaintenanceRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/MaintenanceRepository.cs @@ -1,4 +1,5 @@ -using System.Data; +using System; +using System.Data; using System.Data.SqlClient; using System.Threading.Tasks; using Bit.Core.Repositories; @@ -62,5 +63,17 @@ namespace Bit.Infrastructure.Dapper.Repositories commandTimeout: 172800); } } + + public async Task DeleteExpiredSponsorshipsAsync(DateTime validUntilBeforeDate) + { + using (var connection = new SqlConnection(ConnectionString)) + { + await connection.ExecuteAsync( + "[dbo].[OrganizationSponsorship_DeleteExpired]", + new { ValidUntilBeforeDate = validUntilBeforeDate }, + commandType: CommandType.StoredProcedure, + commandTimeout: 172800); + } + } } } diff --git a/src/Infrastructure.Dapper/Repositories/OrganizationApiKeyRepository.cs b/src/Infrastructure.Dapper/Repositories/OrganizationApiKeyRepository.cs new file mode 100644 index 000000000..6f12b390d --- /dev/null +++ b/src/Infrastructure.Dapper/Repositories/OrganizationApiKeyRepository.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Core.Settings; +using Dapper; + +namespace Bit.Infrastructure.Dapper.Repositories +{ + public class OrganizationApiKeyRepository : Repository, IOrganizationApiKeyRepository + { + public OrganizationApiKeyRepository(GlobalSettings globalSettings) + : this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + { + + } + + public OrganizationApiKeyRepository(string connectionString, string readOnlyConnectionString) + : base(connectionString, readOnlyConnectionString) + { } + + public async Task> GetManyByOrganizationIdTypeAsync(Guid organizationId, OrganizationApiKeyType? type = null) + { + using (var connection = new SqlConnection(ConnectionString)) + { + return await connection.QueryAsync( + "[dbo].[OrganizationApikey_ReadManyByOrganizationIdType]", + new + { + OrganizationId = organizationId, + Type = type, + }, + commandType: CommandType.StoredProcedure); + } + } + } +} diff --git a/src/Infrastructure.Dapper/Repositories/OrganizationConnectionRepository.cs b/src/Infrastructure.Dapper/Repositories/OrganizationConnectionRepository.cs new file mode 100644 index 000000000..827061ec9 --- /dev/null +++ b/src/Infrastructure.Dapper/Repositories/OrganizationConnectionRepository.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Data.SqlClient; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Core.Settings; +using Dapper; + +namespace Bit.Infrastructure.Dapper.Repositories +{ + public class OrganizationConnectionRepository : Repository, IOrganizationConnectionRepository + { + public OrganizationConnectionRepository(GlobalSettings globalSettings) + : base(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString) + { } + + public async Task> GetByOrganizationIdTypeAsync(Guid organizationId, OrganizationConnectionType type) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + $"[{Schema}].[OrganizationConnection_ReadByOrganizationIdType]", + new + { + OrganizationId = organizationId, + Type = type + }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + + public async Task> GetEnabledByOrganizationIdTypeAsync(Guid organizationId, OrganizationConnectionType type) => + (await GetByOrganizationIdTypeAsync(organizationId, type)).Where(c => c.Enabled).ToList(); + } +} diff --git a/src/Infrastructure.Dapper/Repositories/OrganizationRepository.cs b/src/Infrastructure.Dapper/Repositories/OrganizationRepository.cs index 1e8283fad..5d4f7ad5d 100644 --- a/src/Infrastructure.Dapper/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/OrganizationRepository.cs @@ -5,7 +5,7 @@ using System.Data.SqlClient; using System.Linq; using System.Threading.Tasks; using Bit.Core.Entities; -using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations; using Bit.Core.Repositories; using Bit.Core.Settings; using Dapper; diff --git a/src/Infrastructure.Dapper/Repositories/OrganizationSponsorshipRepository.cs b/src/Infrastructure.Dapper/Repositories/OrganizationSponsorshipRepository.cs index 90217d859..fca28e0f9 100644 --- a/src/Infrastructure.Dapper/Repositories/OrganizationSponsorshipRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/OrganizationSponsorshipRepository.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Data; using System.Data.SqlClient; using System.Linq; @@ -20,6 +21,76 @@ namespace Bit.Infrastructure.Dapper.Repositories : base(connectionString, readOnlyConnectionString) { } + public async Task> CreateManyAsync(IEnumerable organizationSponsorships) + { + if (!organizationSponsorships.Any()) + { + return default; + } + + foreach (var organizationSponsorship in organizationSponsorships) + { + organizationSponsorship.SetNewId(); + } + + var orgSponsorshipsTVP = organizationSponsorships.ToTvp(); + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.ExecuteAsync( + $"[dbo].[OrganizationSponsorship_CreateMany]", + new { OrganizationSponsorshipsInput = orgSponsorshipsTVP }, + commandType: CommandType.StoredProcedure); + } + + return organizationSponsorships.Select(u => u.Id).ToList(); + } + + public async Task ReplaceManyAsync(IEnumerable organizationSponsorships) + { + if (!organizationSponsorships.Any()) + { + return; + } + + var orgSponsorshipsTVP = organizationSponsorships.ToTvp(); + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.ExecuteAsync( + $"[dbo].[OrganizationSponsorship_UpdateMany]", + new { OrganizationSponsorshipsInput = orgSponsorshipsTVP }, + commandType: CommandType.StoredProcedure); + } + } + + public async Task UpsertManyAsync(IEnumerable organizationSponsorships) + { + var createSponsorships = new List(); + var replaceSponsorships = new List(); + foreach (var organizationSponsorship in organizationSponsorships) + { + if (organizationSponsorship.Id.Equals(default)) + { + createSponsorships.Add(organizationSponsorship); + } + else + { + replaceSponsorships.Add(organizationSponsorship); + } + } + + await CreateManyAsync(createSponsorships); + await ReplaceManyAsync(replaceSponsorships); + } + + public async Task DeleteManyAsync(IEnumerable organizationSponsorshipIds) + { + using (var connection = new SqlConnection(ConnectionString)) + { + await connection.ExecuteAsync("[dbo].[OrganizationSponsorship_DeleteByIds]", + new { Ids = organizationSponsorshipIds.ToGuidIdArrayTVP() }, commandType: CommandType.StoredProcedure); + } + } + public async Task GetBySponsoringOrganizationUserIdAsync(Guid sponsoringOrganizationUserId) { using (var connection = new SqlConnection(ConnectionString)) @@ -48,5 +119,33 @@ namespace Bit.Infrastructure.Dapper.Repositories return results.SingleOrDefault(); } } + + public async Task GetLatestSyncDateBySponsoringOrganizationIdAsync(Guid sponsoringOrganizationId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + return await connection.QuerySingleOrDefaultAsync( + "[dbo].[OrganizationSponsorship_ReadLatestBySponsoringOrganizationId]", + new { SponsoringOrganizationId = sponsoringOrganizationId }, + commandType: CommandType.StoredProcedure); + } + } + + public async Task> GetManyBySponsoringOrganizationAsync(Guid sponsoringOrganizationId) + { + using (var connection = new SqlConnection(ConnectionString)) + { + var results = await connection.QueryAsync( + "[dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationId]", + new + { + SponsoringOrganizationId = sponsoringOrganizationId + }, + commandType: CommandType.StoredProcedure); + + return results.ToList(); + } + } + } } diff --git a/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs index 267634f3f..361f71b6c 100644 --- a/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.Dapper/Repositories/OrganizationUserRepository.cs @@ -8,9 +8,9 @@ using System.Threading.Tasks; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Settings; -using Bit.Core.Utilities; using Dapper; namespace Bit.Infrastructure.Dapper.Repositories diff --git a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs index 54199b2e7..b0efbed02 100644 --- a/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs +++ b/src/Infrastructure.EntityFramework/EntityFrameworkServiceCollectionExtensions.cs @@ -42,6 +42,8 @@ namespace Bit.Infrastructure.EntityFramework services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/src/Infrastructure.EntityFramework/Models/Organization.cs b/src/Infrastructure.EntityFramework/Models/Organization.cs index 40c7d19ae..c5708628f 100644 --- a/src/Infrastructure.EntityFramework/Models/Organization.cs +++ b/src/Infrastructure.EntityFramework/Models/Organization.cs @@ -12,6 +12,8 @@ namespace Bit.Infrastructure.EntityFramework.Models public virtual ICollection SsoConfigs { get; set; } public virtual ICollection SsoUsers { get; set; } public virtual ICollection Transactions { get; set; } + public virtual ICollection ApiKeys { get; set; } + public virtual ICollection Connections { get; set; } } public class OrganizationMapperProfile : Profile diff --git a/src/Infrastructure.EntityFramework/Models/OrganizationApiKey.cs b/src/Infrastructure.EntityFramework/Models/OrganizationApiKey.cs new file mode 100644 index 000000000..c0e6c33e0 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Models/OrganizationApiKey.cs @@ -0,0 +1,17 @@ +using AutoMapper; + +namespace Bit.Infrastructure.EntityFramework.Models +{ + public class OrganizationApiKey : Core.Entities.OrganizationApiKey + { + public virtual Organization Organization { get; set; } + } + + public class OrganizationApiKeyMapperProfile : Profile + { + public OrganizationApiKeyMapperProfile() + { + CreateMap().ReverseMap(); + } + } +} diff --git a/src/Infrastructure.EntityFramework/Models/OrganizationConnection.cs b/src/Infrastructure.EntityFramework/Models/OrganizationConnection.cs new file mode 100644 index 000000000..f53ee711c --- /dev/null +++ b/src/Infrastructure.EntityFramework/Models/OrganizationConnection.cs @@ -0,0 +1,17 @@ +using AutoMapper; + +namespace Bit.Infrastructure.EntityFramework.Models +{ + public class OrganizationConnection : Core.Entities.OrganizationConnection + { + public virtual Organization Organization { get; set; } + } + + public class OrganizationConnectionMapperProfile : Profile + { + public OrganizationConnectionMapperProfile() + { + CreateMap().ReverseMap(); + } + } +} diff --git a/src/Infrastructure.EntityFramework/Models/OrganizationSponsorship.cs b/src/Infrastructure.EntityFramework/Models/OrganizationSponsorship.cs index a80e950e2..c9eee03e5 100644 --- a/src/Infrastructure.EntityFramework/Models/OrganizationSponsorship.cs +++ b/src/Infrastructure.EntityFramework/Models/OrganizationSponsorship.cs @@ -4,7 +4,6 @@ namespace Bit.Infrastructure.EntityFramework.Models { public class OrganizationSponsorship : Core.Entities.OrganizationSponsorship { - public virtual Installation Installation { get; set; } public virtual Organization SponsoringOrganization { get; set; } public virtual Organization SponsoredOrganization { get; set; } } diff --git a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs index f1cca95bc..5e96686b8 100644 --- a/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs +++ b/src/Infrastructure.EntityFramework/Repositories/DatabaseContext.cs @@ -1,4 +1,6 @@ -using Bit.Infrastructure.EntityFramework.Models; +using System.Text.Json; +using System.Text.Json.Serialization; +using Bit.Infrastructure.EntityFramework.Models; using Microsoft.EntityFrameworkCore; namespace Bit.Infrastructure.EntityFramework.Repositories @@ -25,7 +27,9 @@ namespace Bit.Infrastructure.EntityFramework.Repositories public DbSet GroupUsers { get; set; } public DbSet Installations { get; set; } public DbSet Organizations { get; set; } + public DbSet OrganizationApiKeys { get; set; } public DbSet OrganizationSponsorships { get; set; } + public DbSet OrganizationConnections { get; set; } public DbSet OrganizationUsers { get; set; } public DbSet Policies { get; set; } public DbSet Providers { get; set; } @@ -66,6 +70,8 @@ namespace Bit.Infrastructure.EntityFramework.Repositories var eTaxRate = builder.Entity(); var eTransaction = builder.Entity(); var eUser = builder.Entity(); + var eOrganizationApiKey = builder.Entity(); + var eOrganizationConnection = builder.Entity(); eCipher.Property(c => c.Id).ValueGeneratedNever(); eCollection.Property(c => c.Id).ValueGeneratedNever(); @@ -84,6 +90,8 @@ namespace Bit.Infrastructure.EntityFramework.Repositories eSend.Property(c => c.Id).ValueGeneratedNever(); eTransaction.Property(c => c.Id).ValueGeneratedNever(); eUser.Property(c => c.Id).ValueGeneratedNever(); + eOrganizationApiKey.Property(c => c.Id).ValueGeneratedNever(); + eOrganizationConnection.Property(c => c.Id).ValueGeneratedNever(); eCollectionCipher.HasKey(cc => new { cc.CollectionId, cc.CipherId }); eCollectionUser.HasKey(cu => new { cu.CollectionId, cu.OrganizationUserId }); @@ -127,6 +135,8 @@ namespace Bit.Infrastructure.EntityFramework.Repositories eTaxRate.ToTable(nameof(TaxRate)); eTransaction.ToTable(nameof(Transaction)); eUser.ToTable(nameof(User)); + eOrganizationApiKey.ToTable(nameof(OrganizationApiKey)); + eOrganizationConnection.ToTable(nameof(OrganizationConnection)); } } } diff --git a/src/Infrastructure.EntityFramework/Repositories/MaintenanceRepository.cs b/src/Infrastructure.EntityFramework/Repositories/MaintenanceRepository.cs index 01d3ab1b4..208ed36c4 100644 --- a/src/Infrastructure.EntityFramework/Repositories/MaintenanceRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/MaintenanceRepository.cs @@ -26,6 +26,19 @@ namespace Bit.Infrastructure.EntityFramework.Repositories } } + public async Task DeleteExpiredSponsorshipsAsync(DateTime validUntilBeforeDate) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var query = from s in dbContext.OrganizationSponsorships + where s.ValidUntil < validUntilBeforeDate + select s; + dbContext.RemoveRange(query); + await dbContext.SaveChangesAsync(); + } + } + public Task DisableCipherAutoStatsAsync() { return Task.CompletedTask; diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationApiKeyRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationApiKeyRepository.cs new file mode 100644 index 000000000..debcaade6 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationApiKeyRepository.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoMapper; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Bit.Core.Utilities; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Infrastructure.EntityFramework.Repositories +{ + public class OrganizationApiKeyRepository : Repository, IOrganizationApiKeyRepository + { + public OrganizationApiKeyRepository(IServiceScopeFactory serviceScopeFactory, IMapper mapper) + : base(serviceScopeFactory, mapper, db => db.OrganizationApiKeys) + { + + } + + public async Task> GetManyByOrganizationIdTypeAsync(Guid organizationId, OrganizationApiKeyType? type = null) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var apiKeys = await dbContext.OrganizationApiKeys + .Where(o => o.OrganizationId == organizationId && (type == null || o.Type == type)) + .ToListAsync(); + return Mapper.Map>(apiKeys); + } + } + } +} diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationConnectionRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationConnectionRepository.cs new file mode 100644 index 000000000..5c7e4e0e6 --- /dev/null +++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationConnectionRepository.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using AutoMapper; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + +namespace Bit.Infrastructure.EntityFramework.Repositories +{ + public class OrganizationConnectionRepository : Repository, IOrganizationConnectionRepository + { + public OrganizationConnectionRepository(IServiceScopeFactory serviceScopeFactory, + IMapper mapper) + : base(serviceScopeFactory, mapper, context => context.OrganizationConnections) + { + } + + public async Task> GetByOrganizationIdTypeAsync(Guid organizationId, OrganizationConnectionType type) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var connections = await dbContext.OrganizationConnections + .Where(oc => oc.OrganizationId == organizationId && oc.Type == type) + .ToListAsync(); + return Mapper.Map>(connections); + } + } + + public async Task> GetEnabledByOrganizationIdTypeAsync(Guid organizationId, OrganizationConnectionType type) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var connections = await dbContext.OrganizationConnections + .Where(oc => oc.OrganizationId == organizationId && oc.Type == type && oc.Enabled) + .ToListAsync(); + return Mapper.Map>(connections); + } + } + } +} diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationRepository.cs index 9aa839b23..6d72acc7d 100644 --- a/src/Infrastructure.EntityFramework/Repositories/OrganizationRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationRepository.cs @@ -3,11 +3,11 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using AutoMapper; +using Bit.Core.Models.Data.Organizations; using Bit.Core.Repositories; using Bit.Infrastructure.EntityFramework.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; -using DataModel = Bit.Core.Models.Data; namespace Bit.Infrastructure.EntityFramework.Repositories { @@ -71,13 +71,13 @@ namespace Bit.Infrastructure.EntityFramework.Repositories } } - public async Task> GetManyAbilitiesAsync() + public async Task> GetManyAbilitiesAsync() { using (var scope = ServiceScopeFactory.CreateScope()) { var dbContext = GetDatabaseContext(scope); return await GetDbSet(dbContext) - .Select(e => new DataModel.OrganizationAbility + .Select(e => new OrganizationAbility { Enabled = e.Enabled, Id = e.Id, @@ -102,18 +102,6 @@ namespace Bit.Infrastructure.EntityFramework.Repositories { var dbContext = GetDatabaseContext(scope); var orgEntity = await dbContext.FindAsync(organization.Id); - var sponsorships = dbContext.OrganizationSponsorships - .Where(os => - os.SponsoringOrganizationId == organization.Id || - os.SponsoredOrganizationId == organization.Id); - - Guid? UpdatedOrgId(Guid? orgId) => orgId == organization.Id ? null : organization.Id; - foreach (var sponsorship in sponsorships) - { - sponsorship.SponsoredOrganizationId = UpdatedOrgId(sponsorship.SponsoredOrganizationId); - sponsorship.SponsoringOrganizationId = UpdatedOrgId(sponsorship.SponsoringOrganizationId); - sponsorship.FriendlyName = null; - } dbContext.Remove(orgEntity); await dbContext.SaveChangesAsync(); diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationSponsorshipRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationSponsorshipRepository.cs index 583101982..e5bee132f 100644 --- a/src/Infrastructure.EntityFramework/Repositories/OrganizationSponsorshipRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationSponsorshipRepository.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using AutoMapper; @@ -15,6 +16,73 @@ namespace Bit.Infrastructure.EntityFramework.Repositories : base(serviceScopeFactory, mapper, (DatabaseContext context) => context.OrganizationSponsorships) { } + public async Task> CreateManyAsync(IEnumerable organizationSponsorships) + { + if (!organizationSponsorships.Any()) + { + return new List(); + } + + foreach (var organizationSponsorship in organizationSponsorships) + { + organizationSponsorship.SetNewId(); + } + + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var entities = Mapper.Map>(organizationSponsorships); + await dbContext.AddRangeAsync(entities); + await dbContext.SaveChangesAsync(); + } + + return organizationSponsorships.Select(u => u.Id).ToList(); + } + + public async Task ReplaceManyAsync(IEnumerable organizationSponsorships) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + dbContext.UpdateRange(organizationSponsorships); + await dbContext.SaveChangesAsync(); + } + } + + public async Task UpsertManyAsync(IEnumerable organizationSponsorships) + { + var createSponsorships = new List(); + var replaceSponsorships = new List(); + foreach (var organizationSponsorship in organizationSponsorships) + { + if (organizationSponsorship.Id.Equals(default)) + { + createSponsorships.Add(organizationSponsorship); + } + else + { + replaceSponsorships.Add(organizationSponsorship); + } + } + + await CreateManyAsync(createSponsorships); + await ReplaceManyAsync(replaceSponsorships); + } + + public async Task DeleteManyAsync(IEnumerable organizationSponsorshipIds) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var entities = await dbContext.OrganizationSponsorships + .Where(os => organizationSponsorshipIds.Contains(os.Id)) + .ToListAsync(); + + dbContext.OrganizationSponsorships.RemoveRange(entities); + await dbContext.SaveChangesAsync(); + } + } + public async Task GetByOfferedToEmailAsync(string email) { using (var scope = ServiceScopeFactory.CreateScope()) @@ -47,5 +115,31 @@ namespace Bit.Infrastructure.EntityFramework.Repositories return orgSponsorship; } } + + public async Task GetLatestSyncDateBySponsoringOrganizationIdAsync(Guid sponsoringOrganizationId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + return await GetDbSet(dbContext).Where(e => e.SponsoringOrganizationId == sponsoringOrganizationId && e.LastSyncDate != null) + .OrderByDescending(e => e.LastSyncDate) + .Select(e => e.LastSyncDate) + .FirstOrDefaultAsync(); + + } + } + + public async Task> GetManyBySponsoringOrganizationAsync(Guid sponsoringOrganizationId) + { + using (var scope = ServiceScopeFactory.CreateScope()) + { + var dbContext = GetDatabaseContext(scope); + var query = from os in dbContext.OrganizationSponsorships + where os.SponsoringOrganizationId == sponsoringOrganizationId + select os; + return Mapper.Map>(await query.ToListAsync()); + } + } + } } diff --git a/src/Infrastructure.EntityFramework/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/Repositories/OrganizationUserRepository.cs index b130cfa8f..fc68375be 100644 --- a/src/Infrastructure.EntityFramework/Repositories/OrganizationUserRepository.cs +++ b/src/Infrastructure.EntityFramework/Repositories/OrganizationUserRepository.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using AutoMapper; using Bit.Core.Enums; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Infrastructure.EntityFramework.Models; using Bit.Infrastructure.EntityFramework.Repositories.Queries; @@ -61,6 +62,7 @@ namespace Bit.Infrastructure.EntityFramework.Repositories var dbContext = GetDatabaseContext(scope); var entities = Mapper.Map>(organizationUsers); await dbContext.AddRangeAsync(entities); + await dbContext.SaveChangesAsync(); } return organizationUsers.Select(u => u.Id).ToList(); @@ -73,14 +75,6 @@ namespace Bit.Infrastructure.EntityFramework.Repositories { var dbContext = GetDatabaseContext(scope); var orgUser = await dbContext.FindAsync(organizationUserId); - var sponsorships = dbContext.OrganizationSponsorships - .Where(os => os.SponsoringOrganizationUserId != default && - os.SponsoringOrganizationUserId.Value == organizationUserId); - foreach (var sponsorship in sponsorships) - { - sponsorship.SponsoringOrganizationUserId = null; - sponsorship.FriendlyName = null; - } dbContext.Remove(orgUser); await dbContext.SaveChangesAsync(); @@ -96,15 +90,6 @@ namespace Bit.Infrastructure.EntityFramework.Repositories .Where(ou => organizationUserIds.Contains(ou.Id)) .ToListAsync(); - var sponsorships = dbContext.OrganizationSponsorships - .Where(os => os.SponsoringOrganizationUserId != default && - organizationUserIds.Contains(os.SponsoringOrganizationUserId ?? default)); - foreach (var sponsorship in sponsorships) - { - sponsorship.SponsoringOrganizationUserId = null; - sponsorship.FriendlyName = null; - } - dbContext.OrganizationUsers.RemoveRange(entities); await dbContext.SaveChangesAsync(); } diff --git a/src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs b/src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs index eac842db0..cd9cb7314 100644 --- a/src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs +++ b/src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserOrganizationDetailsViewQuery.cs @@ -1,5 +1,5 @@ using System.Linq; -using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Infrastructure.EntityFramework.Repositories.Queries { diff --git a/src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserUserViewQuery.cs b/src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserUserViewQuery.cs index 5b8f49c22..9f96ccd39 100644 --- a/src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserUserViewQuery.cs +++ b/src/Infrastructure.EntityFramework/Repositories/Queries/OrganizationUserUserViewQuery.cs @@ -1,5 +1,5 @@ using System.Linq; -using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; namespace Bit.Infrastructure.EntityFramework.Repositories.Queries { diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs index 5109f7a72..c57011d0e 100644 --- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs +++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs @@ -9,6 +9,7 @@ using Bit.Core.Enums; using Bit.Core.Identity; using Bit.Core.IdentityServer; using Bit.Core.Models.Business.Tokenables; +using Bit.Core.OrganizationFeatures; using Bit.Core.Repositories; using Bit.Core.Resources; using Bit.Core.Services; @@ -90,12 +91,11 @@ namespace Bit.SharedWeb.Utilities } } - public static void AddBaseServices(this IServiceCollection services) + public static void AddBaseServices(this IServiceCollection services, IGlobalSettings globalSettings) { services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); + services.AddOrganizationServices(globalSettings); services.AddScoped(); services.AddScoped(); services.AddScoped(); @@ -127,6 +127,8 @@ namespace Bit.SharedWeb.Utilities { // Required for UserService services.AddWebAuthn(globalSettings); + // Required for HTTP calls + services.AddHttpClient(); services.AddSingleton(); services.AddSingleton((serviceProvider) => diff --git a/src/Sql/Sql.sqlproj b/src/Sql/Sql.sqlproj index d94eed746..ee3768d8a 100644 --- a/src/Sql/Sql.sqlproj +++ b/src/Sql/Sql.sqlproj @@ -377,5 +377,18 @@ + + + + + + + + + + + + + diff --git a/src/Sql/dbo/Stored Procedures/OrganizationApiKey_Create.sql b/src/Sql/dbo/Stored Procedures/OrganizationApiKey_Create.sql new file mode 100644 index 000000000..e5bbfa09a --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationApiKey_Create.sql @@ -0,0 +1,27 @@ +CREATE PROCEDURE [dbo].[OrganizationApiKey_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @ApiKey VARCHAR(30), + @Type TINYINT, + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[OrganizationApiKey] + ( + [Id], + [OrganizationId], + [ApiKey], + [Type], + [RevisionDate] + ) + VALUES + ( + @Id, + @OrganizationId, + @ApiKey, + @Type, + @RevisionDate + ) +END diff --git a/src/Sql/dbo/Stored Procedures/OrganizationApiKey_DeleteById.sql b/src/Sql/dbo/Stored Procedures/OrganizationApiKey_DeleteById.sql new file mode 100644 index 000000000..8dd6db638 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationApiKey_DeleteById.sql @@ -0,0 +1,9 @@ +CREATE PROCEDURE [dbo].[OrganizationApiKey_DeleteById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DELETE FROM [dbo].[OrganizationApiKey] + WHERE [Id] = @Id +END diff --git a/src/Sql/dbo/Stored Procedures/OrganizationApiKey_OrganizationDeleted.sql b/src/Sql/dbo/Stored Procedures/OrganizationApiKey_OrganizationDeleted.sql new file mode 100644 index 000000000..768215939 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationApiKey_OrganizationDeleted.sql @@ -0,0 +1,12 @@ +CREATE PROCEDURE [dbo].[OrganizationApiKey_OrganizationDeleted] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DELETE + FROM + [dbo].[OrganizationApiKey] + WHERE + [OrganizationId] = @OrganizationId +END diff --git a/src/Sql/dbo/Stored Procedures/OrganizationApiKey_ReadManyByOrganizationIdType.sql b/src/Sql/dbo/Stored Procedures/OrganizationApiKey_ReadManyByOrganizationIdType.sql new file mode 100644 index 000000000..ddb8bac0d --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationApiKey_ReadManyByOrganizationIdType.sql @@ -0,0 +1,15 @@ +CREATE PROCEDURE [dbo].[OrganizationApiKey_ReadManyByOrganizationIdType] + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT = NULL +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationApiKeyView] + WHERE + [OrganizationId] = @OrganizationId AND + (@Type IS NULL OR [Type] = @Type) +END diff --git a/src/Sql/dbo/Stored Procedures/OrganizationApiKey_Update.sql b/src/Sql/dbo/Stored Procedures/OrganizationApiKey_Update.sql new file mode 100644 index 000000000..07ccd2569 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationApiKey_Update.sql @@ -0,0 +1,18 @@ +CREATE PROCEDURE [dbo].[OrganizationApiKey_Update] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @ApiKey VARCHAR(30), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[OrganizationApiKey] + SET + [ApiKey] = @ApiKey, + [RevisionDate] = @RevisionDate + WHERE + [Id] = @Id +END diff --git a/src/Sql/dbo/Stored Procedures/OrganizationConnection_Create.sql b/src/Sql/dbo/Stored Procedures/OrganizationConnection_Create.sql new file mode 100644 index 000000000..376c924a0 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationConnection_Create.sql @@ -0,0 +1,27 @@ +CREATE PROCEDURE [dbo].[OrganizationConnection_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Enabled BIT, + @Config NVARCHAR(MAX) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[OrganizationConnection] + ( + [Id], + [OrganizationId], + [Type], + [Enabled], + [Config] + ) + VALUES + ( + @Id, + @OrganizationId, + @Type, + @Enabled, + @Config + ) +END diff --git a/src/Sql/dbo/Stored Procedures/OrganizationConnection_DeleteById.sql b/src/Sql/dbo/Stored Procedures/OrganizationConnection_DeleteById.sql new file mode 100644 index 000000000..701b2a212 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationConnection_DeleteById.sql @@ -0,0 +1,11 @@ +CREATE PROCEDURE [dbo].[OrganizationConnection_DeleteById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DELETE FROM + [dbo].[OrganizationConnection] + WHERE + [Id] = @Id +END diff --git a/src/Sql/dbo/Stored Procedures/OrganizationConnection_OrganizationDeleted.sql b/src/Sql/dbo/Stored Procedures/OrganizationConnection_OrganizationDeleted.sql new file mode 100644 index 000000000..26d4e9867 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationConnection_OrganizationDeleted.sql @@ -0,0 +1,11 @@ +CREATE PROCEDURE [dbo].[OrganizationConnection_OrganizationDeleted] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DELETE FROM + [dbo].[OrganizationConnection] + WHERE + [OrganizationId] = @Id +END diff --git a/src/Sql/dbo/Stored Procedures/OrganizationConnection_ReadById.sql b/src/Sql/dbo/Stored Procedures/OrganizationConnection_ReadById.sql new file mode 100644 index 000000000..524153488 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationConnection_ReadById.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[OrganizationConnection_ReadById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationConnectionView] + WHERE + [Id] = @Id +END diff --git a/src/Sql/dbo/Stored Procedures/OrganizationConnection_ReadByOrganizationIdType.sql b/src/Sql/dbo/Stored Procedures/OrganizationConnection_ReadByOrganizationIdType.sql new file mode 100644 index 000000000..be1d6469a --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationConnection_ReadByOrganizationIdType.sql @@ -0,0 +1,15 @@ +CREATE PROCEDURE [dbo].[OrganizationConnection_ReadByOrganizationIdType] + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationConnectionView] + WHERE + [OrganizationId] = @OrganizationId AND + [Type] = @Type +END diff --git a/src/Sql/dbo/Stored Procedures/OrganizationConnection_Update.sql b/src/Sql/dbo/Stored Procedures/OrganizationConnection_Update.sql new file mode 100644 index 000000000..9658eff6c --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationConnection_Update.sql @@ -0,0 +1,20 @@ +CREATE PROCEDURE [dbo].[OrganizationConnection_Update] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Enabled BIT, + @Config NVARCHAR(MAX) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[OrganizationConnection] + SET + [OrganizationId] = @OrganizationId, + [Type] = @Type, + [Enabled] = @Enabled, + [Config] = @Config + WHERE + [Id] = @Id +END diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Create.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Create.sql index f26e9a35b..5a349d258 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Create.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Create.sql @@ -1,15 +1,14 @@ CREATE PROCEDURE [dbo].[OrganizationSponsorship_Create] @Id UNIQUEIDENTIFIER OUTPUT, - @InstallationId UNIQUEIDENTIFIER, @SponsoringOrganizationId UNIQUEIDENTIFIER, @SponsoringOrganizationUserID UNIQUEIDENTIFIER, @SponsoredOrganizationId UNIQUEIDENTIFIER, + @FriendlyName NVARCHAR(256), @OfferedToEmail NVARCHAR(256), @PlanSponsorshipType TINYINT, - @CloudSponsor BIT, + @ToDelete BIT, @LastSyncDate DATETIME2 (7), - @TimesRenewedWithoutValidation TINYINT, - @SponsorshipLapsedDate DATETIME2 (7) + @ValidUntil DATETIME2 (7) AS BEGIN SET NOCOUNT ON @@ -17,30 +16,28 @@ BEGIN INSERT INTO [dbo].[OrganizationSponsorship] ( [Id], - [InstallationId], [SponsoringOrganizationId], [SponsoringOrganizationUserID], [SponsoredOrganizationId], + [FriendlyName], [OfferedToEmail], [PlanSponsorshipType], - [CloudSponsor], + [ToDelete], [LastSyncDate], - [TimesRenewedWithoutValidation], - [SponsorshipLapsedDate] + [ValidUntil] ) VALUES ( @Id, - @InstallationId, @SponsoringOrganizationId, @SponsoringOrganizationUserID, @SponsoredOrganizationId, + @FriendlyName, @OfferedToEmail, @PlanSponsorshipType, - @CloudSponsor, + @ToDelete, @LastSyncDate, - @TimesRenewedWithoutValidation, - @SponsorshipLapsedDate + @ValidUntil ) END GO diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_CreateMany.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_CreateMany.sql new file mode 100644 index 000000000..69dbc42a2 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_CreateMany.sql @@ -0,0 +1,33 @@ +CREATE PROCEDURE [dbo].[OrganizationSponsorship_CreateMany] + @OrganizationSponsorshipsInput [dbo].[OrganizationSponsorshipType] READONLY +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[OrganizationSponsorship] + ( + [Id], + [SponsoringOrganizationId], + [SponsoringOrganizationUserID], + [SponsoredOrganizationId], + [FriendlyName], + [OfferedToEmail], + [PlanSponsorshipType], + [ToDelete], + [LastSyncDate], + [ValidUntil] + ) + SELECT + OS.[Id], + OS.[SponsoringOrganizationId], + OS.[SponsoringOrganizationUserID], + OS.[SponsoredOrganizationId], + OS.[FriendlyName], + OS.[OfferedToEmail], + OS.[PlanSponsorshipType], + OS.[ToDelete], + OS.[LastSyncDate], + OS.[ValidUntil] + FROM + @OrganizationSponsorshipsInput OS +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_DeleteByIds.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_DeleteByIds.sql new file mode 100644 index 000000000..337ad2a39 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_DeleteByIds.sql @@ -0,0 +1,23 @@ +CREATE PROCEDURE [dbo].[OrganizationSponsorship_DeleteByIds] + @Ids [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + DECLARE @BatchSize INT = 100 + + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION OrgSponsorship_DeleteMany + + DELETE TOP(@BatchSize) OS + FROM + [dbo].[OrganizationSponsorship] OS + INNER JOIN + @Ids I ON I.Id = OS.Id + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION OrgSponsorship_DeleteMany + END +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_DeleteExpired.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_DeleteExpired.sql new file mode 100644 index 000000000..2ac02be65 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_DeleteExpired.sql @@ -0,0 +1,19 @@ +CREATE PROCEDURE [dbo].[OrganizationSponsorship_DeleteExpired] + @ValidUntilBeforeDate DATETIME2 (7) +AS +BEGIN + SET NOCOUNT ON + + DECLARE @BatchSize INT = 100 + + WHILE @BatchSize > 0 + BEGIN + DELETE TOP(@BatchSize) + FROM + [dbo].[OrganizationSponsorship] + WHERE + [ValidUntil] < @ValidUntilBeforeDate + + SET @BatchSize = @@ROWCOUNT + END +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoringOrganizationId.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoringOrganizationId.sql new file mode 100644 index 000000000..fa9c7447a --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_ReadBySponsoringOrganizationId.sql @@ -0,0 +1,13 @@ +CREATE PROCEDURE [dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationId] + @SponsoringOrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationSponsorshipView] + WHERE + [SponsoringOrganizationId] = @SponsoringOrganizationId +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Update.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Update.sql index 88ac04856..128c5b039 100644 --- a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Update.sql +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_Update.sql @@ -1,15 +1,14 @@ CREATE PROCEDURE [dbo].[OrganizationSponsorship_Update] @Id UNIQUEIDENTIFIER, - @InstallationId UNIQUEIDENTIFIER, @SponsoringOrganizationId UNIQUEIDENTIFIER, @SponsoringOrganizationUserID UNIQUEIDENTIFIER, @SponsoredOrganizationId UNIQUEIDENTIFIER, + @FriendlyName NVARCHAR(256), @OfferedToEmail NVARCHAR(256), @PlanSponsorshipType TINYINT, - @CloudSponsor BIT, + @ToDelete BIT, @LastSyncDate DATETIME2 (7), - @TimesRenewedWithoutValidation TINYINT, - @SponsorshipLapsedDate DATETIME2 (7) + @ValidUntil DATETIME2 (7) AS BEGIN SET NOCOUNT ON @@ -17,16 +16,15 @@ BEGIN UPDATE [dbo].[OrganizationSponsorship] SET - [InstallationId] = @InstallationId, [SponsoringOrganizationId] = @SponsoringOrganizationId, [SponsoringOrganizationUserID] = @SponsoringOrganizationUserID, [SponsoredOrganizationId] = @SponsoredOrganizationId, + [FriendlyName] = @FriendlyName, [OfferedToEmail] = @OfferedToEmail, [PlanSponsorshipType] = @PlanSponsorshipType, - [CloudSponsor] = @CloudSponsor, + [ToDelete] = @ToDelete, [LastSyncDate] = @LastSyncDate, - [TimesRenewedWithoutValidation] = @TimesRenewedWithoutValidation, - [SponsorshipLapsedDate] = @SponsorshipLapsedDate + [ValidUntil] = @ValidUntil WHERE [Id] = @Id END diff --git a/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_UpdateMany.sql b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_UpdateMany.sql new file mode 100644 index 000000000..082773873 --- /dev/null +++ b/src/Sql/dbo/Stored Procedures/OrganizationSponsorship_UpdateMany.sql @@ -0,0 +1,25 @@ +CREATE PROCEDURE [dbo].[OrganizationSponsorship_UpdateMany] + @OrganizationSponsorshipsInput [dbo].[OrganizationSponsorshipType] READONLY +AS +BEGIN + SET NOCOUNT ON + + UPDATE + OS + SET + [Id] = OSI.[Id], + [SponsoringOrganizationId] = OSI.[SponsoringOrganizationId], + [SponsoringOrganizationUserID] = OSI.[SponsoringOrganizationUserID], + [SponsoredOrganizationId] = OSI.[SponsoredOrganizationId], + [FriendlyName] = OSI.[FriendlyName], + [OfferedToEmail] = OSI.[OfferedToEmail], + [PlanSponsorshipType] = OSI.[PlanSponsorshipType], + [ToDelete] = OSI.[ToDelete], + [LastSyncDate] = OSI.[LastSyncDate], + [ValidUntil] = OSI.[ValidUntil] + FROM + [dbo].[OrganizationSponsorship] OS + INNER JOIN + @OrganizationSponsorshipsInput OSI ON OS.Id = OSI.Id + +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Organization_Create.sql b/src/Sql/dbo/Stored Procedures/Organization_Create.sql index 3f467e4a6..51c2eaf26 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_Create.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_Create.sql @@ -32,7 +32,6 @@ @ReferenceData VARCHAR(MAX), @Enabled BIT, @LicenseKey VARCHAR(100), - @ApiKey VARCHAR(30), @PublicKey VARCHAR(MAX), @PrivateKey VARCHAR(MAX), @TwoFactorProviders NVARCHAR(MAX), @@ -81,7 +80,6 @@ BEGIN [ReferenceData], [Enabled], [LicenseKey], - [ApiKey], [PublicKey], [PrivateKey], [TwoFactorProviders], @@ -127,7 +125,6 @@ BEGIN @ReferenceData, @Enabled, @LicenseKey, - @ApiKey, @PublicKey, @PrivateKey, @TwoFactorProviders, @@ -138,4 +135,4 @@ BEGIN @MaxAutoscaleSeats, @UseKeyConnector ) -END +END \ No newline at end of file diff --git a/src/Sql/dbo/Stored Procedures/Organization_DeleteById.sql b/src/Sql/dbo/Stored Procedures/Organization_DeleteById.sql index d32f7d360..ab5217a12 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_DeleteById.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_DeleteById.sql @@ -57,7 +57,9 @@ BEGIN WHERE [OrganizationId] = @Id - EXEC[dbo].[OrganizationSponsorship_OrganizationDeleted] @Id + EXEC [dbo].[OrganizationApiKey_OrganizationDeleted] @Id + EXEC [dbo].[OrganizationConnection_OrganizationDeleted] @Id + EXEC [dbo].[OrganizationSponsorship_OrganizationDeleted] @Id DELETE FROM diff --git a/src/Sql/dbo/Stored Procedures/Organization_Update.sql b/src/Sql/dbo/Stored Procedures/Organization_Update.sql index a0ff29c7e..2339518af 100644 --- a/src/Sql/dbo/Stored Procedures/Organization_Update.sql +++ b/src/Sql/dbo/Stored Procedures/Organization_Update.sql @@ -32,7 +32,6 @@ @ReferenceData VARCHAR(MAX), @Enabled BIT, @LicenseKey VARCHAR(100), - @ApiKey VARCHAR(30), @PublicKey VARCHAR(MAX), @PrivateKey VARCHAR(MAX), @TwoFactorProviders NVARCHAR(MAX), @@ -81,7 +80,6 @@ BEGIN [ReferenceData] = @ReferenceData, [Enabled] = @Enabled, [LicenseKey] = @LicenseKey, - [ApiKey] = @ApiKey, [PublicKey] = @PublicKey, [PrivateKey] = @PrivateKey, [TwoFactorProviders] = @TwoFactorProviders, diff --git a/src/Sql/dbo/Tables/Event.sql b/src/Sql/dbo/Tables/Event.sql index e9e8754fd..e8769631c 100644 --- a/src/Sql/dbo/Tables/Event.sql +++ b/src/Sql/dbo/Tables/Event.sql @@ -3,6 +3,7 @@ [Type] INT NOT NULL, [UserId] UNIQUEIDENTIFIER NULL, [OrganizationId] UNIQUEIDENTIFIER NULL, + [InstallationId] UNIQUEIDENTIFIER NULL, [CipherId] UNIQUEIDENTIFIER NULL, [CollectionId] UNIQUEIDENTIFIER NULL, [PolicyId] UNIQUEIDENTIFIER NULL, diff --git a/src/Sql/dbo/Tables/Organization.sql b/src/Sql/dbo/Tables/Organization.sql index a40a462b2..3985c85d6 100644 --- a/src/Sql/dbo/Tables/Organization.sql +++ b/src/Sql/dbo/Tables/Organization.sql @@ -32,16 +32,15 @@ [ReferenceData] NVARCHAR (MAX) NULL, [Enabled] BIT NOT NULL, [LicenseKey] VARCHAR (100) NULL, - [ApiKey] VARCHAR (30) NOT NULL, [PublicKey] VARCHAR (MAX) NULL, [PrivateKey] VARCHAR (MAX) NULL, [TwoFactorProviders] NVARCHAR (MAX) NULL, [ExpirationDate] DATETIME2 (7) NULL, [CreationDate] DATETIME2 (7) NOT NULL, - [RevisionDate] DATETIME2 (7) NOT NULL, - [OwnersNotifiedOfAutoscaling] DATETIME2(7) NULL, - [MaxAutoscaleSeats] INT NULL, - [UseKeyConnector] BIT NOT NULL, + [RevisionDate] DATETIME2 (7) NOT NULL, + [OwnersNotifiedOfAutoscaling] DATETIME2(7) NULL, + [MaxAutoscaleSeats] INT NULL, + [UseKeyConnector] BIT NOT NULL, CONSTRAINT [PK_Organization] PRIMARY KEY CLUSTERED ([Id] ASC) ); @@ -55,4 +54,3 @@ GO CREATE UNIQUE NONCLUSTERED INDEX [IX_Organization_Identifier] ON [dbo].[Organization]([Identifier] ASC) WHERE [Identifier] IS NOT NULL; - diff --git a/src/Sql/dbo/Tables/OrganizationApiKey.sql b/src/Sql/dbo/Tables/OrganizationApiKey.sql new file mode 100644 index 000000000..fbae51df3 --- /dev/null +++ b/src/Sql/dbo/Tables/OrganizationApiKey.sql @@ -0,0 +1,14 @@ +CREATE TABLE [dbo].[OrganizationApiKey] ( + [Id] UNIQUEIDENTIFIER, + [OrganizationId] UNIQUEIDENTIFIER NOT NULL, + [Type] TINYINT NOT NULL, + [ApiKey] VARCHAR(30) NOT NULL, + [RevisionDate] DATETIME2(7) NOT NULL + CONSTRAINT [PK_OrganizationApiKey] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_OrganizationApiKey_OrganizationId] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) +); + +GO + +CREATE NONCLUSTERED INDEX [IX_OrganizationApiKey_OrganizationId] + ON [dbo].[OrganizationApiKey]([OrganizationId] ASC); diff --git a/src/Sql/dbo/Tables/OrganizationConnection.sql b/src/Sql/dbo/Tables/OrganizationConnection.sql new file mode 100644 index 000000000..3577b859a --- /dev/null +++ b/src/Sql/dbo/Tables/OrganizationConnection.sql @@ -0,0 +1,13 @@ +CREATE TABLE [dbo].[OrganizationConnection] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [OrganizationId] UNIQUEIDENTIFIER NOT NULL, + [Type] TINYINT NOT NULL, + [Enabled] BIT NOT NULL, + [Config] NVARCHAR (MAX) NULL, + CONSTRAINT [PK_OrganizationConnection] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_OrganizationConnection_OrganizationId] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) +); +GO + +CREATE NONCLUSTERED INDEX [IX_OrganizationConnection_OrganizationId] + ON [dbo].[OrganizationConnection]([OrganizationId] ASC); diff --git a/src/Sql/dbo/Tables/OrganizationSponsorship.sql b/src/Sql/dbo/Tables/OrganizationSponsorship.sql index 391ab599b..79c92edce 100644 --- a/src/Sql/dbo/Tables/OrganizationSponsorship.sql +++ b/src/Sql/dbo/Tables/OrganizationSponsorship.sql @@ -1,27 +1,20 @@ CREATE TABLE [dbo].[OrganizationSponsorship] ( [Id] UNIQUEIDENTIFIER NOT NULL, - [InstallationId] UNIQUEIDENTIFIER NULL, [SponsoringOrganizationId] UNIQUEIDENTIFIER NULL, - [SponsoringOrganizationUserID] UNIQUEIDENTIFIER NULL, + [SponsoringOrganizationUserID] UNIQUEIDENTIFIER NOT NULL, [SponsoredOrganizationId] UNIQUEIDENTIFIER NULL, + [FriendlyName] NVARCHAR(256) NULL, [OfferedToEmail] NVARCHAR (256) NULL, [PlanSponsorshipType] TINYINT NULL, - [CloudSponsor] BIT NULL, + [ToDelete] BIT NULL, [LastSyncDate] DATETIME2 (7) NULL, - [TimesRenewedWithoutValidation] TINYINT DEFAULT 0, - [SponsorshipLapsedDate] DATETIME2 (7) NULL, + [ValidUntil] DATETIME2 (7) NULL, CONSTRAINT [PK_OrganizationSponsorship] PRIMARY KEY CLUSTERED ([Id] ASC), - CONSTRAINT [FK_OrganizationSponsorship_InstallationId] FOREIGN KEY ([InstallationId]) REFERENCES [dbo].[Installation] ([Id]), CONSTRAINT [FK_OrganizationSponsorship_SponsoringOrg] FOREIGN KEY ([SponsoringOrganizationId]) REFERENCES [dbo].[Organization] ([Id]), CONSTRAINT [FK_OrganizationSponsorship_SponsoredOrg] FOREIGN KEY ([SponsoredOrganizationId]) REFERENCES [dbo].[Organization] ([Id]), ); -GO -CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_InstallationId] - ON [dbo].[OrganizationSponsorship]([InstallationId] ASC) - WHERE [InstallationId] IS NOT NULL; - GO CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationId] ON [dbo].[OrganizationSponsorship]([SponsoringOrganizationId] ASC) @@ -29,8 +22,7 @@ CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationId] GO CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationUserId] - ON [dbo].[OrganizationSponsorship]([SponsoringOrganizationUserID] ASC) - WHERE [SponsoringOrganizationUserID] IS NOT NULL; + ON [dbo].[OrganizationSponsorship]([SponsoringOrganizationUserID] ASC); GO CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_OfferedToEmail] diff --git a/src/Sql/dbo/User Defined Types/OrganizationSponsorshipType.sql b/src/Sql/dbo/User Defined Types/OrganizationSponsorshipType.sql new file mode 100644 index 000000000..123fa8081 --- /dev/null +++ b/src/Sql/dbo/User Defined Types/OrganizationSponsorshipType.sql @@ -0,0 +1,12 @@ +CREATE TYPE [dbo].[OrganizationSponsorshipType] AS TABLE( + [Id] UNIQUEIDENTIFIER, + [SponsoringOrganizationId] UNIQUEIDENTIFIER, + [SponsoringOrganizationUserID] UNIQUEIDENTIFIER, + [SponsoredOrganizationId] UNIQUEIDENTIFIER, + [FriendlyName] NVARCHAR(256), + [OfferedToEmail] VARCHAR(256), + [PlanSponsorshipType] TINYINT, + [LastSyncDate] DATETIME2(7), + [ValidUntil] DATETIME2(7), + [ToDelete] BIT +) \ No newline at end of file diff --git a/src/Sql/dbo/Views/OrganizationApiKeyView.sql b/src/Sql/dbo/Views/OrganizationApiKeyView.sql new file mode 100644 index 000000000..cb4d985b3 --- /dev/null +++ b/src/Sql/dbo/Views/OrganizationApiKeyView.sql @@ -0,0 +1,6 @@ +CREATE VIEW [dbo].[OrganizationApiKeyView] +AS +SELECT + * +FROM + [dbo].[OrganizationApiKey] diff --git a/src/Sql/dbo/Views/OrganizationConnectionView.sql b/src/Sql/dbo/Views/OrganizationConnectionView.sql new file mode 100644 index 000000000..dfbc7a605 --- /dev/null +++ b/src/Sql/dbo/Views/OrganizationConnectionView.sql @@ -0,0 +1,6 @@ +CREATE VIEW [dbo].[OrganizationConnectionView] +AS +SELECT + * +FROM + [dbo].[OrganizationConnection] diff --git a/test/Api.Test/AutoFixture/Attributes/ControllerCustomizeAttribute.cs b/test/Api.Test/AutoFixture/Attributes/ControllerCustomizeAttribute.cs index 951f0d582..353b2b752 100644 --- a/test/Api.Test/AutoFixture/Attributes/ControllerCustomizeAttribute.cs +++ b/test/Api.Test/AutoFixture/Attributes/ControllerCustomizeAttribute.cs @@ -4,9 +4,17 @@ using Bit.Test.Common.AutoFixture.Attributes; namespace Bit.Api.Test.AutoFixture.Attributes { + /// + /// Disables setting of Auto Properties on the Controller to avoid ASP.net initialization errors from a mock environment. Still sets constructor dependencies. + /// public class ControllerCustomizeAttribute : BitCustomizeAttribute { private readonly Type _controllerType; + + /// + /// Initialize an instance of the ControllerCustomizeAttribute class + /// + /// The Type of the controller to allow autofixture to create public ControllerCustomizeAttribute(Type controllerType) { _controllerType = controllerType; diff --git a/test/Api.Test/Controllers/OrganizationConnectionsControllerTests.cs b/test/Api.Test/Controllers/OrganizationConnectionsControllerTests.cs new file mode 100644 index 000000000..7d1268460 --- /dev/null +++ b/test/Api.Test/Controllers/OrganizationConnectionsControllerTests.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Bit.Api.Controllers; +using Bit.Api.Models.Request.Organizations; +using Bit.Api.Models.Response.Organizations; +using Bit.Api.Test.AutoFixture.Attributes; +using Bit.Core.Context; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Business; +using Bit.Core.Models.OrganizationConnectionConfigs; +using Bit.Core.OrganizationFeatures.OrganizationConnections.Interfaces; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Settings; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using NSubstitute; +using Xunit; + +namespace Bit.Api.Test.Controllers +{ + [ControllerCustomize(typeof(OrganizationConnectionsController))] + [SutProviderCustomize] + [JsonDocumentCustomize] + public class OrganizationConnectionsControllerTests + { + public static IEnumerable ConnectionTypes => + Enum.GetValues().Select(p => new object[] { p }); + + + [Theory] + [BitAutoData(true, true)] + [BitAutoData(false, true)] + [BitAutoData(true, false)] + [BitAutoData(false, false)] + public void ConnectionEnabled_RequiresBothSelfHostAndCommunications(bool selfHosted, bool enableCloudCommunication, SutProvider sutProvider) + { + var globalSettingsMock = sutProvider.GetDependency(); + globalSettingsMock.SelfHosted.Returns(selfHosted); + globalSettingsMock.EnableCloudCommunication.Returns(enableCloudCommunication); + + Action assert = selfHosted && enableCloudCommunication ? Assert.True : Assert.False; + + var result = sutProvider.Sut.ConnectionsEnabled(); + + assert(result); + } + + [Theory] + [BitAutoData] + public async Task CreateConnection_RequiresOwnerPermissions(SutProvider sutProvider) + { + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.CreateConnection(null)); + + Assert.Contains("Only the owner of an organization can create a connection.", exception.Message); + } + + [Theory] + [BitMemberAutoData(nameof(ConnectionTypes))] + public async Task CreateConnection_OnlyOneConnectionOfEachType(OrganizationConnectionType type, + OrganizationConnectionRequestModel model, BillingSyncConfig config, Guid existingEntityId, + SutProvider sutProvider) + { + model.Type = type; + model.Config = JsonDocumentFromObject(config); + var typedModel = new OrganizationConnectionRequestModel(model); + var existing = typedModel.ToData(existingEntityId).ToEntity(); + + sutProvider.GetDependency().OrganizationOwner(model.OrganizationId).Returns(true); + + sutProvider.GetDependency().GetByOrganizationIdTypeAsync(model.OrganizationId, type).Returns(new[] { existing }); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.CreateConnection(model)); + + Assert.Contains($"The requested organization already has a connection of type {model.Type}. Only one of each connection type may exist per organization.", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task CreateConnection_Success(OrganizationConnectionRequestModel model, BillingSyncConfig config, + Guid cloudOrgId, SutProvider sutProvider) + { + model.Config = JsonDocumentFromObject(config); + var typedModel = new OrganizationConnectionRequestModel(model); + typedModel.ParsedConfig.CloudOrganizationId = cloudOrgId; + + sutProvider.GetDependency().CreateAsync(default) + .ReturnsForAnyArgs(typedModel.ToData(Guid.NewGuid()).ToEntity()); + sutProvider.GetDependency().OrganizationOwner(model.OrganizationId).Returns(true); + sutProvider.GetDependency() + .ReadOrganizationLicenseAsync(Arg.Any()) + .Returns(new OrganizationLicense + { + Id = cloudOrgId, + }); + + await sutProvider.Sut.CreateConnection(model); + + await sutProvider.GetDependency().Received(1) + .CreateAsync(Arg.Is(AssertHelper.AssertPropertyEqual(typedModel.ToData()))); + } + + [Theory] + [BitAutoData] + public async Task UpdateConnection_RequiresOwnerPermissions(SutProvider sutProvider) + { + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateConnection(default, null)); + + Assert.Contains("Only the owner of an organization can update a connection.", exception.Message); + } + + [Theory] + [BitMemberAutoData(nameof(ConnectionTypes))] + public async Task UpdateConnection_OnlyOneConnectionOfEachType(OrganizationConnectionType type, + OrganizationConnection existing1, OrganizationConnection existing2, BillingSyncConfig config, + SutProvider sutProvider) + { + existing1.Config = JsonSerializer.Serialize(config); + var typedModel = RequestModelFromEntity(existing1); + + sutProvider.GetDependency().OrganizationOwner(typedModel.OrganizationId).Returns(true); + + sutProvider.GetDependency().GetByOrganizationIdTypeAsync(typedModel.OrganizationId, type).Returns(new[] { existing1, existing2 }); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateConnection(existing1.Id, typedModel)); + + Assert.Contains($"The requested organization already has a connection of type {typedModel.Type}. Only one of each connection type may exist per organization.", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task UpdateConnection_Success(OrganizationConnection existing, BillingSyncConfig config, + OrganizationConnection updated, + SutProvider sutProvider) + { + updated.Config = JsonSerializer.Serialize(config); + updated.Id = existing.Id; + var model = RequestModelFromEntity(updated); + + sutProvider.GetDependency().OrganizationOwner(model.OrganizationId).Returns(true); + sutProvider.GetDependency().GetByOrganizationIdTypeAsync(model.OrganizationId, model.Type).Returns(new[] { existing }); + sutProvider.GetDependency().UpdateAsync(default).ReturnsForAnyArgs(updated); + + var expected = new OrganizationConnectionResponseModel(updated, typeof(BillingSyncConfig)); + var result = await sutProvider.Sut.UpdateConnection(existing.Id, model); + + AssertHelper.AssertPropertyEqual(expected, result); + await sutProvider.GetDependency().Received(1) + .UpdateAsync(Arg.Is(AssertHelper.AssertPropertyEqual(model.ToData(updated.Id)))); + } + + [Theory] + [BitAutoData] + public async Task GetConnection_RequiresOwnerPermissions(Guid connectionId, SutProvider sutProvider) + { + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.GetConnection(connectionId, OrganizationConnectionType.CloudBillingSync)); + + Assert.Contains("Only the owner of an organization can retrieve a connection.", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task GetConnection_Success(OrganizationConnection connection, BillingSyncConfig config, + SutProvider sutProvider) + { + connection.Config = JsonSerializer.Serialize(config); + + sutProvider.GetDependency().GetByOrganizationIdTypeAsync(connection.OrganizationId, connection.Type).Returns(new[] { connection }); + sutProvider.GetDependency().OrganizationOwner(connection.OrganizationId).Returns(true); + + var expected = new OrganizationConnectionResponseModel(connection, typeof(BillingSyncConfig)); + var actual = await sutProvider.Sut.GetConnection(connection.OrganizationId, connection.Type); + + AssertHelper.AssertPropertyEqual(expected, actual); + } + + [Theory] + [BitAutoData] + public async Task DeleteConnection_NotFound(Guid connectionId, + SutProvider sutProvider) + { + await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteConnection(connectionId)); + } + + [Theory] + [BitAutoData] + public async Task DeleteConnection_RequiresOwnerPermissions(OrganizationConnection connection, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetByIdAsync(connection.Id).Returns(connection); + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.DeleteConnection(connection.Id)); + + Assert.Contains("Only the owner of an organization can remove a connection.", exception.Message); + } + + [Theory] + [BitAutoData] + public async Task DeleteConnection_Success(OrganizationConnection connection, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetByIdAsync(connection.Id).Returns(connection); + sutProvider.GetDependency().OrganizationOwner(connection.OrganizationId).Returns(true); + + await sutProvider.Sut.DeleteConnection(connection.Id); + + await sutProvider.GetDependency().DeleteAsync(connection); + } + + private static OrganizationConnectionRequestModel RequestModelFromEntity(OrganizationConnection entity) + { + return new(new OrganizationConnectionRequestModel() + { + Type = entity.Type, + OrganizationId = entity.OrganizationId, + Enabled = entity.Enabled, + Config = JsonDocument.Parse(entity.Config), + }); + } + + private static JsonDocument JsonDocumentFromObject(T obj) => JsonDocument.Parse(JsonSerializer.Serialize(obj)); + } +} diff --git a/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs index d925e8adc..9364b4be2 100644 --- a/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs +++ b/test/Api.Test/Controllers/OrganizationSponsorshipsControllerTests.cs @@ -9,6 +9,7 @@ using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Enums; using Bit.Core.Exceptions; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Utilities; @@ -44,14 +45,14 @@ namespace Bit.Api.Test.Controllers sutProvider.GetDependency().UserId.Returns(user.Id); sutProvider.GetDependency().GetUserByIdAsync(user.Id) .Returns(user); - sutProvider.GetDependency().ValidateRedemptionTokenAsync(sponsorshipToken, + sutProvider.GetDependency().ValidateRedemptionTokenAsync(sponsorshipToken, user.Email).Returns((false, null)); var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.RedeemSponsorship(sponsorshipToken, model)); Assert.Contains("Failed to parse sponsorship token.", exception.Message); - await sutProvider.GetDependency() + await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() .SetUpSponsorshipAsync(default, default); } @@ -65,7 +66,7 @@ namespace Bit.Api.Test.Controllers sutProvider.GetDependency().UserId.Returns(user.Id); sutProvider.GetDependency().GetUserByIdAsync(user.Id) .Returns(user); - sutProvider.GetDependency().ValidateRedemptionTokenAsync(sponsorshipToken, + sutProvider.GetDependency().ValidateRedemptionTokenAsync(sponsorshipToken, user.Email).Returns((true, sponsorship)); sutProvider.GetDependency().OrganizationOwner(model.SponsoredOrganizationId).Returns(false); @@ -73,7 +74,7 @@ namespace Bit.Api.Test.Controllers sutProvider.Sut.RedeemSponsorship(sponsorshipToken, model)); Assert.Contains("Can only redeem sponsorship for an organization you own.", exception.Message); - await sutProvider.GetDependency() + await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() .SetUpSponsorshipAsync(default, default); } @@ -87,14 +88,14 @@ namespace Bit.Api.Test.Controllers sutProvider.GetDependency().UserId.Returns(user.Id); sutProvider.GetDependency().GetUserByIdAsync(user.Id) .Returns(user); - sutProvider.GetDependency().ValidateRedemptionTokenAsync(sponsorshipToken, + sutProvider.GetDependency().ValidateRedemptionTokenAsync(sponsorshipToken, user.Email).Returns((true, sponsorship)); sutProvider.GetDependency().OrganizationOwner(model.SponsoredOrganizationId).Returns(true); sutProvider.GetDependency().GetByIdAsync(model.SponsoredOrganizationId).Returns(sponsoringOrganization); await sutProvider.Sut.RedeemSponsorship(sponsorshipToken, model); - await sutProvider.GetDependency().Received(1) + await sutProvider.GetDependency().Received(1) .SetUpSponsorshipAsync(sponsorship, sponsoringOrganization); } @@ -106,12 +107,12 @@ namespace Bit.Api.Test.Controllers sutProvider.GetDependency().UserId.Returns(user.Id); sutProvider.GetDependency().GetUserByIdAsync(user.Id) .Returns(user); - sutProvider.GetDependency() + sutProvider.GetDependency() .ValidateRedemptionTokenAsync(sponsorshipToken, user.Email).Returns((true, sponsorship)); await sutProvider.Sut.PreValidateSponsorshipToken(sponsorshipToken); - await sutProvider.GetDependency().Received(1) + await sutProvider.GetDependency().Received(1) .ValidateRedemptionTokenAsync(sponsorshipToken, user.Email); } @@ -128,9 +129,9 @@ namespace Bit.Api.Test.Controllers sutProvider.Sut.RevokeSponsorship(sponsoringOrgUser.Id)); Assert.Contains("Can only revoke a sponsorship you granted.", exception.Message); - await sutProvider.GetDependency() + await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() - .RemoveSponsorshipAsync(default, default); + .RemoveSponsorshipAsync(default); } [Theory] @@ -144,9 +145,9 @@ namespace Bit.Api.Test.Controllers sutProvider.Sut.RemoveSponsorship(sponsoredOrg.Id)); Assert.Contains("Only the owner of an organization can remove sponsorship.", exception.Message); - await sutProvider.GetDependency() + await sutProvider.GetDependency() .DidNotReceiveWithAnyArgs() - .RemoveSponsorshipAsync(default, default); + .RemoveSponsorshipAsync(default); } } } diff --git a/test/Api.Test/Controllers/OrganizationsControllerTests.cs b/test/Api.Test/Controllers/OrganizationsControllerTests.cs index 3f2576d07..323d10270 100644 --- a/test/Api.Test/Controllers/OrganizationsControllerTests.cs +++ b/test/Api.Test/Controllers/OrganizationsControllerTests.cs @@ -7,6 +7,7 @@ using Bit.Core.Context; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Models.Data; +using Bit.Core.OrganizationFeatures.OrganizationApiKeys.Interfaces; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -27,6 +28,9 @@ namespace Bit.Api.Test.Controllers private readonly ISsoConfigRepository _ssoConfigRepository; private readonly ISsoConfigService _ssoConfigService; private readonly IUserService _userService; + private readonly IGetOrganizationApiKeyCommand _getOrganizationApiKeyCommand; + private readonly IRotateOrganizationApiKeyCommand _rotateOrganizationApiKeyCommand; + private readonly IOrganizationApiKeyRepository _organizationApiKeyRepository; private readonly OrganizationsController _sut; @@ -41,11 +45,15 @@ namespace Bit.Api.Test.Controllers _policyRepository = Substitute.For(); _ssoConfigRepository = Substitute.For(); _ssoConfigService = Substitute.For(); + _getOrganizationApiKeyCommand = Substitute.For(); + _rotateOrganizationApiKeyCommand = Substitute.For(); + _organizationApiKeyRepository = Substitute.For(); _userService = Substitute.For(); _sut = new OrganizationsController(_organizationRepository, _organizationUserRepository, _policyRepository, _organizationService, _userService, _paymentService, _currentContext, - _ssoConfigRepository, _ssoConfigService, _globalSettings); + _ssoConfigRepository, _ssoConfigService, _getOrganizationApiKeyCommand, _rotateOrganizationApiKeyCommand, + _organizationApiKeyRepository, _globalSettings); } public void Dispose() @@ -112,4 +120,3 @@ namespace Bit.Api.Test.Controllers } } } - diff --git a/test/Api.Test/packages.lock.json b/test/Api.Test/packages.lock.json index b76c091df..3c2e3db32 100644 --- a/test/Api.Test/packages.lock.json +++ b/test/Api.Test/packages.lock.json @@ -309,6 +309,15 @@ "IdentityModel": "4.3.0" } }, + "Kralizek.AutoFixture.Extensions.MockHttp": { + "type": "Transitive", + "resolved": "1.2.0", + "contentHash": "6zmks7/5mVczazv910N7V2EdiU6B+rY61lwdgVO0o2iZuTI6KI3T+Hgkrjv0eGOKYucq2OMC+gnAc5Ej2ajoTQ==", + "dependencies": { + "AutoFixture": "4.11.0", + "RichardSzalay.MockHttp": "6.0.0" + } + }, "libsodium": { "type": "Transitive", "resolved": "1.0.18", @@ -1591,6 +1600,11 @@ "System.Diagnostics.DiagnosticSource": "4.7.1" } }, + "RichardSzalay.MockHttp": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "bStGNqIX/MGYtML7K3EzdsE/k5HGVAcg7XgN23TQXGXqxNC9fvYFR94fA0sGM5hAT36R+BBGet6ZDQxXL/IPxg==" + }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", "resolved": "4.3.2", @@ -3540,6 +3554,7 @@ "AutoFixture.AutoNSubstitute": "4.14.0", "AutoFixture.Xunit2": "4.14.0", "Core": "1.47.1", + "Kralizek.AutoFixture.Extensions.MockHttp": "1.2.0", "Microsoft.NET.Test.Sdk": "16.6.1", "NSubstitute": "4.2.2", "xunit": "2.4.1" diff --git a/test/Billing.Test/packages.lock.json b/test/Billing.Test/packages.lock.json index 5d7a9f52e..89b8bd4ce 100644 --- a/test/Billing.Test/packages.lock.json +++ b/test/Billing.Test/packages.lock.json @@ -317,6 +317,15 @@ "IdentityModel": "4.3.0" } }, + "Kralizek.AutoFixture.Extensions.MockHttp": { + "type": "Transitive", + "resolved": "1.2.0", + "contentHash": "6zmks7/5mVczazv910N7V2EdiU6B+rY61lwdgVO0o2iZuTI6KI3T+Hgkrjv0eGOKYucq2OMC+gnAc5Ej2ajoTQ==", + "dependencies": { + "AutoFixture": "4.11.0", + "RichardSzalay.MockHttp": "6.0.0" + } + }, "libsodium": { "type": "Transitive", "resolved": "1.0.18", @@ -1663,6 +1672,11 @@ "System.Diagnostics.DiagnosticSource": "4.7.1" } }, + "RichardSzalay.MockHttp": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "bStGNqIX/MGYtML7K3EzdsE/k5HGVAcg7XgN23TQXGXqxNC9fvYFR94fA0sGM5hAT36R+BBGet6ZDQxXL/IPxg==" + }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", "resolved": "4.3.2", @@ -3639,6 +3653,7 @@ "AutoFixture.AutoNSubstitute": "4.14.0", "AutoFixture.Xunit2": "4.14.0", "Core": "1.47.1", + "Kralizek.AutoFixture.Extensions.MockHttp": "1.2.0", "Microsoft.NET.Test.Sdk": "16.6.1", "NSubstitute": "4.2.2", "xunit": "2.4.1" diff --git a/test/Common/AutoFixture/Attributes/BitAutoDataAttribute.cs b/test/Common/AutoFixture/Attributes/BitAutoDataAttribute.cs index 4f93f7752..a32259f3c 100644 --- a/test/Common/AutoFixture/Attributes/BitAutoDataAttribute.cs +++ b/test/Common/AutoFixture/Attributes/BitAutoDataAttribute.cs @@ -7,6 +7,7 @@ using Xunit.Sdk; namespace Bit.Test.Common.AutoFixture.Attributes { + [DataDiscoverer("AutoFixture.Xunit2.NoPreDiscoveryDataDiscoverer", "AutoFixture.Xunit2")] public class BitAutoDataAttribute : DataAttribute { private readonly Func _createFixture; diff --git a/test/Common/AutoFixture/Attributes/MemberAutoDataAttribute.cs b/test/Common/AutoFixture/Attributes/BitMemberAutoDataAttribute.cs similarity index 100% rename from test/Common/AutoFixture/Attributes/MemberAutoDataAttribute.cs rename to test/Common/AutoFixture/Attributes/BitMemberAutoDataAttribute.cs diff --git a/test/Common/AutoFixture/Attributes/JsonDocumentCustomizeAttribute.cs b/test/Common/AutoFixture/Attributes/JsonDocumentCustomizeAttribute.cs new file mode 100644 index 000000000..d4df0599a --- /dev/null +++ b/test/Common/AutoFixture/Attributes/JsonDocumentCustomizeAttribute.cs @@ -0,0 +1,11 @@ +using AutoFixture; +using Bit.Test.Common.AutoFixture.JsonDocumentFixtures; + +namespace Bit.Test.Common.AutoFixture.Attributes +{ + public class JsonDocumentCustomizeAttribute : BitCustomizeAttribute + { + public string Json { get; set; } + public override ICustomization GetCustomization() => new JsonDocumentCustomization() { Json = Json }; + } +} diff --git a/test/Common/AutoFixture/Attributes/SutAutoDataAttribute.cs b/test/Common/AutoFixture/Attributes/SutAutoDataAttribute.cs index 6bb3b92ce..9f7c7fab8 100644 --- a/test/Common/AutoFixture/Attributes/SutAutoDataAttribute.cs +++ b/test/Common/AutoFixture/Attributes/SutAutoDataAttribute.cs @@ -1,8 +1,6 @@ using System; using System.Linq; -using System.Reflection; using AutoFixture; -using AutoFixture.Xunit2; namespace Bit.Test.Common.AutoFixture.Attributes { diff --git a/test/Common/AutoFixture/FixtureExtensions.cs b/test/Common/AutoFixture/FixtureExtensions.cs index a78b2f4bf..162784a35 100644 --- a/test/Common/AutoFixture/FixtureExtensions.cs +++ b/test/Common/AutoFixture/FixtureExtensions.cs @@ -7,5 +7,8 @@ namespace Bit.Test.Common.AutoFixture { public static IFixture WithAutoNSubstitutions(this IFixture fixture) => fixture.Customize(new AutoNSubstituteCustomization()); + + public static IFixture WithAutoNSubstitutionsAutoPopulatedProperties(this IFixture fixture) + => fixture.Customize(new AutoNSubstituteCustomization { ConfigureMembers = true }); } } diff --git a/test/Common/AutoFixture/JsonDocumentFixtures.cs b/test/Common/AutoFixture/JsonDocumentFixtures.cs new file mode 100644 index 000000000..9f2c9e22a --- /dev/null +++ b/test/Common/AutoFixture/JsonDocumentFixtures.cs @@ -0,0 +1,33 @@ +using System; +using System.Text.Json; +using AutoFixture; +using AutoFixture.Kernel; + +namespace Bit.Test.Common.AutoFixture.JsonDocumentFixtures +{ + public class JsonDocumentCustomization : ICustomization, ISpecimenBuilder + { + + public string Json { get; set; } + + public void Customize(IFixture fixture) + { + fixture.Customizations.Add(this); + } + + public object Create(object request, ISpecimenContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + var type = request as Type; + if (type == null || (type != typeof(JsonDocument))) + { + return new NoSpecimen(); + } + + return JsonDocument.Parse(Json ?? "{}"); + } + } +} diff --git a/test/Common/Common.csproj b/test/Common/Common.csproj index 16dc167d4..4a00c3f67 100644 --- a/test/Common/Common.csproj +++ b/test/Common/Common.csproj @@ -1,25 +1,22 @@ - false Bit.Test.Common - - - - + + + all runtime; build; native; contentfiles; analyzers - - - + + + + - - - + + - diff --git a/test/Common/Helpers/AssertHelper.cs b/test/Common/Helpers/AssertHelper.cs index 3c42ba4f1..53f3b28aa 100644 --- a/test/Common/Helpers/AssertHelper.cs +++ b/test/Common/Helpers/AssertHelper.cs @@ -1,5 +1,7 @@ using System; using System.Linq; +using System.Linq.Expressions; +using System.Reflection; using System.Text.Json; using Bit.Core.Utilities; using Xunit; @@ -20,7 +22,7 @@ namespace Bit.Test.Common.Helpers if (actual == null) { - throw new Exception("Expected object is null but actual is not"); + throw new Exception("Actual object is null but expected is not"); } foreach (var expectedPropInfo in expected.GetType().GetProperties().Where(pi => !relevantExcludedProperties.Contains(pi.Name))) @@ -38,6 +40,11 @@ namespace Bit.Test.Common.Helpers { Assert.Equal(expectedPropInfo.GetValue(expected), actualPropInfo.GetValue(actual)); } + else if (expectedPropInfo.PropertyType == typeof(JsonDocument) && actualPropInfo.PropertyType == typeof(JsonDocument)) + { + static string JsonDocString(PropertyInfo info, object obj) => JsonSerializer.Serialize(info.GetValue(obj)); + Assert.Equal(JsonDocString(expectedPropInfo, expected), JsonDocString(actualPropInfo, actual)); + } else { var prefix = $"{expectedPropInfo.PropertyType.Name}."; @@ -48,12 +55,24 @@ namespace Bit.Test.Common.Helpers } } - public static Predicate AssertEqualExpectedPredicate(T expected) => (actual) => + private static Predicate AssertPropertyEqualPredicate(T expected, params string[] excludedPropertyStrings) => (actual) => + { + AssertPropertyEqual(expected, actual, excludedPropertyStrings); + return true; + }; + + public static Expression> AssertPropertyEqual(T expected, params string[] excludedPropertyStrings) => + (T actual) => AssertPropertyEqualPredicate(expected, excludedPropertyStrings)(actual); + + private static Predicate AssertEqualExpectedPredicate(T expected) => (actual) => { Assert.Equal(expected, actual); return true; }; + public static Expression> AssertEqualExpected(T expected) => + (T actual) => AssertEqualExpectedPredicate(expected)(actual); + public static JsonElement AssertJsonProperty(JsonElement element, string propertyName, JsonValueKind jsonValueKind) { if (!element.TryGetProperty(propertyName, out var subElement)) @@ -64,5 +83,15 @@ namespace Bit.Test.Common.Helpers Assert.Equal(jsonValueKind, subElement.ValueKind); return subElement; } + + public static TimeSpan AssertRecent(DateTime dateTime, int skewSeconds = 2) + => AssertRecent(dateTime, TimeSpan.FromSeconds(skewSeconds)); + + public static TimeSpan AssertRecent(DateTime dateTime, TimeSpan skew) + { + var difference = DateTime.UtcNow - dateTime; + Assert.True(difference < skew); + return difference; + } } } diff --git a/test/Common/Helpers/TestCaseHelper.cs b/test/Common/Helpers/TestCaseHelper.cs new file mode 100644 index 000000000..350c33e26 --- /dev/null +++ b/test/Common/Helpers/TestCaseHelper.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Bit.Test.Common.Helpers +{ + public static class TestCaseHelper + { + public static IEnumerable> GetCombinations(params T[] items) + { + var count = Math.Pow(2, items.Length); + for (var i = 0; i < count; i++) + { + var str = Convert.ToString(i, 2).PadLeft(items.Length, '0'); + List combination = new(); + for (var j = 0; j < str.Length; j++) + { + if (str[j] == '1') + { + combination.Add(items[j]); + } + } + yield return combination; + } + } + + public static IEnumerable> GetCombinationsOfMultipleLists(params IEnumerable[] optionLists) + { + if (!optionLists.Any()) + { + yield break; + } + + foreach (var item in optionLists.First()) + { + var itemArray = new[] { item }; + + if (optionLists.Length == 1) + { + yield return itemArray; + } + + foreach (var nextCombination in GetCombinationsOfMultipleLists(optionLists.Skip(1).ToArray())) + { + yield return itemArray.Concat(nextCombination); + } + } + } + } +} diff --git a/test/Common/Test/TestCaseHelperTests.cs b/test/Common/Test/TestCaseHelperTests.cs new file mode 100644 index 000000000..07a8e10f4 --- /dev/null +++ b/test/Common/Test/TestCaseHelperTests.cs @@ -0,0 +1,53 @@ +using System; +using System.Linq; +using Bit.Test.Common.Helpers; +using Xunit; + +namespace Bit.Test.Common.Test +{ + public class TestCaseHelperTests + { + [Fact] + public void GetCombinations_EmptyList() + { + Assert.Equal(new[] { Array.Empty() }, TestCaseHelper.GetCombinations(Array.Empty()).ToArray()); + } + + [Fact] + public void GetCombinations_OneItemList() + { + Assert.Equal(new[] { Array.Empty(), new[] { 1 } }, TestCaseHelper.GetCombinations(1)); + } + + [Fact] + public void GetCombinations_TwoItemList() + { + Assert.Equal(new[] { Array.Empty(), new[] { 2 }, new[] { 1 }, new[] { 1, 2 } }, TestCaseHelper.GetCombinations(1, 2)); + } + + [Fact] + public void GetCombinationsOfMultipleLists_OneOne() + { + Assert.Equal(new[] { new object[] { 1, "1" } }, TestCaseHelper.GetCombinationsOfMultipleLists(new object[] { 1 }, new object[] { "1" })); + } + + + [Fact] + public void GetCombinationsOfMultipleLists_OneTwo() + { + Assert.Equal(new[] { new object[] { 1, "1" }, new object[] { 1, "2" } }, TestCaseHelper.GetCombinationsOfMultipleLists(new object[] { 1 }, new object[] { "1", "2" })); + } + + [Fact] + public void GetCombinationsOfMultipleLists_TwoOne() + { + Assert.Equal(new[] { new object[] { 1, "1" }, new object[] { 2, "1" } }, TestCaseHelper.GetCombinationsOfMultipleLists(new object[] { 1, 2 }, new object[] { "1" })); + } + + [Fact] + public void GetCombinationsOfMultipleLists_TwoTwo() + { + Assert.Equal(new[] { new object[] { 1, "1" }, new object[] { 1, "2" }, new object[] { 2, "1" }, new object[] { 2, "2" } }, TestCaseHelper.GetCombinationsOfMultipleLists(new object[] { 1, 2 }, new object[] { "1", "2" })); + } + } +} diff --git a/test/Common/packages.lock.json b/test/Common/packages.lock.json index cb5b15e2d..4d02f6583 100644 --- a/test/Common/packages.lock.json +++ b/test/Common/packages.lock.json @@ -22,6 +22,16 @@ "xunit.extensibility.core": "[2.2.0, 3.0.0)" } }, + "Kralizek.AutoFixture.Extensions.MockHttp": { + "type": "Direct", + "requested": "[1.2.0, )", + "resolved": "1.2.0", + "contentHash": "6zmks7/5mVczazv910N7V2EdiU6B+rY61lwdgVO0o2iZuTI6KI3T+Hgkrjv0eGOKYucq2OMC+gnAc5Ej2ajoTQ==", + "dependencies": { + "AutoFixture": "4.11.0", + "RichardSzalay.MockHttp": "6.0.0" + } + }, "Microsoft.NET.Test.Sdk": { "type": "Direct", "requested": "[16.6.1, )", @@ -1586,6 +1596,11 @@ "System.Diagnostics.DiagnosticSource": "4.7.1" } }, + "RichardSzalay.MockHttp": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "bStGNqIX/MGYtML7K3EzdsE/k5HGVAcg7XgN23TQXGXqxNC9fvYFR94fA0sGM5hAT36R+BBGet6ZDQxXL/IPxg==" + }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", "resolved": "4.3.2", diff --git a/test/Core.Test/AutoFixture/OrganizationLicenseCustomization.cs b/test/Core.Test/AutoFixture/OrganizationLicenseCustomization.cs new file mode 100644 index 000000000..885329979 --- /dev/null +++ b/test/Core.Test/AutoFixture/OrganizationLicenseCustomization.cs @@ -0,0 +1,20 @@ +using System; +using AutoFixture; +using Bit.Core.Models.Business; +using Bit.Test.Common.AutoFixture.Attributes; + +namespace Bit.Core.Test.AutoFixture +{ + public class OrganizationLicenseCustomizeAttribute : BitCustomizeAttribute + { + public override ICustomization GetCustomization() => new OrganizationLicenseCustomization(); + } + public class OrganizationLicenseCustomization : ICustomization + { + public void Customize(IFixture fixture) + { + fixture.Customize(composer => composer + .With(o => o.Signature, Guid.NewGuid().ToString().Replace('-', '+'))); + } + } +} diff --git a/test/Core.Test/AutoFixture/OrganizationSponsorshipFixtures.cs b/test/Core.Test/AutoFixture/OrganizationSponsorshipFixtures.cs index d002444d0..b8639547e 100644 --- a/test/Core.Test/AutoFixture/OrganizationSponsorshipFixtures.cs +++ b/test/Core.Test/AutoFixture/OrganizationSponsorshipFixtures.cs @@ -11,6 +11,33 @@ using Bit.Test.Common.AutoFixture.Attributes; namespace Bit.Core.Test.AutoFixture.OrganizationSponsorshipFixtures { + public class OrganizationSponsorshipCustomizeAttribute : BitCustomizeAttribute + { + public bool ToDelete = false; + public override ICustomization GetCustomization() => ToDelete ? + new ToDeleteOrganizationSponsorship() : + new ValidOrganizationSponsorship(); + } + + public class ValidOrganizationSponsorship : ICustomization + { + public void Customize(IFixture fixture) + { + fixture.Customize(composer => composer + .With(s => s.ToDelete, false) + .With(s => s.LastSyncDate, DateTime.UtcNow.AddDays(new Random().Next(-90, 0)))); + } + } + + public class ToDeleteOrganizationSponsorship : ICustomization + { + public void Customize(IFixture fixture) + { + fixture.Customize(composer => composer + .With(s => s.ToDelete, true)); + } + } + internal class OrganizationSponsorshipBuilder : ISpecimenBuilder { public object Create(object request, ISpecimenContext context) diff --git a/test/Core.Test/Core.Test.csproj b/test/Core.Test/Core.Test.csproj index 0ff3df757..08829e259 100644 --- a/test/Core.Test/Core.Test.csproj +++ b/test/Core.Test/Core.Test.csproj @@ -1,36 +1,34 @@ - - + false Bit.Core.Test - runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + + all runtime; build; native; contentfiles; analyzers - - + + + - - + - + diff --git a/test/Core.Test/Models/Business/Tokenables/OrganizationSponsorshipOfferTokenableTests.cs b/test/Core.Test/Models/Business/Tokenables/OrganizationSponsorshipOfferTokenableTests.cs new file mode 100644 index 000000000..cb7bcd990 --- /dev/null +++ b/test/Core.Test/Models/Business/Tokenables/OrganizationSponsorshipOfferTokenableTests.cs @@ -0,0 +1,159 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Business.Tokenables; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.Models.Business.Tokenables +{ + public class OrganizationSponsorshipOfferTokenableTests + { + public static IEnumerable PlanSponsorshipTypes() => Enum.GetValues().Select(x => new object[] { x }); + + [Fact] + public void IsInvalidIfIdentifierIsWrong() + { + var token = new OrganizationSponsorshipOfferTokenable() + { + Email = "email", + Id = Guid.NewGuid(), + Identifier = "not correct", + SponsorshipType = PlanSponsorshipType.FamiliesForEnterprise, + }; + + Assert.False(token.Valid); + } + + [Fact] + public void IsInvalidIfIdIsDefault() + { + var token = new OrganizationSponsorshipOfferTokenable() + { + Email = "email", + Id = default, + SponsorshipType = PlanSponsorshipType.FamiliesForEnterprise, + }; + + Assert.False(token.Valid); + } + + + [Fact] + public void IsInvalidIfEmailIsEmpty() + { + var token = new OrganizationSponsorshipOfferTokenable() + { + Email = "", + Id = Guid.NewGuid(), + SponsorshipType = PlanSponsorshipType.FamiliesForEnterprise, + }; + + Assert.False(token.Valid); + } + + [Theory, BitAutoData] + public void IsValid_Success(OrganizationSponsorship sponsorship) + { + var token = new OrganizationSponsorshipOfferTokenable(sponsorship); + + Assert.True(token.IsValid(sponsorship, sponsorship.OfferedToEmail)); + } + + [Theory, BitAutoData] + public void IsValid_RequiresNonNullSponsorship(OrganizationSponsorship sponsorship) + { + var token = new OrganizationSponsorshipOfferTokenable(sponsorship); + + Assert.False(token.IsValid(null, sponsorship.OfferedToEmail)); + } + + [Theory, BitAutoData] + public void IsValid_RequiresCurrentEmailToBeSameAsOfferedToEmail(OrganizationSponsorship sponsorship, string currentEmail) + { + var token = new OrganizationSponsorshipOfferTokenable(sponsorship); + + Assert.False(token.IsValid(sponsorship, currentEmail)); + } + + [Theory, BitAutoData] + public void IsValid_RequiresSameSponsorshipId(OrganizationSponsorship sponsorship1, OrganizationSponsorship sponsorship2) + { + sponsorship1.Id = sponsorship2.Id; + + var token = new OrganizationSponsorshipOfferTokenable(sponsorship1); + + Assert.False(token.IsValid(sponsorship2, sponsorship1.OfferedToEmail)); + } + + [Theory, BitAutoData] + public void IsValid_RequiresSameEmail(OrganizationSponsorship sponsorship1, OrganizationSponsorship sponsorship2) + { + sponsorship1.OfferedToEmail = sponsorship2.OfferedToEmail; + + var token = new OrganizationSponsorshipOfferTokenable(sponsorship1); + + Assert.False(token.IsValid(sponsorship2, sponsorship1.OfferedToEmail)); + } + + [Theory, BitAutoData] + public void Constructor_GrabsIdFromSponsorship(OrganizationSponsorship sponsorship) + { + var token = new OrganizationSponsorshipOfferTokenable(sponsorship); + + Assert.Equal(sponsorship.Id, token.Id); + } + + [Theory, BitAutoData] + public void Constructor_GrabsEmailFromSponsorshipOfferedToEmail(OrganizationSponsorship sponsorship) + { + var token = new OrganizationSponsorshipOfferTokenable(sponsorship); + + Assert.Equal(sponsorship.OfferedToEmail, token.Email); + } + + [Theory, BitMemberAutoData(nameof(PlanSponsorshipTypes))] + public void Constructor_GrabsSponsorshipType(PlanSponsorshipType planSponsorshipType, + OrganizationSponsorship sponsorship) + { + sponsorship.PlanSponsorshipType = planSponsorshipType; + var token = new OrganizationSponsorshipOfferTokenable(sponsorship); + + Assert.Equal(sponsorship.PlanSponsorshipType, token.SponsorshipType); + } + + [Theory, BitAutoData] + public void Constructor_DefaultId_Throws(OrganizationSponsorship sponsorship) + { + sponsorship.Id = default; + + Assert.Throws(() => new OrganizationSponsorshipOfferTokenable(sponsorship)); + } + + [Theory, BitAutoData] + public void Constructor_NoOfferedToEmail_Throws(OrganizationSponsorship sponsorship) + { + sponsorship.OfferedToEmail = null; + + Assert.Throws(() => new OrganizationSponsorshipOfferTokenable(sponsorship)); + } + + [Theory, BitAutoData] + public void Constructor_EmptyOfferedToEmail_Throws(OrganizationSponsorship sponsorship) + { + sponsorship.OfferedToEmail = ""; + + Assert.Throws(() => new OrganizationSponsorshipOfferTokenable(sponsorship)); + } + + [Theory, BitAutoData] + public void Constructor_NoPlanSponsorshipType_Throws(OrganizationSponsorship sponsorship) + { + sponsorship.PlanSponsorshipType = null; + + Assert.Throws(() => new OrganizationSponsorshipOfferTokenable(sponsorship)); + } + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyCommandTests.cs new file mode 100644 index 000000000..69530a563 --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationApiKeys/GetOrganizationApiKeyCommandTests.cs @@ -0,0 +1,104 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.OrganizationFeatures.OrganizationApiKeys; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.OrganizationFeatures.OrganizationApiKeys +{ + [SutProviderCustomize] + public class GetOrganizationApiKeyCommandTests + { + [Theory] + [BitAutoData] + public async Task GetOrganizationApiKey_HasOne_Returns(SutProvider sutProvider, + Guid id, Guid organizationId, OrganizationApiKeyType keyType) + { + sutProvider.GetDependency() + .GetManyByOrganizationIdTypeAsync(organizationId, keyType) + .Returns(new List + { + new OrganizationApiKey + { + Id = id, + OrganizationId = organizationId, + ApiKey = "test", + Type = keyType, + RevisionDate = DateTime.Now.AddDays(-1), + }, + }); + + var apiKey = await sutProvider.Sut.GetOrganizationApiKeyAsync(organizationId, keyType); + Assert.NotNull(apiKey); + Assert.Equal(id, apiKey.Id); + } + + [Theory] + [BitAutoData] + public async Task GetOrganizationApiKey_HasTwo_Throws(SutProvider sutProvider, + Guid organizationId, OrganizationApiKeyType keyType) + { + sutProvider.GetDependency() + .GetManyByOrganizationIdTypeAsync(organizationId, keyType) + .Returns(new List + { + new OrganizationApiKey + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + ApiKey = "test", + Type = keyType, + RevisionDate = DateTime.Now.AddDays(-1), + }, + new OrganizationApiKey + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + ApiKey = "test_other", + Type = keyType, + RevisionDate = DateTime.Now.AddDays(-1), + }, + }); + + await Assert.ThrowsAsync( + async () => await sutProvider.Sut.GetOrganizationApiKeyAsync(organizationId, keyType)); + } + + [Theory] + [BitAutoData] + public async Task GetOrganizationApiKey_HasNone_CreatesAndReturns(SutProvider sutProvider, + Guid organizationId, OrganizationApiKeyType keyType) + { + sutProvider.GetDependency() + .GetManyByOrganizationIdTypeAsync(organizationId, keyType) + .Returns(Enumerable.Empty()); + + var apiKey = await sutProvider.Sut.GetOrganizationApiKeyAsync(organizationId, keyType); + + Assert.NotNull(apiKey); + Assert.Equal(organizationId, apiKey.OrganizationId); + Assert.Equal(keyType, apiKey.Type); + await sutProvider.GetDependency() + .Received(1) + .CreateAsync(Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task GetOrganizationApiKey_BadType_Throws(SutProvider sutProvider, + Guid organizationId, OrganizationApiKeyType keyType) + { + keyType = (OrganizationApiKeyType)byte.MaxValue; + + await Assert.ThrowsAsync( + async () => await sutProvider.Sut.GetOrganizationApiKeyAsync(organizationId, keyType)); + } + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationApiKeys/RotateOrganizationApiKeyCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationApiKeys/RotateOrganizationApiKeyCommandTests.cs new file mode 100644 index 000000000..03606717b --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationApiKeys/RotateOrganizationApiKeyCommandTests.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.OrganizationFeatures.OrganizationApiKeys; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using Xunit; + +namespace Bit.Core.Test.OrganizationFeatures.OrganizationApiKeys +{ + [SutProviderCustomize] + public class RotateOrganizationApiKeyCommandTests + { + [Theory, BitAutoData] + public async Task RotateApiKeyAsync_RotatesKey(SutProvider sutProvider, + OrganizationApiKey organizationApiKey) + { + var existingKey = organizationApiKey.ApiKey; + organizationApiKey = await sutProvider.Sut.RotateApiKeyAsync(organizationApiKey); + Assert.NotEqual(existingKey, organizationApiKey.ApiKey); + AssertHelper.AssertRecent(organizationApiKey.RevisionDate); + } + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationCollections/CreateOrganizationConnectionCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationCollections/CreateOrganizationConnectionCommandTests.cs new file mode 100644 index 000000000..7eecf00f1 --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationCollections/CreateOrganizationConnectionCommandTests.cs @@ -0,0 +1,29 @@ +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Models.Data.Organizations.OrganizationConnections; +using Bit.Core.Models.OrganizationConnectionConfigs; +using Bit.Core.OrganizationFeatures.OrganizationConnections; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.OrganizationFeatures.OrganizationConnections +{ + [SutProviderCustomize] + public class CreateOrganizationConnectionCommandTests + { + [Theory] + [BitAutoData] + public async Task CreateAsync_CallsCreate(OrganizationConnectionData data, + SutProvider sutProvider) + { + await sutProvider.Sut.CreateAsync(data); + + await sutProvider.GetDependency().Received(1) + .CreateAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data.ToEntity()))); + } + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationCollections/DeleteOrganizationConnectionCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationCollections/DeleteOrganizationConnectionCommandTests.cs new file mode 100644 index 000000000..0ebc48b3b --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationCollections/DeleteOrganizationConnectionCommandTests.cs @@ -0,0 +1,28 @@ +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Models.Data.Organizations.OrganizationConnections; +using Bit.Core.OrganizationFeatures.OrganizationConnections; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.OrganizationFeatures.OrganizationConnections +{ + [SutProviderCustomize] + public class DeleteOrganizationConnectionCommandTests + { + [Theory] + [BitAutoData] + public async Task DeleteAsync_CallsDelete(OrganizationConnection connection, + SutProvider sutProvider) + { + await sutProvider.Sut.DeleteAsync(connection); + + await sutProvider.GetDependency().Received(1) + .DeleteAsync(connection); + } + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationCollections/UpdateOrganizationConnectionCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationCollections/UpdateOrganizationConnectionCommandTests.cs new file mode 100644 index 000000000..6bd5e5450 --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationCollections/UpdateOrganizationConnectionCommandTests.cs @@ -0,0 +1,61 @@ +using System; +using System.Threading.Tasks; +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; +using Bit.Core.Repositories; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.OrganizationFeatures.OrganizationConnections +{ + [SutProviderCustomize] + public class UpdateOrganizationConnectionCommandTests + { + [Theory] + [BitAutoData] + public async Task UpdateAsync_NoId_Fails(OrganizationConnectionData data, + SutProvider sutProvider) + { + data.Id = null; + + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(data)); + + Assert.Contains("Cannot update connection, Connection does not exist.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .UpsertAsync(default); + } + + [Theory] + [BitAutoData] + public async Task UpdateAsync_ConnectionDoesNotExist_ThrowsNotFound( + OrganizationConnectionData data, + SutProvider sutProvider) + { + var exception = await Assert.ThrowsAsync(() => sutProvider.Sut.UpdateAsync(data)); + + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .UpsertAsync(default); + } + + [Theory] + [BitAutoData] + public async Task UpdateAsync_CallsUpsert(OrganizationConnectionData data, + OrganizationConnection existing, + SutProvider sutProvider) + { + data.Id = existing.Id; + + sutProvider.GetDependency().GetByIdAsync(data.Id.Value).Returns(existing); + await sutProvider.Sut.UpdateAsync(data); + + await sutProvider.GetDependency().Received(1) + .UpsertAsync(Arg.Is(AssertHelper.AssertPropertyEqual(data.ToEntity()))); + } + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CancelSponsorshipCommandTestsBase.cs b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CancelSponsorshipCommandTestsBase.cs new file mode 100644 index 000000000..a617ae8ab --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CancelSponsorshipCommandTestsBase.cs @@ -0,0 +1,76 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using NSubstitute; + +namespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise +{ + public abstract class CancelSponsorshipCommandTestsBase : FamiliesForEnterpriseTestsBase + { + protected async Task AssertRemovedSponsoredPaymentAsync(Organization sponsoredOrg, + OrganizationSponsorship sponsorship, SutProvider sutProvider) + { + await sutProvider.GetDependency().Received(1) + .RemoveOrganizationSponsorshipAsync(sponsoredOrg, sponsorship); + await sutProvider.GetDependency().Received(1).UpsertAsync(sponsoredOrg); + if (sponsorship != null) + { + await sutProvider.GetDependency().Received(1) + .SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(sponsoredOrg.BillingEmailAddress(), sponsorship.ValidUntil.GetValueOrDefault()); + } + } + + protected async Task AssertDeletedSponsorshipAsync(OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + await sutProvider.GetDependency().Received(1) + .DeleteAsync(sponsorship); + } + + protected static async Task AssertDidNotRemoveSponsorshipAsync(SutProvider sutProvider) + { + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .DeleteAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .UpsertAsync(default); + } + + protected async Task AssertRemovedSponsorshipAsync(OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + await sutProvider.GetDependency().Received(1) + .DeleteAsync(sponsorship); + } + + protected static async Task AssertDidNotRemoveSponsoredPaymentAsync(SutProvider sutProvider) + { + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .RemoveOrganizationSponsorshipAsync(default, default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .UpsertAsync(default); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(default, default); + } + + protected static async Task AssertDidNotDeleteSponsorshipAsync(SutProvider sutProvider) + { + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .DeleteAsync(default); + } + + protected static async Task AssertDidNotUpdateSponsorshipAsync(SutProvider sutProvider) + { + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .UpsertAsync(default); + } + + protected static async Task AssertUpdatedSponsorshipAsync(OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + await sutProvider.GetDependency().Received(1).UpsertAsync(sponsorship); + } + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudRevokeSponsorshipCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudRevokeSponsorshipCommandTests.cs new file mode 100644 index 000000000..0e2161273 --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudRevokeSponsorshipCommandTests.cs @@ -0,0 +1,52 @@ +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud; +using Bit.Core.Test.AutoFixture.OrganizationSponsorshipFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud +{ + [SutProviderCustomize] + [OrganizationSponsorshipCustomize] + public class CloudRevokeSponsorshipCommandTests : CancelSponsorshipCommandTestsBase + { + [Theory] + [BitAutoData] + public async Task RevokeSponsorship_NoExistingSponsorship_ThrowsBadRequest( + SutProvider sutProvider) + { + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RevokeSponsorshipAsync(null)); + + Assert.Contains("You are not currently sponsoring an organization.", exception.Message); + await AssertDidNotDeleteSponsorshipAsync(sutProvider); + await AssertDidNotUpdateSponsorshipAsync(sutProvider); + } + + [Theory] + [BitAutoData] + public async Task RevokeSponsorship_SponsorshipNotRedeemed_DeletesSponsorship(OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + sponsorship.SponsoredOrganizationId = null; + + await sutProvider.Sut.RevokeSponsorshipAsync(sponsorship); + await AssertDeletedSponsorshipAsync(sponsorship, sutProvider); + } + + [Theory] + [BitAutoData] + public async Task RevokeSponsorship_SponsorshipRedeemed_MarksForDelete(OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + await sutProvider.Sut.RevokeSponsorshipAsync(sponsorship); + + Assert.True(sponsorship.ToDelete); + await AssertUpdatedSponsorshipAsync(sponsorship, sutProvider); + await AssertDidNotDeleteSponsorshipAsync(sutProvider); + } + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudSyncSponsorshipsCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudSyncSponsorshipsCommandTests.cs new file mode 100644 index 000000000..d0c2aff61 --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/CloudSyncSponsorshipsCommandTests.cs @@ -0,0 +1,230 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.Models.Data.Organizations.OrganizationSponsorships; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud +{ + + [SutProviderCustomize] + public class CloudSyncSponsorshipsCommandTests : FamiliesForEnterpriseTestsBase + { + + [Theory] + [BitAutoData] + public async Task SyncOrganization_SponsoringOrgNotFound_ThrowsBadRequest( + IEnumerable sponsorshipsData, + SutProvider sutProvider) + { + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.SyncOrganization(null, sponsorshipsData)); + + Assert.Contains("Failed to sync sponsorship - missing organization.", exception.Message); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertManyAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteManyAsync(default); + } + + [Theory] + [BitAutoData] + public async Task SyncOrganization_NoSponsorships_EarlyReturn( + Organization organization, + SutProvider sutProvider) + { + var result = await sutProvider.Sut.SyncOrganization(organization, Enumerable.Empty()); + + Assert.Empty(result.Item1.SponsorshipsBatch); + Assert.Empty(result.Item2); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertManyAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteManyAsync(default); + } + + [Theory] + [BitMemberAutoData(nameof(NonEnterprisePlanTypes))] + public async Task SyncOrganization_BadSponsoringOrgPlan_NoSync( + PlanType planType, + Organization organization, IEnumerable sponsorshipsData, + SutProvider sutProvider) + { + organization.PlanType = planType; + + await sutProvider.Sut.SyncOrganization(organization, sponsorshipsData); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertManyAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteManyAsync(default); + } + + [Theory] + [BitAutoData] + public async Task SyncOrganization_Success_RecordsEvent(Organization organization, + SutProvider sutProvider) + { + await sutProvider.Sut.SyncOrganization(organization, Array.Empty()); + + await sutProvider.GetDependency().Received(1).LogOrganizationEventAsync(organization, EventType.Organization_SponsorshipsSynced, Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task SyncOrganization_OneExisting_OneNew_Success(SutProvider sutProvider, + Organization sponsoringOrganization, OrganizationSponsorship existingSponsorship, OrganizationSponsorship newSponsorship) + { + // Arrange + sponsoringOrganization.PlanType = PlanType.EnterpriseAnnually; + + existingSponsorship.ToDelete = false; + newSponsorship.ToDelete = false; + + sutProvider.GetDependency() + .GetManyBySponsoringOrganizationAsync(sponsoringOrganization.Id) + .Returns(new List + { + existingSponsorship, + }); + + // Act + var (syncData, toEmailSponsorships) = await sutProvider.Sut.SyncOrganization(sponsoringOrganization, new[] + { + new OrganizationSponsorshipData(existingSponsorship), + new OrganizationSponsorshipData(newSponsorship), + }); + + // Assert + // Should have updated the cloud copy for each item given + await sutProvider.GetDependency() + .Received(1) + .UpsertManyAsync(Arg.Is>(sponsorships => sponsorships.Count() == 2)); + + // Neither were marked as delete, should not have deleted + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteManyAsync(default); + + // Only one sponsorship was new so it should only send one + Assert.Single(toEmailSponsorships); + } + + [Theory] + [BitAutoData] + public async Task SyncOrganization_TwoToDelete_OneCanDelete_Success(SutProvider sutProvider, + Organization sponsoringOrganization, OrganizationSponsorship canDeleteSponsorship, OrganizationSponsorship cannotDeleteSponsorship) + { + // Arrange + sponsoringOrganization.PlanType = PlanType.EnterpriseAnnually; + + canDeleteSponsorship.ToDelete = true; + canDeleteSponsorship.SponsoredOrganizationId = null; + + cannotDeleteSponsorship.ToDelete = true; + cannotDeleteSponsorship.SponsoredOrganizationId = Guid.NewGuid(); + + sutProvider.GetDependency() + .GetManyBySponsoringOrganizationAsync(sponsoringOrganization.Id) + .Returns(new List + { + canDeleteSponsorship, + cannotDeleteSponsorship, + }); + + // Act + var (syncData, toEmailSponsorships) = await sutProvider.Sut.SyncOrganization(sponsoringOrganization, new[] + { + new OrganizationSponsorshipData(canDeleteSponsorship), + new OrganizationSponsorshipData(cannotDeleteSponsorship), + }); + + // Assert + + await sutProvider.GetDependency() + .Received(1) + .UpsertManyAsync(Arg.Is>(sponsorships => sponsorships.Count() == 2)); + + // Deletes the sponsorship that had delete requested and is not sponsoring an org + await sutProvider.GetDependency() + .Received(1) + .DeleteManyAsync(Arg.Is>(toDeleteIds => + toDeleteIds.Count() == 1 && toDeleteIds.ElementAt(0) == canDeleteSponsorship.Id)); + } + + [Theory] + [BitAutoData] + public async Task SyncOrganization_BadData_DoesNotSave(SutProvider sutProvider, + Organization sponsoringOrganization, OrganizationSponsorship badOrganizationSponsorship) + { + sponsoringOrganization.PlanType = PlanType.EnterpriseAnnually; + + badOrganizationSponsorship.ToDelete = true; + badOrganizationSponsorship.LastSyncDate = null; + + sutProvider.GetDependency() + .GetManyBySponsoringOrganizationAsync(sponsoringOrganization.Id) + .Returns(new List()); + + var (syncData, toEmailSponsorships) = await sutProvider.Sut.SyncOrganization(sponsoringOrganization, new[] + { + new OrganizationSponsorshipData(badOrganizationSponsorship), + }); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertManyAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteManyAsync(default); + } + + [Theory] + [BitAutoData] + public async Task SyncOrganization_OrgDisabledForFourMonths_DoesNotSave(SutProvider sutProvider, + Organization sponsoringOrganization, OrganizationSponsorship organizationSponsorship) + { + sponsoringOrganization.PlanType = PlanType.EnterpriseAnnually; + sponsoringOrganization.Enabled = false; + sponsoringOrganization.ExpirationDate = DateTime.UtcNow.AddDays(-120); + + organizationSponsorship.ToDelete = false; + + sutProvider.GetDependency() + .GetManyBySponsoringOrganizationAsync(sponsoringOrganization.Id) + .Returns(new List()); + + var (syncData, toEmailSponsorships) = await sutProvider.Sut.SyncOrganization(sponsoringOrganization, new[] + { + new OrganizationSponsorshipData(organizationSponsorship), + }); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertManyAsync(default); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteManyAsync(default); + } + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/OrganizationSponsorshipRenewCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/OrganizationSponsorshipRenewCommandTests.cs new file mode 100644 index 000000000..cf3566f3d --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/OrganizationSponsorshipRenewCommandTests.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud; +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 +{ + [SutProviderCustomize] + public class OrganizationSponsorshipRenewCommandTests + { + [Theory] + [BitAutoData] + public async Task UpdateExpirationDate_UpdatesValidUntil(OrganizationSponsorship sponsorship, DateTime expireDate, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetBySponsoredOrganizationIdAsync(sponsorship.SponsoredOrganizationId.Value).Returns(sponsorship); + + await sutProvider.Sut.UpdateExpirationDateAsync(sponsorship.SponsoredOrganizationId.Value, expireDate); + + await sutProvider.GetDependency().Received(1) + .UpsertAsync(sponsorship); + } + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/RemoveSponsorshipCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/RemoveSponsorshipCommandTests.cs new file mode 100644 index 000000000..5cdf62a73 --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/RemoveSponsorshipCommandTests.cs @@ -0,0 +1,45 @@ + +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud; +using Bit.Core.Test.AutoFixture.OrganizationSponsorshipFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise +{ + [SutProviderCustomize] + [OrganizationSponsorshipCustomize] + public class RemoveSponsorshipCommandTests : CancelSponsorshipCommandTestsBase + { + [Theory] + [BitAutoData] + public async Task RemoveSponsorship_SponsoredOrgNull_ThrowsBadRequest(OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + sponsorship.SponsoredOrganizationId = null; + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RemoveSponsorshipAsync(sponsorship)); + + Assert.Contains("The requested organization is not currently being sponsored.", exception.Message); + Assert.False(sponsorship.ToDelete); + await AssertDidNotDeleteSponsorshipAsync(sutProvider); + await AssertDidNotUpdateSponsorshipAsync(sutProvider); + } + + [Theory] + [BitAutoData] + public async Task RemoveSponsorship_SponsorshipNotFound_ThrowsBadRequest(SutProvider sutProvider) + { + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RemoveSponsorshipAsync(null)); + + Assert.Contains("The requested organization is not currently being sponsored.", exception.Message); + await AssertDidNotDeleteSponsorshipAsync(sutProvider); + await AssertDidNotUpdateSponsorshipAsync(sutProvider); + } + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SendSponsorshipOfferCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SendSponsorshipOfferCommandTests.cs new file mode 100644 index 000000000..fbe9fd6eb --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SendSponsorshipOfferCommandTests.cs @@ -0,0 +1,125 @@ +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Test.AutoFixture.OrganizationSponsorshipFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise +{ + [SutProviderCustomize] + [OrganizationSponsorshipCustomize] + public class SendSponsorshipOfferCommandTests : FamiliesForEnterpriseTestsBase + { + [Theory] + [BitAutoData] + public async Task SendSponsorshipOffer_SendSponsorshipOfferAsync_ExistingAccount_Success(OrganizationSponsorship sponsorship, string sponsoringOrgName, User user, SutProvider sutProvider) + { + sutProvider.GetDependency().GetByEmailAsync(sponsorship.OfferedToEmail).Returns(user); + + await sutProvider.Sut.SendSponsorshipOfferAsync(sponsorship, sponsoringOrgName); + + await sutProvider.GetDependency().Received(1).SendFamiliesForEnterpriseOfferEmailAsync(sponsoringOrgName, sponsorship.OfferedToEmail, true, Arg.Any()); + } + + + [Theory] + [BitAutoData] + public async Task SendSponsorshipOffer_SendSponsorshipOfferAsync_NewAccount_Success(OrganizationSponsorship sponsorship, string sponsoringOrgName, SutProvider sutProvider) + { + sutProvider.GetDependency().GetByEmailAsync(sponsorship.OfferedToEmail).Returns((User)null); + + await sutProvider.Sut.SendSponsorshipOfferAsync(sponsorship, sponsoringOrgName); + + await sutProvider.GetDependency().Received(1).SendFamiliesForEnterpriseOfferEmailAsync(sponsoringOrgName, sponsorship.OfferedToEmail, false, Arg.Any()); + } + + [Theory] + [BitAutoData] + public async Task ResendSponsorshipOffer_SponsoringOrgNotFound_ThrowsBadRequest( + OrganizationUser orgUser, OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.SendSponsorshipOfferAsync(null, orgUser, sponsorship)); + + Assert.Contains("Cannot find the requested sponsoring organization.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendFamiliesForEnterpriseOfferEmailAsync(default, default, default, default); + } + + [Theory] + [BitAutoData] + public async Task ResendSponsorshipOffer_SponsoringOrgUserNotFound_ThrowsBadRequest(Organization org, + OrganizationSponsorship sponsorship, SutProvider sutProvider) + { + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.SendSponsorshipOfferAsync(org, null, sponsorship)); + + Assert.Contains("Only confirmed users can sponsor other organizations.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendFamiliesForEnterpriseOfferEmailAsync(default, default, default, default); + } + + [Theory] + [BitAutoData] + [BitMemberAutoData(nameof(NonConfirmedOrganizationUsersStatuses))] + public async Task ResendSponsorshipOffer_SponsoringOrgUserNotConfirmed_ThrowsBadRequest(OrganizationUserStatusType status, + Organization org, OrganizationUser orgUser, OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + orgUser.Status = status; + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.SendSponsorshipOfferAsync(org, orgUser, sponsorship)); + + Assert.Contains("Only confirmed users can sponsor other organizations.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendFamiliesForEnterpriseOfferEmailAsync(default, default, default, default); + } + + [Theory] + [BitAutoData] + public async Task ResendSponsorshipOffer_SponsorshipNotFound_ThrowsBadRequest(Organization org, + OrganizationUser orgUser, + SutProvider sutProvider) + { + orgUser.Status = OrganizationUserStatusType.Confirmed; + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.SendSponsorshipOfferAsync(org, orgUser, null)); + + Assert.Contains("Cannot find an outstanding sponsorship offer for this organization.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendFamiliesForEnterpriseOfferEmailAsync(default, default, default, default); + } + + [Theory] + [BitAutoData] + public async Task ResendSponsorshipOffer_NoOfferToEmail_ThrowsBadRequest(Organization org, + OrganizationUser orgUser, OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + orgUser.Status = OrganizationUserStatusType.Confirmed; + sponsorship.OfferedToEmail = null; + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.SendSponsorshipOfferAsync(org, orgUser, sponsorship)); + + Assert.Contains("Cannot find an outstanding sponsorship offer for this organization.", exception.Message); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SendFamiliesForEnterpriseOfferEmailAsync(default, default, default, default); + } + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SetUpSponsorshipCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SetUpSponsorshipCommandTests.cs new file mode 100644 index 000000000..76f5b51bc --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/SetUpSponsorshipCommandTests.cs @@ -0,0 +1,97 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Test.AutoFixture.OrganizationSponsorshipFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud +{ + [SutProviderCustomize] + [OrganizationSponsorshipCustomize] + public class SetUpSponsorshipCommandTests : FamiliesForEnterpriseTestsBase + { + [Theory] + [BitAutoData] + public async Task SetUpSponsorship_SponsorshipNotFound_ThrowsBadRequest(Organization org, + SutProvider sutProvider) + { + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.SetUpSponsorshipAsync(null, org)); + + Assert.Contains("No unredeemed sponsorship offer exists for you.", exception.Message); + await AssertDidNotSetUpAsync(sutProvider); + } + + [Theory] + [BitAutoData] + public async Task SetUpSponsorship_OrgAlreadySponsored_ThrowsBadRequest(Organization org, + OrganizationSponsorship sponsorship, OrganizationSponsorship existingSponsorship, + SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(org.Id).Returns(existingSponsorship); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.SetUpSponsorshipAsync(sponsorship, org)); + + Assert.Contains("Cannot redeem a sponsorship offer for an organization that is already sponsored. Revoke existing sponsorship first.", exception.Message); + await AssertDidNotSetUpAsync(sutProvider); + } + + [Theory] + [BitMemberAutoData(nameof(FamiliesPlanTypes))] + public async Task SetUpSponsorship_TooLongSinceLastSync_ThrowsBadRequest(PlanType planType, Organization org, + OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + org.PlanType = planType; + sponsorship.LastSyncDate = DateTime.UtcNow.AddDays(-365); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.SetUpSponsorshipAsync(sponsorship, org)); + + Assert.Contains("This sponsorship offer is more than 6 months old and has expired.", exception.Message); + await sutProvider.GetDependency() + .Received(1) + .DeleteAsync(sponsorship); + await AssertDidNotSetUpAsync(sutProvider); + } + + [Theory] + [BitMemberAutoData(nameof(NonFamiliesPlanTypes))] + public async Task SetUpSponsorship_OrgNotFamiles_ThrowsBadRequest(PlanType planType, + OrganizationSponsorship sponsorship, Organization org, + SutProvider sutProvider) + { + org.PlanType = planType; + sponsorship.LastSyncDate = DateTime.UtcNow; + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.SetUpSponsorshipAsync(sponsorship, org)); + + Assert.Contains("Can only redeem sponsorship offer on families organizations.", exception.Message); + await AssertDidNotSetUpAsync(sutProvider); + } + + private static async Task AssertDidNotSetUpAsync(SutProvider sutProvider) + { + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .SponsorOrganizationAsync(default, default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertAsync(default); + } + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateBillingSyncKeyCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateBillingSyncKeyCommandTests.cs new file mode 100644 index 000000000..e2a2ad354 --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateBillingSyncKeyCommandTests.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud; +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 +{ + [SutProviderCustomize] + public class ValidateBillingSyncKeyCommandTests + { + [Theory] + [BitAutoData] + public async Task ValidateBillingSyncKeyAsync_NullOrganization_Throws(SutProvider sutProvider) + { + await Assert.ThrowsAsync(() => sutProvider.Sut.ValidateBillingSyncKeyAsync(null, null)); + } + + [Theory] + [BitAutoData((string)null)] + [BitAutoData("")] + [BitAutoData(" ")] + public async Task ValidateBillingSyncKeyAsync_BadString_ReturnsFalse(string billingSyncKey, SutProvider sutProvider) + { + Assert.False(await sutProvider.Sut.ValidateBillingSyncKeyAsync(new Organization(), billingSyncKey)); + } + + [Theory] + [BitAutoData] + public async Task ValidateBillingSyncKeyAsync_KeyEquals_ReturnsTrue(SutProvider sutProvider, + Organization organization, OrganizationApiKey orgApiKey, string billingSyncKey) + { + orgApiKey.ApiKey = billingSyncKey; + + sutProvider.GetDependency() + .GetManyByOrganizationIdTypeAsync(organization.Id, OrganizationApiKeyType.BillingSync) + .Returns(new[] { orgApiKey }); + + Assert.True(await sutProvider.Sut.ValidateBillingSyncKeyAsync(organization, billingSyncKey)); + } + + [Theory] + [BitAutoData] + public async Task ValidateBillingSyncKeyAsync_KeyDoesNotEqual_ReturnsFalse(SutProvider sutProvider, + Organization organization, OrganizationApiKey orgApiKey, string billingSyncKey) + { + sutProvider.GetDependency() + .GetManyByOrganizationIdTypeAsync(organization.Id, OrganizationApiKeyType.BillingSync) + .Returns(new[] { orgApiKey }); + + Assert.False(await sutProvider.Sut.ValidateBillingSyncKeyAsync(organization, billingSyncKey)); + } + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateRedemptionTokenCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateRedemptionTokenCommandTests.cs new file mode 100644 index 000000000..9e474cd27 --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateRedemptionTokenCommandTests.cs @@ -0,0 +1,92 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Business.Tokenables; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud; +using Bit.Core.Repositories; +using Bit.Core.Tokens; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud +{ + [SutProviderCustomize] + public class ValidateRedemptionTokenCommandTests + { + [Theory] + [BitAutoData] + public async Task ValidateRedemptionTokenAsync_CannotUnprotect_ReturnsFalse(SutProvider sutProvider, + string encryptedString) + { + sutProvider + .GetDependency>() + .TryUnprotect(encryptedString, out _) + .Returns(call => + { + call[1] = null; + return false; + }); + + var (valid, sponsorship) = await sutProvider.Sut.ValidateRedemptionTokenAsync(encryptedString, null); + Assert.False(valid); + Assert.Null(sponsorship); + } + + [Theory] + [BitAutoData] + public async Task ValidateRedemptionTokenAsync_NoSponsorship_ReturnsFalse(SutProvider sutProvider, + string encryptedString, OrganizationSponsorshipOfferTokenable tokenable) + { + sutProvider + .GetDependency>() + .TryUnprotect(encryptedString, out _) + .Returns(call => + { + call[1] = tokenable; + return true; + }); + + var (valid, sponsorship) = await sutProvider.Sut.ValidateRedemptionTokenAsync(encryptedString, "test@email.com"); + Assert.False(valid); + Assert.Null(sponsorship); + } + + [Theory] + [BitAutoData] + public async Task ValidateRedemptionTokenAsync_ValidSponsorship_ReturnsFalse(SutProvider sutProvider, + string encryptedString, string email, OrganizationSponsorshipOfferTokenable tokenable) + { + tokenable.Email = email; + + sutProvider + .GetDependency>() + .TryUnprotect(encryptedString, out _) + .Returns(call => + { + call[1] = tokenable; + return true; + }); + + sutProvider.GetDependency() + .GetByIdAsync(tokenable.Id) + .Returns(new OrganizationSponsorship + { + Id = tokenable.Id, + PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise, + OfferedToEmail = email + }); + + var (valid, sponsorship) = await sutProvider.Sut + .ValidateRedemptionTokenAsync(encryptedString, email); + + Assert.True(valid); + Assert.NotNull(sponsorship); + } + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommandTests.cs new file mode 100644 index 000000000..78fb70264 --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/Cloud/ValidateSponsorshipCommandTests.cs @@ -0,0 +1,256 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud; +using Bit.Core.Repositories; +using Bit.Core.Test.AutoFixture.OrganizationSponsorshipFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using NSubstitute; +using Xunit; + +namespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Cloud +{ + [SutProviderCustomize] + [OrganizationSponsorshipCustomize] + public class ValidateSponsorshipCommandTests : CancelSponsorshipCommandTestsBase + { + [Theory] + [BitAutoData] + public async Task ValidateSponsorshipAsync_NoSponsoredOrg_EarlyReturn(Guid sponsoredOrgId, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetByIdAsync(sponsoredOrgId).Returns((Organization)null); + + var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrgId); + + Assert.False(result); + await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); + await AssertDidNotDeleteSponsorshipAsync(sutProvider); + } + + [Theory] + [BitAutoData] + public async Task ValidateSponsorshipAsync_NoExistingSponsorship_UpdatesStripePlan(Organization sponsoredOrg, + SutProvider sutProvider) + { + sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); + + var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); + + Assert.False(result); + await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, null, sutProvider); + } + + [Theory] + [BitAutoData] + public async Task ValidateSponsorshipAsync_SponsoringOrgDefault_UpdatesStripePlan(Organization sponsoredOrg, + OrganizationSponsorship existingSponsorship, SutProvider sutProvider) + { + existingSponsorship.SponsoringOrganizationId = default; + + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship); + sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); + + var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); + + Assert.False(result); + await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider); + await AssertDeletedSponsorshipAsync(existingSponsorship, sutProvider); + } + + [Theory] + [BitAutoData] + public async Task ValidateSponsorshipAsync_SponsoringOrgUserDefault_UpdatesStripePlan(Organization sponsoredOrg, + OrganizationSponsorship existingSponsorship, SutProvider sutProvider) + { + existingSponsorship.SponsoringOrganizationUserId = default; + + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship); + sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); + + var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); + + Assert.False(result); + await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider); + await AssertDeletedSponsorshipAsync(existingSponsorship, sutProvider); + } + + [Theory] + [BitAutoData] + public async Task ValidateSponsorshipAsync_SponsorshipTypeNull_UpdatesStripePlan(Organization sponsoredOrg, + OrganizationSponsorship existingSponsorship, SutProvider sutProvider) + { + existingSponsorship.PlanSponsorshipType = null; + + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship); + sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); + + var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); + + Assert.False(result); + await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider); + await AssertDeletedSponsorshipAsync(existingSponsorship, sutProvider); + } + + [Theory] + [BitAutoData] + public async Task ValidateSponsorshipAsync_SponsoringOrgNotFound_UpdatesStripePlan(Organization sponsoredOrg, + OrganizationSponsorship existingSponsorship, SutProvider sutProvider) + { + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship); + sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); + + var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); + + Assert.False(result); + await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider); + await AssertDeletedSponsorshipAsync(existingSponsorship, sutProvider); + } + + [Theory] + [BitMemberAutoData(nameof(NonEnterprisePlanTypes))] + public async Task ValidateSponsorshipAsync_SponsoringOrgNotEnterprise_UpdatesStripePlan(PlanType planType, + Organization sponsoredOrg, OrganizationSponsorship existingSponsorship, Organization sponsoringOrg, + SutProvider sutProvider) + { + sponsoringOrg.PlanType = planType; + existingSponsorship.SponsoringOrganizationId = sponsoringOrg.Id; + + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship); + sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); + sutProvider.GetDependency().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg); + + var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); + + Assert.False(result); + await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider); + await AssertDeletedSponsorshipAsync(existingSponsorship, sutProvider); + } + + [Theory] + [BitMemberAutoData(nameof(EnterprisePlanTypes))] + public async Task ValidateSponsorshipAsync_SponsoringOrgDisabledLongerThanGrace_UpdatesStripePlan(PlanType planType, + Organization sponsoredOrg, OrganizationSponsorship existingSponsorship, Organization sponsoringOrg, + SutProvider sutProvider) + { + sponsoringOrg.PlanType = planType; + sponsoringOrg.Enabled = false; + sponsoringOrg.ExpirationDate = DateTime.UtcNow.AddDays(-100); + existingSponsorship.SponsoringOrganizationId = sponsoringOrg.Id; + + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship); + sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); + sutProvider.GetDependency().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg); + + var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); + + Assert.False(result); + await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider); + await AssertDeletedSponsorshipAsync(existingSponsorship, sutProvider); + } + + [Theory] + [OrganizationSponsorshipCustomize(ToDelete = true)] + [BitMemberAutoData(nameof(EnterprisePlanTypes))] + public async Task ValidateSponsorshipAsync_ToDeleteSponsorship_IsInvalid(PlanType planType, + Organization sponsoredOrg, OrganizationSponsorship sponsorship, Organization sponsoringOrg, + SutProvider sutProvider) + { + sponsoringOrg.PlanType = planType; + sponsoringOrg.Enabled = true; + sponsorship.SponsoringOrganizationId = sponsoringOrg.Id; + + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(sponsorship); + sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); + sutProvider.GetDependency().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg); + + var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); + + Assert.False(result); + + await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, sponsorship, sutProvider); + await AssertDeletedSponsorshipAsync(sponsorship, sutProvider); + } + + + [Theory] + [BitMemberAutoData(nameof(EnterprisePlanTypes))] + public async Task ValidateSponsorshipAsync_SponsoringOrgDisabledUnknownTime_UpdatesStripePlan(PlanType planType, + Organization sponsoredOrg, OrganizationSponsorship existingSponsorship, Organization sponsoringOrg, + SutProvider sutProvider) + { + sponsoringOrg.PlanType = planType; + sponsoringOrg.Enabled = false; + sponsoringOrg.ExpirationDate = null; + existingSponsorship.SponsoringOrganizationId = sponsoringOrg.Id; + + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship); + sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); + sutProvider.GetDependency().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg); + + var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); + + Assert.False(result); + await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider); + await AssertRemovedSponsorshipAsync(existingSponsorship, sutProvider); + } + + [Theory] + [BitMemberAutoData(nameof(EnterprisePlanTypes))] + public async Task ValidateSponsorshipAsync_SponsoringOrgDisabledLessThanGrace_Valid(PlanType planType, + Organization sponsoredOrg, OrganizationSponsorship existingSponsorship, Organization sponsoringOrg, + SutProvider sutProvider) + { + sponsoringOrg.PlanType = planType; + sponsoringOrg.Enabled = true; + sponsoringOrg.ExpirationDate = DateTime.UtcNow.AddDays(-1); + existingSponsorship.SponsoringOrganizationId = sponsoringOrg.Id; + + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship); + sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); + sutProvider.GetDependency().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg); + + var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); + + Assert.True(result); + + await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); + await AssertDidNotRemoveSponsorshipAsync(sutProvider); + } + + + [Theory] + [BitMemberAutoData(nameof(EnterprisePlanTypes))] + public async Task ValidateSponsorshipAsync_Valid(PlanType planType, + Organization sponsoredOrg, OrganizationSponsorship existingSponsorship, Organization sponsoringOrg, + SutProvider sutProvider) + { + sponsoringOrg.PlanType = planType; + sponsoringOrg.Enabled = true; + existingSponsorship.SponsoringOrganizationId = sponsoringOrg.Id; + + sutProvider.GetDependency() + .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship); + sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); + sutProvider.GetDependency().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg); + + var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); + + Assert.True(result); + + await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); + await AssertDidNotDeleteSponsorshipAsync(sutProvider); + } + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommandTests.cs new file mode 100644 index 000000000..7df148eb9 --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/CreateSponsorshipCommandTests.cs @@ -0,0 +1,181 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Exceptions; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise; +using Bit.Core.Repositories; +using Bit.Core.Services; +using Bit.Core.Test.AutoFixture.OrganizationSponsorshipFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; +using NSubstitute; +using NSubstitute.ExceptionExtensions; +using NSubstitute.ReturnsExtensions; +using Xunit; + +namespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise +{ + [SutProviderCustomize] + public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase + { + private bool SponsorshipValidator(OrganizationSponsorship sponsorship, OrganizationSponsorship expectedSponsorship) + { + try + { + AssertHelper.AssertPropertyEqual(sponsorship, expectedSponsorship, nameof(OrganizationSponsorship.Id)); + return true; + } + catch + { + return false; + } + } + + [Theory, BitAutoData] + public async Task CreateSponsorship_OfferedToNotFound_ThrowsBadRequest(OrganizationUser orgUser, SutProvider sutProvider) + { + sutProvider.GetDependency().GetUserByIdAsync(orgUser.UserId.Value).ReturnsNull(); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.CreateSponsorshipAsync(null, orgUser, PlanSponsorshipType.FamiliesForEnterprise, default, default)); + + Assert.Contains("Cannot offer a Families Organization Sponsorship to yourself. Choose a different email.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateAsync(default); + } + + [Theory, BitAutoData] + public async Task CreateSponsorship_OfferedToSelf_ThrowsBadRequest(OrganizationUser orgUser, string sponsoredEmail, User user, SutProvider sutProvider) + { + user.Email = sponsoredEmail; + sutProvider.GetDependency().GetUserByIdAsync(orgUser.UserId.Value).Returns(user); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.CreateSponsorshipAsync(null, orgUser, PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, default)); + + Assert.Contains("Cannot offer a Families Organization Sponsorship to yourself. Choose a different email.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateAsync(default); + } + + [Theory, BitMemberAutoData(nameof(NonEnterprisePlanTypes))] + public async Task CreateSponsorship_BadSponsoringOrgPlan_ThrowsBadRequest(PlanType sponsoringOrgPlan, + Organization org, OrganizationUser orgUser, User user, SutProvider sutProvider) + { + org.PlanType = sponsoringOrgPlan; + orgUser.Status = OrganizationUserStatusType.Confirmed; + + sutProvider.GetDependency().GetUserByIdAsync(orgUser.UserId.Value).Returns(user); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.CreateSponsorshipAsync(org, orgUser, PlanSponsorshipType.FamiliesForEnterprise, default, default)); + + Assert.Contains("Specified Organization cannot sponsor other organizations.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateAsync(default); + } + + [Theory] + [BitMemberAutoData(nameof(NonConfirmedOrganizationUsersStatuses))] + public async Task CreateSponsorship_BadSponsoringUserStatus_ThrowsBadRequest( + OrganizationUserStatusType statusType, Organization org, OrganizationUser orgUser, User user, + SutProvider sutProvider) + { + org.PlanType = PlanType.EnterpriseAnnually; + orgUser.Status = statusType; + + sutProvider.GetDependency().GetUserByIdAsync(orgUser.UserId.Value).Returns(user); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.CreateSponsorshipAsync(org, orgUser, PlanSponsorshipType.FamiliesForEnterprise, default, default)); + + Assert.Contains("Only confirmed users can sponsor other organizations.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateAsync(default); + } + + [Theory] + [OrganizationSponsorshipCustomize] + [BitAutoData] + public async Task CreateSponsorship_AlreadySponsoring_Throws(Organization org, + OrganizationUser orgUser, User user, OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + org.PlanType = PlanType.EnterpriseAnnually; + orgUser.Status = OrganizationUserStatusType.Confirmed; + + sutProvider.GetDependency().GetUserByIdAsync(orgUser.UserId.Value).Returns(user); + sutProvider.GetDependency() + .GetBySponsoringOrganizationUserIdAsync(orgUser.Id).Returns(sponsorship); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.CreateSponsorshipAsync(org, orgUser, sponsorship.PlanSponsorshipType.Value, default, default)); + + Assert.Contains("Can only sponsor one organization per Organization User.", exception.Message); + await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() + .CreateAsync(default); + } + + [Theory] + [BitAutoData] + public async Task CreateSponsorship_CreatesSponsorship(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, User user, + string sponsoredEmail, string friendlyName, Guid sponsorshipId, SutProvider sutProvider) + { + sponsoringOrg.PlanType = PlanType.EnterpriseAnnually; + sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed; + + sutProvider.GetDependency().GetUserByIdAsync(sponsoringOrgUser.UserId.Value).Returns(user); + sutProvider.GetDependency().WhenForAnyArgs(x => x.UpsertAsync(default)).Do(callInfo => + { + var sponsorship = callInfo.Arg(); + sponsorship.Id = sponsorshipId; + }); + + + await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, + PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName); + + var expectedSponsorship = new OrganizationSponsorship + { + Id = sponsorshipId, + SponsoringOrganizationId = sponsoringOrg.Id, + SponsoringOrganizationUserId = sponsoringOrgUser.Id, + FriendlyName = friendlyName, + OfferedToEmail = sponsoredEmail, + PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise, + }; + + await sutProvider.GetDependency().Received(1) + .UpsertAsync(Arg.Is(s => SponsorshipValidator(s, expectedSponsorship))); + } + + [Theory] + [BitAutoData] + public async Task CreateSponsorship_CreateSponsorshipThrows_RevertsDatabase(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, User user, + string sponsoredEmail, string friendlyName, SutProvider sutProvider) + { + sponsoringOrg.PlanType = PlanType.EnterpriseAnnually; + sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed; + + var expectedException = new Exception(); + OrganizationSponsorship createdSponsorship = null; + sutProvider.GetDependency().GetUserByIdAsync(sponsoringOrgUser.UserId.Value).Returns(user); + sutProvider.GetDependency().UpsertAsync(default).ThrowsForAnyArgs(callInfo => + { + createdSponsorship = callInfo.ArgAt(0); + createdSponsorship.Id = Guid.NewGuid(); + return expectedException; + }); + + var actualException = await Assert.ThrowsAsync(() => + sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, + PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName)); + Assert.Same(expectedException, actualException); + + await sutProvider.GetDependency().Received(1) + .DeleteAsync(createdSponsorship); + } + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/FamiliesForEnterpriseTestsBase.cs b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/FamiliesForEnterpriseTestsBase.cs new file mode 100644 index 000000000..868a5951f --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/FamiliesForEnterpriseTestsBase.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Bit.Core.Enums; +using Bit.Core.Utilities; + +namespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise +{ + public abstract class FamiliesForEnterpriseTestsBase + { + public static IEnumerable EnterprisePlanTypes => + Enum.GetValues().Where(p => StaticStore.GetPlan(p).Product == ProductType.Enterprise).Select(p => new object[] { p }); + + public static IEnumerable NonEnterprisePlanTypes => + Enum.GetValues().Where(p => StaticStore.GetPlan(p).Product != ProductType.Enterprise).Select(p => new object[] { p }); + + public static IEnumerable FamiliesPlanTypes => + Enum.GetValues().Where(p => StaticStore.GetPlan(p).Product == ProductType.Families).Select(p => new object[] { p }); + + public static IEnumerable NonFamiliesPlanTypes => + Enum.GetValues().Where(p => StaticStore.GetPlan(p).Product != ProductType.Families).Select(p => new object[] { p }); + + public static IEnumerable NonConfirmedOrganizationUsersStatuses => + Enum.GetValues() + .Where(s => s != OrganizationUserStatusType.Confirmed) + .Select(s => new object[] { s }); + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedRevokeSponsorshipCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedRevokeSponsorshipCommandTests.cs new file mode 100644 index 000000000..280f8a736 --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedRevokeSponsorshipCommandTests.cs @@ -0,0 +1,55 @@ +using System; +using System.Threading.Tasks; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted; +using Bit.Core.Test.AutoFixture.OrganizationSponsorshipFixtures; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Xunit; + +namespace Bit.Core.Test.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted +{ + [SutProviderCustomize] + [OrganizationSponsorshipCustomize] + public class SelfHostedRevokeSponsorshipCommandTests : CancelSponsorshipCommandTestsBase + { + [Theory] + [BitAutoData] + public async Task RevokeSponsorship_NoExistingSponsorship_ThrowsBadRequest( + SutProvider sutProvider) + { + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.RevokeSponsorshipAsync(null)); + + Assert.Contains("You are not currently sponsoring an organization.", exception.Message); + await AssertDidNotDeleteSponsorshipAsync(sutProvider); + await AssertDidNotUpdateSponsorshipAsync(sutProvider); + } + + [Theory] + [BitAutoData] + public async Task RevokeSponsorship_SponsorshipNotSynced_DeletesSponsorship(OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + sponsorship.LastSyncDate = null; + + await sutProvider.Sut.RevokeSponsorshipAsync(sponsorship); + await AssertDeletedSponsorshipAsync(sponsorship, sutProvider); + } + + [Theory] + [BitAutoData] + public async Task RevokeSponsorship_SponsorshipSynced_MarksForDeletion(OrganizationSponsorship sponsorship, + SutProvider sutProvider) + { + sponsorship.LastSyncDate = DateTime.UtcNow; + + await sutProvider.Sut.RevokeSponsorshipAsync(sponsorship); + + Assert.True(sponsorship.ToDelete); + await AssertUpdatedSponsorshipAsync(sponsorship, sutProvider); + await AssertDidNotDeleteSponsorshipAsync(sutProvider); + } + } +} diff --git a/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommandTests.cs b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommandTests.cs new file mode 100644 index 000000000..1dbf964c2 --- /dev/null +++ b/test/Core.Test/OrganizationFeatures/OrganizationSponsorships/FamiliesForEnterprise/SelfHosted/SelfHostedSyncSponsorshipsCommandTests.cs @@ -0,0 +1,194 @@ + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Text.Json; +using System.Threading.Tasks; +using AutoFixture; +using Bit.Core.Entities; +using Bit.Core.Exceptions; +using Bit.Core.Models.Api.Response.OrganizationSponsorships; +using Bit.Core.Models.Data.Organizations.OrganizationSponsorships; +using Bit.Core.Models.OrganizationConnectionConfigs; +using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.SelfHosted; +using Bit.Core.Repositories; +using Bit.Core.Settings; +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 GetSutProvider(bool enableCloudCommunication = true, string identityResponse = null, string apiResponse = null) + { + var fixture = new Fixture().WithAutoNSubstitutionsAutoPopulatedProperties(); + fixture.AddMockHttp(); + + var settings = fixture.Create(); + settings.SelfHosted = true; + settings.EnableCloudCommunication = enableCloudCommunication; + + var apiUri = fixture.Create(); + var identityUri = fixture.Create(); + 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(); + mockHttpClientFactory.CreateClient(Arg.Is("client")).Returns(apiHttp); + mockHttpClientFactory.CreateClient(Arg.Is("identity")).Returns(identityHttp); + + return new SutProvider(fixture) + .SetDependency(settings) + .SetDependency(mockHttpClientFactory) + .Create(); + } + + [Theory] + [BitAutoData] + public async Task SyncOrganization_BillingSyncKeyDisabled_ThrowsBadRequest( + Guid cloudOrganizationId, OrganizationConnection billingSyncConnection) + { + var sutProvider = GetSutProvider(); + billingSyncConnection.Enabled = false; + billingSyncConnection.SetConfig(new BillingSyncConfig + { + BillingSyncKey = "okslkcslkjf" + }); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.SyncOrganization(billingSyncConnection.OrganizationId, cloudOrganizationId, billingSyncConnection)); + + Assert.Contains($"Billing Sync Key disabled", exception.Message); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteManyAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertManyAsync(default); + } + + [Theory] + [BitAutoData] + public async Task SyncOrganization_BillingSyncKeyEmpty_ThrowsBadRequest( + Guid cloudOrganizationId, OrganizationConnection billingSyncConnection) + { + var sutProvider = GetSutProvider(); + billingSyncConnection.Config = ""; + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.SyncOrganization(billingSyncConnection.OrganizationId, cloudOrganizationId, billingSyncConnection)); + + Assert.Contains($"No Billing Sync Key known", exception.Message); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteManyAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertManyAsync(default); + } + + [Theory] + [BitAutoData] + public async Task SyncOrganization_CloudCommunicationDisabled_EarlyReturn( + Guid cloudOrganizationId, OrganizationConnection billingSyncConnection) + { + var sutProvider = GetSutProvider(false); + + var exception = await Assert.ThrowsAsync(() => + sutProvider.Sut.SyncOrganization(billingSyncConnection.OrganizationId, cloudOrganizationId, billingSyncConnection)); + + Assert.Contains($"Cloud communication is disabled", exception.Message); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteManyAsync(default); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertManyAsync(default); + } + + [Theory] + [OrganizationSponsorshipCustomize] + [BitAutoData] + public async Task SyncOrganization_SyncsSponsorships( + Guid cloudOrganizationId, OrganizationConnection billingSyncConnection, IEnumerable sponsorships) + { + var syncJsonResponse = JsonSerializer.Serialize(new OrganizationSponsorshipSyncResponseModel( + new OrganizationSponsorshipSyncData + { + SponsorshipsBatch = sponsorships.Select(o => new OrganizationSponsorshipData(o)) + })); + + var sutProvider = GetSutProvider(apiResponse: syncJsonResponse); + billingSyncConnection.SetConfig(new BillingSyncConfig + { + BillingSyncKey = "okslkcslkjf" + }); + sutProvider.GetDependency() + .GetManyBySponsoringOrganizationAsync(Arg.Any()).Returns(sponsorships.ToList()); + + await sutProvider.Sut.SyncOrganization(billingSyncConnection.OrganizationId, cloudOrganizationId, billingSyncConnection); + + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .DeleteManyAsync(default); + await sutProvider.GetDependency() + .Received(1) + .UpsertManyAsync(Arg.Any>()); + } + + [Theory] + [OrganizationSponsorshipCustomize(ToDelete = true)] + [BitAutoData] + public async Task SyncOrganization_DeletesSponsorships( + Guid cloudOrganizationId, OrganizationConnection billingSyncConnection, IEnumerable sponsorships) + { + var syncJsonResponse = JsonSerializer.Serialize(new OrganizationSponsorshipSyncResponseModel( + new OrganizationSponsorshipSyncData + { + SponsorshipsBatch = sponsorships.Select(o => new OrganizationSponsorshipData(o) { CloudSponsorshipRemoved = true }) + })); + + var sutProvider = GetSutProvider(apiResponse: syncJsonResponse); + billingSyncConnection.SetConfig(new BillingSyncConfig + { + BillingSyncKey = "okslkcslkjf" + }); + sutProvider.GetDependency() + .GetManyBySponsoringOrganizationAsync(Arg.Any()).Returns(sponsorships.ToList()); + + await sutProvider.Sut.SyncOrganization(billingSyncConnection.OrganizationId, cloudOrganizationId, billingSyncConnection); + + await sutProvider.GetDependency() + .Received(1) + .DeleteManyAsync(Arg.Any>()); + await sutProvider.GetDependency() + .DidNotReceiveWithAnyArgs() + .UpsertManyAsync(default); + } + } +} diff --git a/test/Core.Test/Repositories/EntityFramework/EqualityComparers/OrganizationCompare.cs b/test/Core.Test/Repositories/EntityFramework/EqualityComparers/OrganizationCompare.cs index 69d140137..47346c466 100644 --- a/test/Core.Test/Repositories/EntityFramework/EqualityComparers/OrganizationCompare.cs +++ b/test/Core.Test/Repositories/EntityFramework/EqualityComparers/OrganizationCompare.cs @@ -42,7 +42,6 @@ namespace Bit.Core.Test.Repositories.EntityFramework.EqualityComparers x.ReferenceData.Equals(y.ReferenceData) && x.Enabled.Equals(y.Enabled) && x.LicenseKey.Equals(y.LicenseKey) && - x.ApiKey.Equals(y.ApiKey) && x.TwoFactorProviders.Equals(y.TwoFactorProviders) && x.ExpirationDate.ToString().Equals(y.ExpirationDate.ToString()); } diff --git a/test/Core.Test/Repositories/EntityFramework/EqualityComparers/OrganizationSponsorshipCompare.cs b/test/Core.Test/Repositories/EntityFramework/EqualityComparers/OrganizationSponsorshipCompare.cs index 97ef30985..8dcb13b91 100644 --- a/test/Core.Test/Repositories/EntityFramework/EqualityComparers/OrganizationSponsorshipCompare.cs +++ b/test/Core.Test/Repositories/EntityFramework/EqualityComparers/OrganizationSponsorshipCompare.cs @@ -8,14 +8,12 @@ namespace Bit.Core.Test.Repositories.EntityFramework.EqualityComparers { public bool Equals(OrganizationSponsorship x, OrganizationSponsorship y) { - return x.InstallationId.Equals(y.InstallationId) && - x.SponsoringOrganizationId.Equals(y.SponsoringOrganizationId) && + return x.SponsoringOrganizationId.Equals(y.SponsoringOrganizationId) && x.SponsoringOrganizationUserId.Equals(y.SponsoringOrganizationUserId) && x.SponsoredOrganizationId.Equals(y.SponsoredOrganizationId) && x.OfferedToEmail.Equals(y.OfferedToEmail) && - x.CloudSponsor.Equals(y.CloudSponsor) && - x.TimesRenewedWithoutValidation.Equals(y.TimesRenewedWithoutValidation) && - x.SponsorshipLapsedDate.ToString().Equals(y.SponsorshipLapsedDate.ToString()); + x.ToDelete.Equals(y.ToDelete) && + x.ValidUntil.ToString().Equals(y.ValidUntil.ToString()); } public int GetHashCode([DisallowNull] OrganizationSponsorship obj) diff --git a/test/Core.Test/Repositories/EntityFramework/OrganizationRepositoryTests.cs b/test/Core.Test/Repositories/EntityFramework/OrganizationRepositoryTests.cs index 251bd08c3..d7e5a192c 100644 --- a/test/Core.Test/Repositories/EntityFramework/OrganizationRepositoryTests.cs +++ b/test/Core.Test/Repositories/EntityFramework/OrganizationRepositoryTests.cs @@ -1,10 +1,8 @@ using System.Collections.Generic; using System.Linq; -using Bit.Core.Models.Data; -using Bit.Core.Test.AutoFixture; +using Bit.Core.Models.Data.Organizations; using Bit.Core.Test.AutoFixture.Attributes; using Bit.Core.Test.AutoFixture.OrganizationFixtures; -using Bit.Core.Test.Helpers.Factories; using Bit.Core.Test.Repositories.EntityFramework.EqualityComparers; using Xunit; using EfRepo = Bit.Infrastructure.EntityFramework.Repositories; diff --git a/test/Core.Test/Repositories/EntityFramework/OrganizationSponsorshipRepositoryTests.cs b/test/Core.Test/Repositories/EntityFramework/OrganizationSponsorshipRepositoryTests.cs index 603b08cae..61a7e5a0e 100644 --- a/test/Core.Test/Repositories/EntityFramework/OrganizationSponsorshipRepositoryTests.cs +++ b/test/Core.Test/Repositories/EntityFramework/OrganizationSponsorshipRepositoryTests.cs @@ -21,7 +21,6 @@ namespace Bit.Core.Test.Repositories.EntityFramework OrganizationSponsorshipCompare equalityComparer, List suts) { - organizationSponsorship.InstallationId = null; organizationSponsorship.SponsoredOrganizationId = null; var savedOrganizationSponsorships = new List(); @@ -56,9 +55,7 @@ namespace Bit.Core.Test.Repositories.EntityFramework SqlRepo.OrganizationSponsorshipRepository sqlOrganizationSponsorshipRepo, OrganizationSponsorshipCompare equalityComparer, List suts) { - postOrganizationSponsorship.InstallationId = null; postOrganizationSponsorship.SponsoredOrganizationId = null; - replaceOrganizationSponsorship.InstallationId = null; replaceOrganizationSponsorship.SponsoredOrganizationId = null; var savedOrganizationSponsorships = new List(); @@ -100,7 +97,6 @@ namespace Bit.Core.Test.Repositories.EntityFramework SqlRepo.OrganizationSponsorshipRepository sqlOrganizationSponsorshipRepo, List suts) { - organizationSponsorship.InstallationId = null; organizationSponsorship.SponsoredOrganizationId = null; foreach (var (sut, orgRepo) in suts.Zip(efOrgRepos)) diff --git a/test/Core.Test/Services/EventServiceTests.cs b/test/Core.Test/Services/EventServiceTests.cs index c4282cee3..f332fd28d 100644 --- a/test/Core.Test/Services/EventServiceTests.cs +++ b/test/Core.Test/Services/EventServiceTests.cs @@ -1,49 +1,44 @@ using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; using Bit.Core.Context; -using Bit.Core.Repositories; +using Bit.Core.Entities; +using Bit.Core.Enums; +using Bit.Core.Models.Data; using Bit.Core.Services; -using Bit.Core.Settings; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; +using Bit.Test.Common.Helpers; using NSubstitute; using Xunit; namespace Bit.Core.Test.Services { + [SutProviderCustomize] public class EventServiceTests { - private readonly EventService _sut; + public static IEnumerable InstallationIdTestCases => TestCaseHelper.GetCombinationsOfMultipleLists( + new object[] { Guid.NewGuid(), null }, + Enum.GetValues().Select(e => (object)e) + ).Select(p => p.ToArray()); - private readonly IEventWriteService _eventWriteService; - private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IProviderUserRepository _providerUserRepository; - private readonly IApplicationCacheService _applicationCacheService; - private readonly CurrentContext _currentContext; - private readonly GlobalSettings _globalSettings; - - public EventServiceTests() + [Theory] + [BitMemberAutoData(nameof(InstallationIdTestCases))] + public async Task LogOrganizationEvent_ProvidesInstallationId(Guid? installationId, EventType eventType, + Organization organization, SutProvider sutProvider) { - _eventWriteService = Substitute.For(); - _organizationUserRepository = Substitute.For(); - _providerUserRepository = Substitute.For(); - _applicationCacheService = Substitute.For(); - _currentContext = new CurrentContext(null); - _globalSettings = new GlobalSettings(); + organization.Enabled = true; + organization.UseEvents = true; - _sut = new EventService( - _eventWriteService, - _organizationUserRepository, - _providerUserRepository, - _applicationCacheService, - _currentContext, - _globalSettings - ); - } + sutProvider.GetDependency().InstallationId.Returns(installationId); - // Remove this test when we add actual tests. It only proves that - // we've properly constructed the system under test. - [Fact] - public void ServiceExists() - { - Assert.NotNull(_sut); + await sutProvider.Sut.LogOrganizationEventAsync(organization, eventType); + + await sutProvider.GetDependency().Received(1).CreateAsync(Arg.Is(e => + e.OrganizationId == organization.Id && + e.Type == eventType && + e.InstallationId == installationId)); } } } diff --git a/test/Core.Test/Services/HandlebarsMailServiceTests.cs b/test/Core.Test/Services/HandlebarsMailServiceTests.cs index f5931e2c8..1ac17845b 100644 --- a/test/Core.Test/Services/HandlebarsMailServiceTests.cs +++ b/test/Core.Test/Services/HandlebarsMailServiceTests.cs @@ -115,7 +115,6 @@ namespace Bit.Core.Test.Services { ("familyUserEmail", typeof(string)), "test@bitwarden.com" }, { ("sponsorEmail", typeof(string)), "test@bitwarden.com" }, { ("familyOrgName", typeof(string)), "Test Org Name" }, - { ("orgCanSponsor", typeof(bool)), true }, { ("existingAccount", typeof(bool)), true }, { ("sponsorshipEndDate", typeof(DateTime)), DateTime.UtcNow.AddDays(1)}, }; diff --git a/test/Core.Test/Services/LicensingServiceTests.cs b/test/Core.Test/Services/LicensingServiceTests.cs index 076293631..d4aa54b15 100644 --- a/test/Core.Test/Services/LicensingServiceTests.cs +++ b/test/Core.Test/Services/LicensingServiceTests.cs @@ -1,53 +1,64 @@ using System; -using Bit.Core.Repositories; +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; +using AutoFixture; +using Bit.Core.Entities; +using Bit.Core.Models.Business; using Bit.Core.Services; using Bit.Core.Settings; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Logging; -using NSubstitute; +using Bit.Core.Test.AutoFixture; +using Bit.Test.Common.AutoFixture; +using Bit.Test.Common.AutoFixture.Attributes; using Xunit; namespace Bit.Core.Test.Services { + [SutProviderCustomize] public class LicensingServiceTests { - private readonly LicensingService _sut; - - private readonly GlobalSettings _globalSettings; - private readonly IUserRepository _userRepository; - private readonly IOrganizationRepository _organizationRepository; - private readonly IOrganizationUserRepository _organizationUserRepository; - private readonly IMailService _mailService; - private readonly IWebHostEnvironment _hostingEnvironment; - private readonly ILogger _logger; - - public LicensingServiceTests() + private static string licenseFilePath(Guid orgId) => + Path.Combine(OrganizationLicenseDirectory.Value, $"{orgId}.json"); + private static string LicenseDirectory => Path.GetDirectoryName(OrganizationLicenseDirectory.Value); + private static Lazy OrganizationLicenseDirectory => new(() => { - _userRepository = Substitute.For(); - _organizationRepository = Substitute.For(); - _organizationUserRepository = Substitute.For(); - _mailService = Substitute.For(); - _hostingEnvironment = Substitute.For(); - _logger = Substitute.For>(); - _globalSettings = new GlobalSettings(); + var directory = Path.Combine(Path.GetTempPath(), "organization"); + if (!Directory.Exists(directory)) + { + Directory.CreateDirectory(directory); + } + return directory; + }); - _sut = new LicensingService( - _userRepository, - _organizationRepository, - _organizationUserRepository, - _mailService, - _hostingEnvironment, - _logger, - _globalSettings - ); + public static SutProvider GetSutProvider() + { + var fixture = new Fixture().WithAutoNSubstitutions(); + + var settings = fixture.Create(); + settings.LicenseDirectory = LicenseDirectory; + settings.SelfHosted = true; + + return new SutProvider(fixture) + .SetDependency(settings) + .Create(); } - // Remove this test when we add actual tests. It only proves that - // we've properly constructed the system under test. - [Fact(Skip = "Needs additional work")] - public void ServiceExists() + [Theory, BitAutoData, OrganizationLicenseCustomize] + public async Task ReadOrganizationLicense(Organization organization, OrganizationLicense license) { - Assert.NotNull(_sut); + var sutProvider = GetSutProvider(); + + File.WriteAllText(licenseFilePath(organization.Id), JsonSerializer.Serialize(license)); + + var actual = await sutProvider.Sut.ReadOrganizationLicenseAsync(organization); + try + { + Assert.Equal(JsonSerializer.Serialize(license), JsonSerializer.Serialize(actual)); + } + finally + { + Directory.Delete(OrganizationLicenseDirectory.Value, true); + } } } } diff --git a/test/Core.Test/Services/MultiServicePushNotificationServiceTests.cs b/test/Core.Test/Services/MultiServicePushNotificationServiceTests.cs index c99347546..9190fb5b0 100644 --- a/test/Core.Test/Services/MultiServicePushNotificationServiceTests.cs +++ b/test/Core.Test/Services/MultiServicePushNotificationServiceTests.cs @@ -1,4 +1,5 @@ using System; +using System.Net.Http; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -13,6 +14,7 @@ namespace Bit.Core.Test.Services { private readonly MultiServicePushNotificationService _sut; + private readonly IHttpClientFactory _httpFactory; private readonly IDeviceRepository _deviceRepository; private readonly IInstallationDeviceRepository _installationDeviceRepository; private readonly GlobalSettings _globalSettings; @@ -23,6 +25,7 @@ namespace Bit.Core.Test.Services public MultiServicePushNotificationServiceTests() { + _httpFactory = Substitute.For(); _deviceRepository = Substitute.For(); _installationDeviceRepository = Substitute.For(); _globalSettings = new GlobalSettings(); @@ -32,6 +35,7 @@ namespace Bit.Core.Test.Services _hubLogger = Substitute.For>(); _sut = new MultiServicePushNotificationService( + _httpFactory, _deviceRepository, _installationDeviceRepository, _globalSettings, diff --git a/test/Core.Test/Services/NotificationsApiPushNotificationServiceTests.cs b/test/Core.Test/Services/NotificationsApiPushNotificationServiceTests.cs index b685e4fc6..f014dddcc 100644 --- a/test/Core.Test/Services/NotificationsApiPushNotificationServiceTests.cs +++ b/test/Core.Test/Services/NotificationsApiPushNotificationServiceTests.cs @@ -1,4 +1,5 @@ using System; +using System.Net.Http; using Bit.Core.Services; using Bit.Core.Settings; using Microsoft.AspNetCore.Http; @@ -12,17 +13,20 @@ namespace Bit.Core.Test.Services { private readonly NotificationsApiPushNotificationService _sut; + private readonly IHttpClientFactory _httpFactory; private readonly GlobalSettings _globalSettings; private readonly IHttpContextAccessor _httpContextAccessor; private readonly ILogger _logger; public NotificationsApiPushNotificationServiceTests() { + _httpFactory = Substitute.For(); _globalSettings = new GlobalSettings(); _httpContextAccessor = Substitute.For(); _logger = Substitute.For>(); _sut = new NotificationsApiPushNotificationService( + _httpFactory, _globalSettings, _httpContextAccessor, _logger diff --git a/test/Core.Test/Services/OrganizationServiceTests.cs b/test/Core.Test/Services/OrganizationServiceTests.cs index 02207d4ed..b7ad40c9a 100644 --- a/test/Core.Test/Services/OrganizationServiceTests.cs +++ b/test/Core.Test/Services/OrganizationServiceTests.cs @@ -9,6 +9,7 @@ using Bit.Core.Enums; using Bit.Core.Exceptions; using Bit.Core.Models.Business; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -66,7 +67,6 @@ namespace Bit.Core.Test.Services .CreateManyAsync(Arg.Is>(users => users.Count() == expectedNewUsersCount)); await sutProvider.GetDependency().Received(1) .BulkSendOrganizationInviteEmailAsync(org.Name, - Arg.Any(), Arg.Is>(messages => messages.Count() == expectedNewUsersCount)); // Send events @@ -125,7 +125,6 @@ namespace Bit.Core.Test.Services .CreateManyAsync(Arg.Is>(users => users.Count() == expectedNewUsersCount)); await sutProvider.GetDependency().Received(1) .BulkSendOrganizationInviteEmailAsync(org.Name, - Arg.Any(), Arg.Is>(messages => messages.Count() == expectedNewUsersCount)); // Sent events @@ -362,7 +361,7 @@ namespace Bit.Core.Test.Services await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, invites); await sutProvider.GetDependency().Received(1) - .BulkSendOrganizationInviteEmailAsync(organization.Name, Arg.Any(), + .BulkSendOrganizationInviteEmailAsync(organization.Name, Arg.Is>(v => v.Count() == invites.SelectMany(i => i.invite.Emails).Count())); } diff --git a/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs b/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs deleted file mode 100644 index 0b43fcd83..000000000 --- a/test/Core.Test/Services/OrganizationSponsorshipServiceTests.cs +++ /dev/null @@ -1,680 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Bit.Core.Entities; -using Bit.Core.Enums; -using Bit.Core.Exceptions; -using Bit.Core.Repositories; -using Bit.Core.Services; -using Bit.Core.Utilities; -using Bit.Test.Common.AutoFixture; -using Bit.Test.Common.AutoFixture.Attributes; -using Bit.Test.Common.Helpers; -using Microsoft.AspNetCore.DataProtection; -using NSubstitute; -using NSubstitute.ExceptionExtensions; -using Xunit; - -namespace Bit.Core.Test.Services -{ - [SutProviderCustomize] - public class OrganizationSponsorshipServiceTests - { - private bool SponsorshipValidator(OrganizationSponsorship sponsorship, OrganizationSponsorship expectedSponsorship) - { - try - { - AssertHelper.AssertPropertyEqual(sponsorship, expectedSponsorship, nameof(OrganizationSponsorship.Id)); - return true; - } - catch - { - return false; - } - } - - public static IEnumerable EnterprisePlanTypes => - Enum.GetValues().Where(p => StaticStore.GetPlan(p).Product == ProductType.Enterprise).Select(p => new object[] { p }); - - public static IEnumerable NonEnterprisePlanTypes => - Enum.GetValues().Where(p => StaticStore.GetPlan(p).Product != ProductType.Enterprise).Select(p => new object[] { p }); - - public static IEnumerable NonFamiliesPlanTypes => - Enum.GetValues().Where(p => StaticStore.GetPlan(p).Product != ProductType.Families).Select(p => new object[] { p }); - - public static IEnumerable NonConfirmedOrganizationUsersStatuses => - Enum.GetValues() - .Where(s => s != OrganizationUserStatusType.Confirmed) - .Select(s => new object[] { s }); - - [Theory] - [BitMemberAutoData(nameof(NonEnterprisePlanTypes))] - public async Task OfferSponsorship_BadSponsoringOrgPlan_ThrowsBadRequest(PlanType sponsoringOrgPlan, - Organization org, OrganizationUser orgUser, SutProvider sutProvider) - { - org.PlanType = sponsoringOrgPlan; - - sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); - - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.OfferSponsorshipAsync(org, orgUser, PlanSponsorshipType.FamiliesForEnterprise, default, default, "test@bitwarden.com")); - - Assert.Contains("Specified Organization cannot sponsor other organizations.", exception.Message); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .CreateAsync(default); - } - - [Theory] - [BitMemberAutoData(nameof(NonConfirmedOrganizationUsersStatuses))] - public async Task CreateSponsorship_BadSponsoringUserStatus_ThrowsBadRequest( - OrganizationUserStatusType statusType, Organization org, OrganizationUser orgUser, - SutProvider sutProvider) - { - org.PlanType = PlanType.EnterpriseAnnually; - orgUser.Status = statusType; - - sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); - - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.OfferSponsorshipAsync(org, orgUser, PlanSponsorshipType.FamiliesForEnterprise, default, default, "test@bitwarden.com")); - - Assert.Contains("Only confirmed users can sponsor other organizations.", exception.Message); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .CreateAsync(default); - } - - [Theory] - [BitAutoData] - public async Task OfferSponsorship_AlreadySponsoring_Throws(Organization org, - OrganizationUser orgUser, OrganizationSponsorship sponsorship, - SutProvider sutProvider) - { - org.PlanType = PlanType.EnterpriseAnnually; - orgUser.Status = OrganizationUserStatusType.Confirmed; - - sutProvider.GetDependency() - .GetBySponsoringOrganizationUserIdAsync(orgUser.Id).Returns(sponsorship); - - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.OfferSponsorshipAsync(org, orgUser, sponsorship.PlanSponsorshipType.Value, default, default, "test@bitwarden.com")); - - Assert.Contains("Can only sponsor one organization per Organization User.", exception.Message); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .CreateAsync(default); - } - - [Theory] - [BitAutoData] - public async Task OfferSponsorship_CreatesSponsorship(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, - string sponsoredEmail, string friendlyName, Guid sponsorshipId, - SutProvider sutProvider) - { - const string email = "test@bitwarden.com"; - - sponsoringOrg.PlanType = PlanType.EnterpriseAnnually; - sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed; - - var dataProtector = Substitute.For(); - sutProvider.GetDependency().CreateProtector(default).ReturnsForAnyArgs(dataProtector); - sutProvider.GetDependency().CreateAsync(default).ReturnsForAnyArgs(callInfo => - { - var sponsorship = callInfo.Arg(); - sponsorship.Id = sponsorshipId; - return sponsorship; - }); - - await sutProvider.Sut.OfferSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, - PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, email); - - var expectedSponsorship = new OrganizationSponsorship - { - Id = sponsorshipId, - SponsoringOrganizationId = sponsoringOrg.Id, - SponsoringOrganizationUserId = sponsoringOrgUser.Id, - FriendlyName = friendlyName, - OfferedToEmail = sponsoredEmail, - PlanSponsorshipType = PlanSponsorshipType.FamiliesForEnterprise, - CloudSponsor = true, - }; - - await sutProvider.GetDependency().Received(1) - .UpsertAsync(Arg.Is(s => SponsorshipValidator(s, expectedSponsorship))); - - await sutProvider.GetDependency().Received(1). - SendFamiliesForEnterpriseOfferEmailAsync(sponsoredEmail, email, - false, Arg.Any()); - } - - [Theory] - [BitAutoData] - public async Task OfferSponsorship_CreateSponsorshipThrows_RevertsDatabase(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, - string sponsoredEmail, string friendlyName, SutProvider sutProvider) - { - sponsoringOrg.PlanType = PlanType.EnterpriseAnnually; - sponsoringOrgUser.Status = OrganizationUserStatusType.Confirmed; - - var expectedException = new Exception(); - OrganizationSponsorship createdSponsorship = null; - sutProvider.GetDependency().UpsertAsync(default).ThrowsForAnyArgs(callInfo => - { - createdSponsorship = callInfo.ArgAt(0); - createdSponsorship.Id = Guid.NewGuid(); - return expectedException; - }); - - var actualException = await Assert.ThrowsAsync(() => - sutProvider.Sut.OfferSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, - PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, "test@bitwarden.com")); - Assert.Same(expectedException, actualException); - - await sutProvider.GetDependency().Received(1) - .DeleteAsync(createdSponsorship); - } - - [Theory] - [BitAutoData] - public async Task ResendSponsorshipOffer_SponsoringOrgNotFound_ThrowsBadRequest( - OrganizationUser orgUser, OrganizationSponsorship sponsorship, - SutProvider sutProvider) - { - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.ResendSponsorshipOfferAsync(null, orgUser, sponsorship, "test@bitwarden.com")); - - Assert.Contains("Cannot find the requested sponsoring organization.", exception.Message); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .SendFamiliesForEnterpriseOfferEmailAsync(default, default, default, default); - } - - [Theory] - [BitAutoData] - public async Task ResendSponsorshipOffer_SponsoringOrgUserNotFound_ThrowsBadRequest(Organization org, - OrganizationSponsorship sponsorship, SutProvider sutProvider) - { - sutProvider.GetDependency().GetByIdAsync(org.Id).Returns(org); - - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.ResendSponsorshipOfferAsync(org, null, sponsorship, "test@bitwarden.com")); - - Assert.Contains("Only confirmed users can sponsor other organizations.", exception.Message); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .SendFamiliesForEnterpriseOfferEmailAsync(default, default, default, default); - } - - [Theory] - [BitAutoData] - [BitMemberAutoData(nameof(NonConfirmedOrganizationUsersStatuses))] - public async Task ResendSponsorshipOffer_SponsoringOrgUserNotConfirmed_ThrowsBadRequest(OrganizationUserStatusType status, - Organization org, OrganizationUser orgUser, OrganizationSponsorship sponsorship, - SutProvider sutProvider) - { - orgUser.Status = status; - - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.ResendSponsorshipOfferAsync(org, orgUser, sponsorship, "test@bitwarden.com")); - - Assert.Contains("Only confirmed users can sponsor other organizations.", exception.Message); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .SendFamiliesForEnterpriseOfferEmailAsync(default, default, default, default); - } - - [Theory] - [BitAutoData] - public async Task ResendSponsorshipOffer_SponsorshipNotFound_ThrowsBadRequest(Organization org, - OrganizationUser orgUser, - SutProvider sutProvider) - { - orgUser.Status = OrganizationUserStatusType.Confirmed; - - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.ResendSponsorshipOfferAsync(org, orgUser, null, "test@bitwarden.com")); - - Assert.Contains("Cannot find an outstanding sponsorship offer for this organization.", exception.Message); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .SendFamiliesForEnterpriseOfferEmailAsync(default, default, default, default); - } - - [Theory] - [BitAutoData] - public async Task ResendSponsorshipOffer_NoOfferToEmail_ThrowsBadRequest(Organization org, - OrganizationUser orgUser, OrganizationSponsorship sponsorship, - SutProvider sutProvider) - { - orgUser.Status = OrganizationUserStatusType.Confirmed; - sponsorship.OfferedToEmail = null; - - sutProvider.GetDependency().GetBySponsoringOrganizationUserIdAsync(orgUser.Id) - .Returns(sponsorship); - - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.ResendSponsorshipOfferAsync(org, orgUser, sponsorship, "test@bitwarden.com")); - - Assert.Contains("Cannot find an outstanding sponsorship offer for this organization.", exception.Message); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .SendFamiliesForEnterpriseOfferEmailAsync(default, default, default, default); - } - - - [Theory] - [BitAutoData] - public async Task SendSponsorshipOfferAsync(OrganizationSponsorship sponsorship, - SutProvider sutProvider) - { - const string email = "test@bitwarden.com"; - - sutProvider.GetDependency() - .GetByEmailAsync(sponsorship.OfferedToEmail) - .Returns(Task.FromResult(new User())); - - await sutProvider.Sut.SendSponsorshipOfferAsync(sponsorship, email); - - await sutProvider.GetDependency().Received(1) - .SendFamiliesForEnterpriseOfferEmailAsync(sponsorship.OfferedToEmail, email, true, Arg.Any()); - } - - [Theory] - [BitAutoData] - public async Task SetUpSponsorship_SponsorshipNotFound_ThrowsBadRequest(Organization org, - SutProvider sutProvider) - { - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.SetUpSponsorshipAsync(null, org)); - - Assert.Contains("No unredeemed sponsorship offer exists for you.", exception.Message); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .SponsorOrganizationAsync(default, default); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .UpsertAsync(default); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .UpsertAsync(default); - } - - [Theory] - [BitAutoData] - public async Task SetUpSponsorship_OrgAlreadySponsored_ThrowsBadRequest(Organization org, - OrganizationSponsorship sponsorship, OrganizationSponsorship existingSponsorship, - SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetBySponsoredOrganizationIdAsync(org.Id).Returns(existingSponsorship); - - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.SetUpSponsorshipAsync(sponsorship, org)); - - Assert.Contains("Cannot redeem a sponsorship offer for an organization that is already sponsored. Revoke existing sponsorship first.", exception.Message); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .SponsorOrganizationAsync(default, default); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .UpsertAsync(default); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .UpsertAsync(default); - } - - [Theory] - [BitMemberAutoData(nameof(NonFamiliesPlanTypes))] - public async Task SetUpSponsorship_OrgNotFamiles_ThrowsBadRequest(PlanType planType, - OrganizationSponsorship sponsorship, Organization org, - SutProvider sutProvider) - { - org.PlanType = planType; - - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.SetUpSponsorshipAsync(sponsorship, org)); - - Assert.Contains("Can only redeem sponsorship offer on families organizations.", exception.Message); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .SponsorOrganizationAsync(default, default); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .UpsertAsync(default); - await sutProvider.GetDependency() - .DidNotReceiveWithAnyArgs() - .UpsertAsync(default); - } - - private async Task AssertRemovedSponsoredPaymentAsync(Organization sponsoredOrg, - OrganizationSponsorship sponsorship, SutProvider sutProvider) - { - await sutProvider.GetDependency().Received(1) - .RemoveOrganizationSponsorshipAsync(sponsoredOrg, sponsorship); - await sutProvider.GetDependency().Received(1).UpsertAsync(sponsoredOrg); - await sutProvider.GetDependency().Received(1) - .SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(sponsoredOrg.BillingEmailAddress(), sponsoredOrg.Name); - } - - private async Task AssertRemovedSponsorshipAsync(OrganizationSponsorship sponsorship, - SutProvider sutProvider) - { - if (sponsorship.CloudSponsor || sponsorship.SponsorshipLapsedDate.HasValue) - { - await sutProvider.GetDependency().Received(1) - .DeleteAsync(sponsorship); - } - else - { - await sutProvider.GetDependency().Received(1) - .UpsertAsync(sponsorship); - } - } - - private static async Task AssertDidNotRemoveSponsoredPaymentAsync(SutProvider sutProvider) - { - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .RemoveOrganizationSponsorshipAsync(default, default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .UpsertAsync(default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .SendFamiliesForEnterpriseSponsorshipRevertingEmailAsync(default, default); - } - - private static async Task AssertDidNotRemoveSponsorshipAsync(SutProvider sutProvider) - { - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .DeleteAsync(default); - await sutProvider.GetDependency().DidNotReceiveWithAnyArgs() - .UpsertAsync(default); - } - - [Theory] - [BitAutoData] - public async Task ValidateSponsorshipAsync_NoSponsoredOrg_EarlyReturn(Guid sponsoredOrgId, - SutProvider sutProvider) - { - sutProvider.GetDependency().GetByIdAsync(sponsoredOrgId).Returns((Organization)null); - - var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrgId); - - Assert.False(result); - await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); - await AssertDidNotRemoveSponsorshipAsync(sutProvider); - } - - [Theory] - [BitAutoData] - public async Task ValidateSponsorshipAsync_NoExistingSponsorship_UpdatesStripePlan(Organization sponsoredOrg, - SutProvider sutProvider) - { - sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); - - var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); - - Assert.False(result); - await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, null, sutProvider); - } - - [Theory] - [BitAutoData] - public async Task ValidateSponsorshipAsync_SponsoringOrgNull_UpdatesStripePlan(Organization sponsoredOrg, - OrganizationSponsorship existingSponsorship, SutProvider sutProvider) - { - existingSponsorship.SponsoringOrganizationId = null; - - sutProvider.GetDependency() - .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship); - sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); - - var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); - - Assert.False(result); - await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider); - await AssertRemovedSponsorshipAsync(existingSponsorship, sutProvider); - } - - [Theory] - [BitAutoData] - public async Task ValidateSponsorshipAsync_SponsoringOrgUserNull_UpdatesStripePlan(Organization sponsoredOrg, - OrganizationSponsorship existingSponsorship, SutProvider sutProvider) - { - existingSponsorship.SponsoringOrganizationUserId = null; - - sutProvider.GetDependency() - .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship); - sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); - - var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); - - Assert.False(result); - await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider); - await AssertRemovedSponsorshipAsync(existingSponsorship, sutProvider); - } - - [Theory] - [BitAutoData] - public async Task ValidateSponsorshipAsync_SponsorshipTypeNull_UpdatesStripePlan(Organization sponsoredOrg, - OrganizationSponsorship existingSponsorship, SutProvider sutProvider) - { - existingSponsorship.PlanSponsorshipType = null; - - sutProvider.GetDependency() - .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship); - sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); - - var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); - - Assert.False(result); - await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider); - await AssertRemovedSponsorshipAsync(existingSponsorship, sutProvider); - } - - [Theory] - [BitAutoData] - public async Task ValidateSponsorshipAsync_SponsoringOrgNotFound_UpdatesStripePlan(Organization sponsoredOrg, - OrganizationSponsorship existingSponsorship, SutProvider sutProvider) - { - sutProvider.GetDependency() - .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship); - sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); - - var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); - - Assert.False(result); - await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider); - await AssertRemovedSponsorshipAsync(existingSponsorship, sutProvider); - } - - [Theory] - [BitMemberAutoData(nameof(NonEnterprisePlanTypes))] - public async Task ValidateSponsorshipAsync_SponsoringOrgNotEnterprise_UpdatesStripePlan(PlanType planType, - Organization sponsoredOrg, OrganizationSponsorship existingSponsorship, Organization sponsoringOrg, - SutProvider sutProvider) - { - sponsoringOrg.PlanType = planType; - existingSponsorship.SponsoringOrganizationId = sponsoringOrg.Id; - - sutProvider.GetDependency() - .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship); - sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); - sutProvider.GetDependency().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg); - - var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); - - Assert.False(result); - await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider); - await AssertRemovedSponsorshipAsync(existingSponsorship, sutProvider); - } - - [Theory] - [BitMemberAutoData(nameof(EnterprisePlanTypes))] - public async Task ValidateSponsorshipAsync_SponsoringOrgDisabled_UpdatesStripePlan(PlanType planType, - Organization sponsoredOrg, OrganizationSponsorship existingSponsorship, Organization sponsoringOrg, - SutProvider sutProvider) - { - sponsoringOrg.PlanType = planType; - sponsoringOrg.Enabled = false; - existingSponsorship.SponsoringOrganizationId = sponsoringOrg.Id; - - sutProvider.GetDependency() - .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship); - sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); - sutProvider.GetDependency().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg); - - var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); - - Assert.False(result); - await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, existingSponsorship, sutProvider); - await AssertRemovedSponsorshipAsync(existingSponsorship, sutProvider); - } - - [Theory] - [BitMemberAutoData(nameof(EnterprisePlanTypes))] - public async Task ValidateSponsorshipAsync_Valid(PlanType planType, - Organization sponsoredOrg, OrganizationSponsorship existingSponsorship, Organization sponsoringOrg, - SutProvider sutProvider) - { - sponsoringOrg.PlanType = planType; - sponsoringOrg.Enabled = true; - existingSponsorship.SponsoringOrganizationId = sponsoringOrg.Id; - - sutProvider.GetDependency() - .GetBySponsoredOrganizationIdAsync(sponsoredOrg.Id).Returns(existingSponsorship); - sutProvider.GetDependency().GetByIdAsync(sponsoredOrg.Id).Returns(sponsoredOrg); - sutProvider.GetDependency().GetByIdAsync(sponsoringOrg.Id).Returns(sponsoringOrg); - - var result = await sutProvider.Sut.ValidateSponsorshipAsync(sponsoredOrg.Id); - - Assert.True(result); - - await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); - await AssertDidNotRemoveSponsorshipAsync(sutProvider); - } - - [Theory] - [BitAutoData] - public async Task RevokeSponsorship_NoExistingSponsorship_ThrowsBadRequest(Organization org, - SutProvider sutProvider) - { - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.RevokeSponsorshipAsync(org, null)); - - Assert.Contains("You are not currently sponsoring an organization.", exception.Message); - await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); - await AssertDidNotRemoveSponsorshipAsync(sutProvider); - } - - [Theory] - [BitAutoData] - public async Task RevokeSponsorship_SponsorshipNotRedeemed_DeletesSponsorship(Organization org, - OrganizationSponsorship sponsorship, SutProvider sutProvider) - { - sponsorship.SponsoredOrganizationId = null; - - await sutProvider.Sut.RevokeSponsorshipAsync(org, sponsorship); - - await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); - await AssertRemovedSponsorshipAsync(sponsorship, sutProvider); - } - - [Theory] - [BitAutoData] - public async Task RevokeSponsorship_SponsoredOrgNotFound_ThrowsBadRequest(OrganizationSponsorship sponsorship, - SutProvider sutProvider) - { - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.RevokeSponsorshipAsync(null, sponsorship)); - - Assert.Contains("Unable to find the sponsored Organization.", exception.Message); - - await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); - await AssertDidNotRemoveSponsorshipAsync(sutProvider); - } - - [Theory] - [BitAutoData] - public async Task RemoveSponsorship_SponsoredOrgNull_ThrowsBadRequest(OrganizationSponsorship sponsorship, - SutProvider sutProvider) - { - sponsorship.SponsoredOrganizationId = null; - - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.RemoveSponsorshipAsync(null, sponsorship)); - - Assert.Contains("The requested organization is not currently being sponsored.", exception.Message); - - await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); - await AssertDidNotRemoveSponsorshipAsync(sutProvider); - } - - [Theory] - [BitAutoData] - public async Task RemoveSponsorship_SponsorshipNotFound_ThrowsBadRequest(Organization sponsoredOrg, - SutProvider sutProvider) - { - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.RemoveSponsorshipAsync(sponsoredOrg, null)); - - Assert.Contains("The requested organization is not currently being sponsored.", exception.Message); - - await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); - await AssertDidNotRemoveSponsorshipAsync(sutProvider); - } - - [Theory] - [BitAutoData] - public async Task RemoveSponsorship_SponsoredOrgNotFound_ThrowsBadRequest(OrganizationSponsorship sponsorship, - SutProvider sutProvider) - { - var exception = await Assert.ThrowsAsync(() => - sutProvider.Sut.RemoveSponsorshipAsync(null, sponsorship)); - - Assert.Contains("Unable to find the sponsored Organization.", exception.Message); - - await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); - await AssertDidNotRemoveSponsorshipAsync(sutProvider); - } - - [Theory] - [BitAutoData] - public async Task DoRemoveSponsorshipAsync_NullDoNothing(SutProvider sutProvider) - { - await sutProvider.Sut.DoRemoveSponsorshipAsync(null, null); - - await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); - await AssertDidNotRemoveSponsorshipAsync(sutProvider); - } - - [Theory] - [BitAutoData] - public async Task DoRemoveSponsorshipAsync_NullSponsoredOrg(OrganizationSponsorship sponsorship, - SutProvider sutProvider) - { - await sutProvider.Sut.DoRemoveSponsorshipAsync(null, sponsorship); - - await AssertDidNotRemoveSponsoredPaymentAsync(sutProvider); - await AssertRemovedSponsorshipAsync(sponsorship, sutProvider); - } - - [Theory] - [BitAutoData] - public async Task DoRemoveSponsorshipAsync_NullSponsorship(Organization sponsoredOrg, - SutProvider sutProvider) - { - await sutProvider.Sut.DoRemoveSponsorshipAsync(sponsoredOrg, null); - - await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, null, sutProvider); - await AssertDidNotRemoveSponsorshipAsync(sutProvider); - } - - [Theory] - [BitAutoData] - public async Task DoRemoveSponsorshipAsync_RemoveBoth(Organization sponsoredOrg, - OrganizationSponsorship sponsorship, SutProvider sutProvider) - { - await sutProvider.Sut.DoRemoveSponsorshipAsync(sponsoredOrg, sponsorship); - - await AssertRemovedSponsoredPaymentAsync(sponsoredOrg, sponsorship, sutProvider); - await AssertRemovedSponsorshipAsync(sponsorship, sutProvider); - } - } -} diff --git a/test/Core.Test/Services/PolicyServiceTests.cs b/test/Core.Test/Services/PolicyServiceTests.cs index c8b49918c..f86b81ab6 100644 --- a/test/Core.Test/Services/PolicyServiceTests.cs +++ b/test/Core.Test/Services/PolicyServiceTests.cs @@ -276,7 +276,7 @@ namespace Bit.Core.Test.Services Enabled = false, }); - var orgUserDetail = new Core.Models.Data.OrganizationUserUserDetails + var orgUserDetail = new Core.Models.Data.Organizations.OrganizationUsers.OrganizationUserUserDetails { Id = Guid.NewGuid(), Status = OrganizationUserStatusType.Accepted, @@ -289,7 +289,7 @@ namespace Bit.Core.Test.Services sutProvider.GetDependency() .GetManyDetailsByOrganizationAsync(policy.OrganizationId) - .Returns(new List + .Returns(new List { orgUserDetail, }); @@ -345,7 +345,7 @@ namespace Bit.Core.Test.Services Enabled = false, }); - var orgUserDetail = new Core.Models.Data.OrganizationUserUserDetails + var orgUserDetail = new Core.Models.Data.Organizations.OrganizationUsers.OrganizationUserUserDetails { Id = Guid.NewGuid(), Status = OrganizationUserStatusType.Accepted, @@ -358,7 +358,7 @@ namespace Bit.Core.Test.Services sutProvider.GetDependency() .GetManyDetailsByOrganizationAsync(policy.OrganizationId) - .Returns(new List + .Returns(new List { orgUserDetail, }); diff --git a/test/Core.Test/Services/RelayPushNotificationServiceTests.cs b/test/Core.Test/Services/RelayPushNotificationServiceTests.cs index 1dffd5cd1..e58c851ac 100644 --- a/test/Core.Test/Services/RelayPushNotificationServiceTests.cs +++ b/test/Core.Test/Services/RelayPushNotificationServiceTests.cs @@ -1,4 +1,5 @@ using System; +using System.Net.Http; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -13,6 +14,7 @@ namespace Bit.Core.Test.Services { private readonly RelayPushNotificationService _sut; + private readonly IHttpClientFactory _httpFactory; private readonly IDeviceRepository _deviceRepository; private readonly GlobalSettings _globalSettings; private readonly IHttpContextAccessor _httpContextAccessor; @@ -20,12 +22,14 @@ namespace Bit.Core.Test.Services public RelayPushNotificationServiceTests() { + _httpFactory = Substitute.For(); _deviceRepository = Substitute.For(); _globalSettings = new GlobalSettings(); _httpContextAccessor = Substitute.For(); _logger = Substitute.For>(); _sut = new RelayPushNotificationService( + _httpFactory, _deviceRepository, _globalSettings, _httpContextAccessor, diff --git a/test/Core.Test/Services/RelayPushRegistrationServiceTests.cs b/test/Core.Test/Services/RelayPushRegistrationServiceTests.cs index dfe69fc66..d15db090c 100644 --- a/test/Core.Test/Services/RelayPushRegistrationServiceTests.cs +++ b/test/Core.Test/Services/RelayPushRegistrationServiceTests.cs @@ -1,4 +1,5 @@ using System; +using System.Net.Http; using Bit.Core.Services; using Bit.Core.Settings; using Microsoft.Extensions.Logging; @@ -11,15 +12,18 @@ namespace Bit.Core.Test.Services { private readonly RelayPushRegistrationService _sut; + private readonly IHttpClientFactory _httpFactory; private readonly GlobalSettings _globalSettings; private readonly ILogger _logger; public RelayPushRegistrationServiceTests() { _globalSettings = new GlobalSettings(); + _httpFactory = Substitute.For(); _logger = Substitute.For>(); _sut = new RelayPushRegistrationService( + _httpFactory, _globalSettings, _logger ); diff --git a/test/Core.Test/Services/SsoConfigServiceTests.cs b/test/Core.Test/Services/SsoConfigServiceTests.cs index 3438ee922..a159b10ff 100644 --- a/test/Core.Test/Services/SsoConfigServiceTests.cs +++ b/test/Core.Test/Services/SsoConfigServiceTests.cs @@ -3,6 +3,7 @@ using System.Threading.Tasks; using Bit.Core.Entities; using Bit.Core.Exceptions; using Bit.Core.Models.Data; +using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Test.Common.AutoFixture; diff --git a/test/Core.Test/Tokens/DataProtectorTokenFactoryTests.cs b/test/Core.Test/Tokens/DataProtectorTokenFactoryTests.cs index 35fcd33a5..8a75a0790 100644 --- a/test/Core.Test/Tokens/DataProtectorTokenFactoryTests.cs +++ b/test/Core.Test/Tokens/DataProtectorTokenFactoryTests.cs @@ -1,4 +1,5 @@ -using AutoFixture; +using System.Security.Cryptography; +using AutoFixture; using Bit.Core.Tokens; using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture.Attributes; @@ -50,5 +51,78 @@ namespace Bit.Core.Test.Tokens Assert.NotEqual(new Token(token).RemovePrefix(prefix), tokenable.ToToken()); } + + [Theory, BitAutoData] + public void ThrowsIfUnprotectFails(TestTokenable tokenable) + { + var sutProvider = GetSutProvider(); + + var token = sutProvider.Sut.Protect(tokenable); + token += "stuff to make sure decryption fails"; + + Assert.Throws(() => sutProvider.Sut.Unprotect(token)); + } + + [Theory, BitAutoData] + public void TryUnprotect_FalseIfUnprotectFails(TestTokenable tokenable) + { + var sutProvider = GetSutProvider(); + var token = sutProvider.Sut.Protect(tokenable) + "fail decryption"; + + var result = sutProvider.Sut.TryUnprotect(token, out var data); + + Assert.False(result); + Assert.Null(data); + } + + [Theory, BitAutoData] + public void TokenValid_FalseIfUnprotectFails(TestTokenable tokenable) + { + var sutProvider = GetSutProvider(); + var token = sutProvider.Sut.Protect(tokenable) + "fail decryption"; + + var result = sutProvider.Sut.TokenValid(token); + + Assert.False(result); + } + + + [Theory, BitAutoData] + public void TokenValid_FalseIfTokenInvalid(TestTokenable tokenable) + { + var sutProvider = GetSutProvider(); + + tokenable.ForceInvalid = true; + var token = sutProvider.Sut.Protect(tokenable); + + var result = sutProvider.Sut.TokenValid(token); + + Assert.False(result); + } + + [Theory, BitAutoData] + public void TryUnprotect_TrueIfSuccess(TestTokenable tokenable) + { + var sutProvider = GetSutProvider(); + var token = sutProvider.Sut.Protect(tokenable); + + var result = sutProvider.Sut.TryUnprotect(token, out var data); + + Assert.True(result); + AssertHelper.AssertPropertyEqual(tokenable, data); + } + + [Theory, BitAutoData] + public void TokenValid_TrueIfSuccess(TestTokenable tokenable) + { + tokenable.ForceInvalid = false; + var sutProvider = GetSutProvider(); + var token = sutProvider.Sut.Protect(tokenable); + + var result = sutProvider.Sut.TokenValid(token); + + Assert.True(result); + } + } } diff --git a/test/Core.Test/Tokens/TestTokenable.cs b/test/Core.Test/Tokens/TestTokenable.cs index 0f2e2536c..7e73cd5e9 100644 --- a/test/Core.Test/Tokens/TestTokenable.cs +++ b/test/Core.Test/Tokens/TestTokenable.cs @@ -1,10 +1,14 @@ -using Bit.Core.Tokens; +using System.Text.Json.Serialization; +using Bit.Core.Tokens; namespace Bit.Core.Test.Tokens { public class TestTokenable : Tokenable { - public override bool Valid => true; + public bool ForceInvalid { get; set; } = false; + + [JsonIgnore] + public override bool Valid => !ForceInvalid; } public class TestExpiringTokenable : ExpiringTokenable diff --git a/test/Core.Test/Utilities/CoreHelpersTests.cs b/test/Core.Test/Utilities/CoreHelpersTests.cs index 6264610f6..e77211be8 100644 --- a/test/Core.Test/Utilities/CoreHelpersTests.cs +++ b/test/Core.Test/Utilities/CoreHelpersTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using AutoFixture; using Bit.Core.Context; @@ -35,6 +36,43 @@ namespace Bit.Core.Test.Utilities // the comb are working properly } + public static IEnumerable GenerateCombCases = new[] + { + new object[] + { + Guid.Parse("a58db474-43d8-42f1-b4ee-0c17647cd0c0"), // Input Guid + new DateTime(2022, 3, 12, 12, 12, 0, DateTimeKind.Utc), // Input Time + Guid.Parse("a58db474-43d8-42f1-b4ee-ae5600c90cc1"), // Expected Comb + }, + new object[] + { + Guid.Parse("f776e6ee-511f-4352-bb28-88513002bdeb"), + new DateTime(2021, 5, 10, 10, 52, 0, DateTimeKind.Utc), + Guid.Parse("f776e6ee-511f-4352-bb28-ad2400b313c1"), + }, + new object[] + { + Guid.Parse("51a25fc7-3cad-497d-8e2f-8d77011648a1"), + new DateTime(1999, 2, 26, 16, 53, 13, DateTimeKind.Utc), + Guid.Parse("51a25fc7-3cad-497d-8e2f-8d77011649cd"), + }, + new object[] + { + Guid.Parse("bfb8f353-3b32-4a9e-bef6-24fe0b54bfb0"), + new DateTime(2024, 10, 20, 1, 32, 16, DateTimeKind.Utc), + Guid.Parse("bfb8f353-3b32-4a9e-bef6-b20f00195780"), + } + }; + + [Theory] + [MemberData(nameof(GenerateCombCases))] + public void GenerateComb_WithInputs_Success(Guid inputGuid, DateTime inputTime, Guid expectedComb) + { + var comb = CoreHelpers.GenerateComb(inputGuid, inputTime); + + Assert.Equal(expectedComb, comb); + } + [Theory] [InlineData(2, 5, new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 })] [InlineData(2, 3, new[] { 1, 2, 3, 4, 5 })] diff --git a/test/Core.Test/packages.lock.json b/test/Core.Test/packages.lock.json index 184ccc2c3..3ef77776e 100644 --- a/test/Core.Test/packages.lock.json +++ b/test/Core.Test/packages.lock.json @@ -28,6 +28,16 @@ "resolved": "3.0.3", "contentHash": "PdyhdzG2LK7YUEtccObPql+3OuFODaFNeYayxdPoK1eHb2StZoeQf1WMb16QrKiIdi4fs5Kog8jxXtlZOgAEuA==" }, + "Kralizek.AutoFixture.Extensions.MockHttp": { + "type": "Direct", + "requested": "[1.2.0, )", + "resolved": "1.2.0", + "contentHash": "6zmks7/5mVczazv910N7V2EdiU6B+rY61lwdgVO0o2iZuTI6KI3T+Hgkrjv0eGOKYucq2OMC+gnAc5Ej2ajoTQ==", + "dependencies": { + "AutoFixture": "4.11.0", + "RichardSzalay.MockHttp": "6.0.0" + } + }, "Microsoft.NET.Test.Sdk": { "type": "Direct", "requested": "[16.6.1, )", @@ -1602,6 +1612,11 @@ "System.Diagnostics.DiagnosticSource": "4.7.1" } }, + "RichardSzalay.MockHttp": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "bStGNqIX/MGYtML7K3EzdsE/k5HGVAcg7XgN23TQXGXqxNC9fvYFR94fA0sGM5hAT36R+BBGet6ZDQxXL/IPxg==" + }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", "resolved": "4.3.2", @@ -3551,6 +3566,7 @@ "AutoFixture.AutoNSubstitute": "4.14.0", "AutoFixture.Xunit2": "4.14.0", "Core": "1.47.1", + "Kralizek.AutoFixture.Extensions.MockHttp": "1.2.0", "Microsoft.NET.Test.Sdk": "16.6.1", "NSubstitute": "4.2.2", "xunit": "2.4.1" diff --git a/test/Identity.Test/packages.lock.json b/test/Identity.Test/packages.lock.json index dac6d3ee1..61b61ddac 100644 --- a/test/Identity.Test/packages.lock.json +++ b/test/Identity.Test/packages.lock.json @@ -309,6 +309,15 @@ "IdentityModel": "4.3.0" } }, + "Kralizek.AutoFixture.Extensions.MockHttp": { + "type": "Transitive", + "resolved": "1.2.0", + "contentHash": "6zmks7/5mVczazv910N7V2EdiU6B+rY61lwdgVO0o2iZuTI6KI3T+Hgkrjv0eGOKYucq2OMC+gnAc5Ej2ajoTQ==", + "dependencies": { + "AutoFixture": "4.11.0", + "RichardSzalay.MockHttp": "6.0.0" + } + }, "libsodium": { "type": "Transitive", "resolved": "1.0.18", @@ -1591,6 +1600,11 @@ "System.Diagnostics.DiagnosticSource": "4.7.1" } }, + "RichardSzalay.MockHttp": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "bStGNqIX/MGYtML7K3EzdsE/k5HGVAcg7XgN23TQXGXqxNC9fvYFR94fA0sGM5hAT36R+BBGet6ZDQxXL/IPxg==" + }, "runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": { "type": "Transitive", "resolved": "4.3.2", @@ -3540,6 +3554,7 @@ "AutoFixture.AutoNSubstitute": "4.14.0", "AutoFixture.Xunit2": "4.14.0", "Core": "1.47.1", + "Kralizek.AutoFixture.Extensions.MockHttp": "1.2.0", "Microsoft.NET.Test.Sdk": "16.6.1", "NSubstitute": "4.2.2", "xunit": "2.4.1" diff --git a/util/EfShared/MigrationBuilderExtensions.cs b/util/EfShared/MigrationBuilderExtensions.cs new file mode 100644 index 000000000..aa8fc04a8 --- /dev/null +++ b/util/EfShared/MigrationBuilderExtensions.cs @@ -0,0 +1,32 @@ + + +using System.Runtime.CompilerServices; +using Bit.Core.Utilities; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Bit +{ + // This file is a manual addition to a project that it helps, a project that chooses to compile it + // should have a projet reference to Core.csproj and a package reference to Microsoft.EntityFrameworkCore.Design + // The reason for this is that if it belonged to it's own library you would have to add manual references to the above + // and manage the version for the EntityFrameworkCore package. This way it also doesn't create another dll + // To include this you can view examples in the MySqlMigrations and PostgresMigrations .csproj files. + // + + public static class MigrationBuilderExtensions + { + /// + /// Reads an embedded resource for it's SQL contents and formats it with the specified direction for easier custom migration steps + /// + /// The MigrationBuilder instance the sql should be applied to + /// The file name portion of the resource name, it is assumed to be in a Scripts folder + /// The direction of the migration taking place + public static void SqlResource(this MigrationBuilder migrationBuilder, string resourceName, [CallerMemberName] string dir = null) + { + var formattedResourceName = string.IsNullOrEmpty(dir) ? resourceName : string.Format(resourceName, dir); + + migrationBuilder.Sql(CoreHelpers.GetEmbeddedResourceContentsAsync( + $"Scripts.{formattedResourceName}")); + } + } +} diff --git a/util/Migrator/DbScripts/2022-03-01_00_AddApiKeysTable.sql b/util/Migrator/DbScripts/2022-03-01_00_AddApiKeysTable.sql new file mode 100644 index 000000000..b1cd1205f --- /dev/null +++ b/util/Migrator/DbScripts/2022-03-01_00_AddApiKeysTable.sql @@ -0,0 +1,1011 @@ +-- Create Organization ApiKey table +IF OBJECT_ID('[dbo].[OrganizationApiKey]') IS NULL +BEGIN +CREATE TABLE [dbo].[OrganizationApiKey] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [OrganizationId] UNIQUEIDENTIFIER NOT NULL, + [Type] TINYINT NOT NULL, + [ApiKey] VARCHAR(30) NOT NULL, + [RevisionDate] DATETIME2(7) NOT NULL, + CONSTRAINT [PK_OrganizationApiKey] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_OrganizationApiKey_OrganizationId] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) +); +END +GO + +-- Create indexes for OrganizationApiKey +IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationApiKey_OrganizationId') +BEGIN +CREATE NONCLUSTERED INDEX [IX_OrganizationApiKey_OrganizationId] + ON [dbo].[OrganizationApiKey]([OrganizationId] ASC); +END +GO + +IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationApiKey_ApiKey') +BEGIN +CREATE NONCLUSTERED INDEX [IX_OrganizationApiKey_ApiKey] + ON [dbo].[OrganizationApiKey]([ApiKey] ASC); +END +GO + +IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'OrganizationApiKeyView') +BEGIN + DROP VIEW [dbo].[OrganizationApiKeyView]; +END +GO + +CREATE VIEW [dbo].[OrganizationApiKeyView] +AS +SELECT + * +FROM + [dbo].[OrganizationApiKey] +GO + +IF OBJECT_ID('[dbo].[OrganizationApiKey_Create]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationApiKey_Create] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationApiKey_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @ApiKey VARCHAR(30), + @Type TINYINT, + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[OrganizationApiKey] + ( + [Id], + [OrganizationId], + [ApiKey], + [Type], + [RevisionDate] + ) + VALUES + ( + @Id, + @OrganizationId, + @ApiKey, + @Type, + @RevisionDate + ) +END +GO + +IF OBJECT_ID('[dbo].[OrganizationApiKey_Update]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationApiKey_Update] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationApiKey_Update] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @ApiKey VARCHAR(30), + @RevisionDate DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[OrganizationApiKey] + SET + [ApiKey] = @ApiKey, + [RevisionDate] = @RevisionDate + WHERE + [Id] = @Id +END +GO + +IF OBJECT_ID('[dbo].[OrganizationApiKey_DeleteById]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationApiKey_DeleteById] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationApiKey_DeleteById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DELETE FROM [dbo].[OrganizationApiKey] + WHERE [Id] = @Id +END +GO + +IF OBJECT_ID('[dbo].[OrganizationApiKey_ReadManyByOrganizationIdType]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationApiKey_ReadManyByOrganizationIdType] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationApiKey_ReadManyByOrganizationIdType] + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT = NULL +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationApiKeyView] + WHERE + [OrganizationId] = @OrganizationId AND + (@Type IS NULL OR [Type] = @Type) +END +GO + +IF OBJECT_ID('[dbo].[OrganizationApiKey_OrganizationDeleted]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationApiKey_OrganizationDeleted] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationApiKey_OrganizationDeleted] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DELETE + FROM + [dbo].[OrganizationApiKey] + WHERE + [OrganizationId] = @OrganizationId +END +GO + +PRINT N'Creating GenerateComb Function' +GO + +CREATE OR ALTER FUNCTION [dbo].[GenerateComb] (@time DATETIME, @uuid UNIQUEIDENTIFIER) +RETURNS UNIQUEIDENTIFIER +AS +BEGIN + DECLARE @comb UNIQUEIDENTIFIER; + + SELECT @comb = CAST( + CAST(@uuid AS BINARY(10)) + + CAST(@time AS BINARY(6)) + AS UNIQUEIDENTIFIER); + + RETURN @comb +END; +GO + +IF COL_LENGTH('[dbo].[Organization]', 'ApiKey') IS NOT NULl +BEGIN + BEGIN TRANSACTION MigrateOrganizationApiKeys + PRINT N'Migrating Organization ApiKeys' + + INSERT INTO [dbo].[OrganizationApiKey] + ( + [Id], + [OrganizationId], + [ApiKey], + [Type], + [RevisionDate] + ) + SELECT + [dbo].[GenerateComb]([CreationDate], NEWID()), + [Id] AS [OrganizationId], + [ApiKey], + 0 AS [Type], -- 0 represents 'Default' type + [RevisionDate] + FROM [dbo].[Organization] + + PRINT N'Dropping old column' + ALTER TABLE + [dbo].[Organization] + DROP COLUMN + [ApiKey] + + COMMIT TRANSACTION MigrateOrganizationApiKeys; +END +GO + +DROP FUNCTION [dbo].[GenerateComb]; +GO + +IF OBJECT_ID('[dbo].[Organization_Create]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Organization_Create] +END +GO + +CREATE PROCEDURE [dbo].[Organization_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @Identifier NVARCHAR(50), + @Name NVARCHAR(50), + @BusinessName NVARCHAR(50), + @BusinessAddress1 NVARCHAR(50), + @BusinessAddress2 NVARCHAR(50), + @BusinessAddress3 NVARCHAR(50), + @BusinessCountry VARCHAR(2), + @BusinessTaxNumber NVARCHAR(30), + @BillingEmail NVARCHAR(256), + @Plan NVARCHAR(50), + @PlanType TINYINT, + @Seats INT, + @MaxCollections SMALLINT, + @UsePolicies BIT, + @UseSso BIT, + @UseGroups BIT, + @UseDirectory BIT, + @UseEvents BIT, + @UseTotp BIT, + @Use2fa BIT, + @UseApi BIT, + @UseResetPassword BIT, + @SelfHost BIT, + @UsersGetPremium BIT, + @Storage BIGINT, + @MaxStorageGb SMALLINT, + @Gateway TINYINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ReferenceData VARCHAR(MAX), + @Enabled BIT, + @LicenseKey VARCHAR(100), + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX), + @TwoFactorProviders NVARCHAR(MAX), + @ExpirationDate DATETIME2(7), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @OwnersNotifiedOfAutoscaling DATETIME2(7), + @MaxAutoscaleSeats INT, + @UseKeyConnector BIT = 0 +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[Organization] + ( + [Id], + [Identifier], + [Name], + [BusinessName], + [BusinessAddress1], + [BusinessAddress2], + [BusinessAddress3], + [BusinessCountry], + [BusinessTaxNumber], + [BillingEmail], + [Plan], + [PlanType], + [Seats], + [MaxCollections], + [UsePolicies], + [UseSso], + [UseGroups], + [UseDirectory], + [UseEvents], + [UseTotp], + [Use2fa], + [UseApi], + [UseResetPassword], + [SelfHost], + [UsersGetPremium], + [Storage], + [MaxStorageGb], + [Gateway], + [GatewayCustomerId], + [GatewaySubscriptionId], + [ReferenceData], + [Enabled], + [LicenseKey], + [PublicKey], + [PrivateKey], + [TwoFactorProviders], + [ExpirationDate], + [CreationDate], + [RevisionDate], + [OwnersNotifiedOfAutoscaling], + [MaxAutoscaleSeats], + [UseKeyConnector] + ) + VALUES + ( + @Id, + @Identifier, + @Name, + @BusinessName, + @BusinessAddress1, + @BusinessAddress2, + @BusinessAddress3, + @BusinessCountry, + @BusinessTaxNumber, + @BillingEmail, + @Plan, + @PlanType, + @Seats, + @MaxCollections, + @UsePolicies, + @UseSso, + @UseGroups, + @UseDirectory, + @UseEvents, + @UseTotp, + @Use2fa, + @UseApi, + @UseResetPassword, + @SelfHost, + @UsersGetPremium, + @Storage, + @MaxStorageGb, + @Gateway, + @GatewayCustomerId, + @GatewaySubscriptionId, + @ReferenceData, + @Enabled, + @LicenseKey, + @PublicKey, + @PrivateKey, + @TwoFactorProviders, + @ExpirationDate, + @CreationDate, + @RevisionDate, + @OwnersNotifiedOfAutoscaling, + @MaxAutoscaleSeats, + @UseKeyConnector + ) +END +GO + +IF OBJECT_ID('[dbo].[Organization_Update]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Organization_Update] +END +GO + +CREATE PROCEDURE [dbo].[Organization_Update] + @Id UNIQUEIDENTIFIER, + @Identifier NVARCHAR(50), + @Name NVARCHAR(50), + @BusinessName NVARCHAR(50), + @BusinessAddress1 NVARCHAR(50), + @BusinessAddress2 NVARCHAR(50), + @BusinessAddress3 NVARCHAR(50), + @BusinessCountry VARCHAR(2), + @BusinessTaxNumber NVARCHAR(30), + @BillingEmail NVARCHAR(256), + @Plan NVARCHAR(50), + @PlanType TINYINT, + @Seats INT, + @MaxCollections SMALLINT, + @UsePolicies BIT, + @UseSso BIT, + @UseGroups BIT, + @UseDirectory BIT, + @UseEvents BIT, + @UseTotp BIT, + @Use2fa BIT, + @UseApi BIT, + @UseResetPassword BIT, + @SelfHost BIT, + @UsersGetPremium BIT, + @Storage BIGINT, + @MaxStorageGb SMALLINT, + @Gateway TINYINT, + @GatewayCustomerId VARCHAR(50), + @GatewaySubscriptionId VARCHAR(50), + @ReferenceData VARCHAR(MAX), + @Enabled BIT, + @LicenseKey VARCHAR(100), + @PublicKey VARCHAR(MAX), + @PrivateKey VARCHAR(MAX), + @TwoFactorProviders NVARCHAR(MAX), + @ExpirationDate DATETIME2(7), + @CreationDate DATETIME2(7), + @RevisionDate DATETIME2(7), + @OwnersNotifiedOfAutoscaling DATETIME2(7), + @MaxAutoscaleSeats INT, + @UseKeyConnector BIT = 0 +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[Organization] + SET + [Identifier] = @Identifier, + [Name] = @Name, + [BusinessName] = @BusinessName, + [BusinessAddress1] = @BusinessAddress1, + [BusinessAddress2] = @BusinessAddress2, + [BusinessAddress3] = @BusinessAddress3, + [BusinessCountry] = @BusinessCountry, + [BusinessTaxNumber] = @BusinessTaxNumber, + [BillingEmail] = @BillingEmail, + [Plan] = @Plan, + [PlanType] = @PlanType, + [Seats] = @Seats, + [MaxCollections] = @MaxCollections, + [UsePolicies] = @UsePolicies, + [UseSso] = @UseSso, + [UseGroups] = @UseGroups, + [UseDirectory] = @UseDirectory, + [UseEvents] = @UseEvents, + [UseTotp] = @UseTotp, + [Use2fa] = @Use2fa, + [UseApi] = @UseApi, + [UseResetPassword] = @UseResetPassword, + [SelfHost] = @SelfHost, + [UsersGetPremium] = @UsersGetPremium, + [Storage] = @Storage, + [MaxStorageGb] = @MaxStorageGb, + [Gateway] = @Gateway, + [GatewayCustomerId] = @GatewayCustomerId, + [GatewaySubscriptionId] = @GatewaySubscriptionId, + [ReferenceData] = @ReferenceData, + [Enabled] = @Enabled, + [LicenseKey] = @LicenseKey, + [PublicKey] = @PublicKey, + [PrivateKey] = @PrivateKey, + [TwoFactorProviders] = @TwoFactorProviders, + [ExpirationDate] = @ExpirationDate, + [CreationDate] = @CreationDate, + [RevisionDate] = @RevisionDate, + [OwnersNotifiedOfAutoscaling] = @OwnersNotifiedOfAutoscaling, + [MaxAutoscaleSeats] = @MaxAutoscaleSeats, + [UseKeyConnector] = @UseKeyConnector + WHERE + [Id] = @Id +END +GO + +IF OBJECT_ID('[dbo].[OrganizationView]') IS NOT NULL +BEGIN + DROP VIEW [dbo].[OrganizationView] +END +GO + +CREATE VIEW [dbo].[OrganizationView] +AS +SELECT + * +FROM + [dbo].[Organization] +GO + +IF OBJECT_ID('[dbo].[OrganizationConnection]') IS NULL +BEGIN +CREATE TABLE [dbo].[OrganizationConnection] ( + [Id] UNIQUEIDENTIFIER NOT NULL, + [OrganizationId] UNIQUEIDENTIFIER NOT NULL, + [Type] TINYINT NOT NULL, + [Enabled] BIT NOT NULL, + [Config] NVARCHAR (MAX) NULL, + CONSTRAINT [PK_OrganizationConnection] PRIMARY KEY CLUSTERED ([Id] ASC), + CONSTRAINT [FK_OrganizationConnection_OrganizationId] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) +) +END + +-- Create indexes for OrganizationConnection +IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationConnection_OrganizationId') +BEGIN +CREATE NONCLUSTERED INDEX [IX_OrganizationConnection_OrganizationId] + ON [dbo].[OrganizationConnection]([OrganizationId] ASC); +END + +-- Create View +IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'OrganizationConnectionView') +BEGIN + DROP VIEW [dbo].[OrganizationConnectionView] +END +GO + +CREATE VIEW [dbo].[OrganizationConnectionView] +AS +SELECT + * +FROM + [dbo].[OrganizationConnection] +GO + +-- Create Stored Procedures +IF OBJECT_ID('[dbo].[OrganizationConnection_ReadById]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationConnection_ReadById]; +END +GO + +CREATE PROCEDURE [dbo].[OrganizationConnection_ReadById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationConnectionView] + WHERE + [Id] = @Id +END +GO + + +IF OBJECT_ID('[dbo].[OrganizationConnection_Create]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationConnection_Create] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationConnection_Create] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Enabled BIT, + @Config NVARCHAR(MAX) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[OrganizationConnection] + ( + [Id], + [OrganizationId], + [Type], + [Enabled], + [Config] + ) + VALUES + ( + @Id, + @OrganizationId, + @Type, + @Enabled, + @Config + ) +END +GO + +IF OBJECT_ID('[dbo].[OrganizationConnection_Update]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationConnection_Update] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationConnection_Update] + @Id UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Enabled BIT, + @Config NVARCHAR(MAX) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[OrganizationConnection] + SET + [OrganizationId] = @OrganizationId, + [Type] = @Type, + [Enabled] = @Enabled, + [Config] = @Config + WHERE + [Id] = @Id +END +GO + +IF OBJECT_ID('[dbo].[OrganizationConnection_DeleteById]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationConnection_DeleteById] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationConnection_DeleteById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DELETE FROM + [dbo].[OrganizationConnection] + WHERE + [Id] = @Id +END +GO + +IF OBJECT_ID('[dbo].[OrganizationConnection_OrganizationDeleted]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationConnection_OrganizationDeleted] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationConnection_OrganizationDeleted] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + DELETE FROM + [dbo].[OrganizationConnection] + WHERE + [OrganizationId] = @Id +END +GO + +IF OBJECT_ID('[dbo].[OrganizationConnection_ReadEnabledByOrganizationIdType]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationConnection_ReadEnabledByOrganizationIdType]; +END +GO + +CREATE PROCEDURE [dbo].[OrganizationConnection_ReadEnabledByOrganizationIdType] + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationConnectionView] + WHERE + [OrganizationId] = @OrganizationId AND + [Type] = @Type AND + [Enabled] = 1 +END +GO + +DECLARE @TimesRenewedDefaultConstraint NVARCHAR(MAX) = (SELECT [con].[name] +FROM [sys].[default_constraints] [con] +INNER JOIN sys.objects obj ON obj.object_id = con.parent_object_id AND obj.type = 'U' AND obj.name = 'OrganizationSponsorship' +INNER JOIN sys.columns col ON col.column_id = con.parent_column_id AND col.object_id = obj.object_id AND col.name = 'TimesRenewedWithoutValidation' +INNER JOIN sys.schemas sch ON sch.schema_id = obj.schema_id AND sch.name = 'dbo' +WHERE con.[type] = 'D') + +IF @TimesRenewedDefaultConstraint IS NOT NULL +BEGIN + DECLARE @sql NVARCHAR(MAX) = 'ALTER TABLE [dbo].[OrganizationSponsorship] DROP CONSTRAINT ' + @TimesRenewedDefaultConstraint + EXEC sp_executesql @sql +END +GO + +IF COL_LENGTH('[dbo].[OrganizationSponsorship]', 'TimesRenewedWithoutValidation') IS NOT NULL +BEGIN + ALTER TABLE [dbo].[OrganizationSponsorship] DROP COLUMN [TimesRenewedWithoutValidation] +END +GO + +IF COL_LENGTH('[dbo].[OrganizationSponsorship]', 'SponsorshipLapsedDate') IS NOT NULL +BEGIN + EXEC sp_rename '[dbo].[OrganizationSponsorship].[SponsorshipLapsedDate]', 'ValidUntil' +END +GO + +IF COL_LENGTH('[dbo].[OrganizationSponsorship]', 'CloudSponsor') IS NOT NULL +BEGIN + ALTER TABLE [dbo].[OrganizationSponsorship] DROP COLUMN [CloudSponsor] +END +GO + +IF COL_LENGTH('[dbo].[OrganizationSponsorship]', 'ToDelete') IS NULL +BEGIN + ALTER TABLE [dbo].[OrganizationSponsorship] ADD [ToDelete] BIT NOT NULL DEFAULT(0) +END +GO + +IF EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationSponsorship_InstallationId') +BEGIN + DROP INDEX [IX_OrganizationSponsorship_InstallationId] + ON [dbo].[OrganizationSponsorship] +END +GO + +IF EXISTS(SELECT name FROM sys.objects WHERE name = 'FK_OrganizationSponsorship_InstallationId' AND type = 'F') +BEGIN + ALTER TABLE [dbo].[OrganizationSponsorship] DROP CONSTRAINT [FK_OrganizationSponsorship_InstallationId] +END +GO + +IF COL_LENGTH('[dbo].[OrganizationSponsorship]', 'InstallationId') IS NOT NULL +BEGIN + ALTER TABLE [dbo].[OrganizationSponsorship] DROP COLUMN [InstallationId] +END +GO + +IF COLUMNPROPERTY(OBJECT_ID('[dbo].[OrganizationSponsorship]', 'U'), 'SponsoringOrganizationUserID', 'AllowsNull') = 1 +BEGIN + PRINT N'Setting all null SponsoringOrganizationUserID to empty guid' + UPDATE + [dbo].[OrganizationSponsorship] + SET + [SponsoringOrganizationUserID] = '00000000-0000-0000-0000-000000000000' + WHERE + [SponsoringOrganizationUserID] IS NULL; + + DROP INDEX [IX_OrganizationSponsorship_SponsoringOrganizationUserId] + ON [dbo].[OrganizationSponsorship] + + ALTER TABLE [dbo].[OrganizationSponsorship] ALTER COLUMN [SponsoringOrganizationUserID] UNIQUEIDENTIFIER NOT NULL; + + CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationUserId] + ON [dbo].[OrganizationSponsorship]([SponsoringOrganizationUserID] ASC); +END +GO + +-- Remake View +IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'OrganizationSponsorshipView') +BEGIN + DROP VIEW [dbo].[OrganizationSponsorshipView] +END +GO + +CREATE VIEW [dbo].[OrganizationSponsorshipView] +AS +SELECT + * +FROM + [dbo].[OrganizationSponsorship] +GO + +-- OrganizationSponsorship_Create +IF OBJECT_ID('[dbo].[OrganizationSponsorship_Create]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationSponsorship_Create] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationSponsorship_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @SponsoringOrganizationId UNIQUEIDENTIFIER, + @SponsoringOrganizationUserID UNIQUEIDENTIFIER, + @SponsoredOrganizationId UNIQUEIDENTIFIER, + @FriendlyName NVARCHAR(256), + @OfferedToEmail NVARCHAR(256), + @PlanSponsorshipType TINYINT, + @ToDelete BIT, + @LastSyncDate DATETIME2 (7), + @ValidUntil DATETIME2 (7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[OrganizationSponsorship] + ( + [Id], + [SponsoringOrganizationId], + [SponsoringOrganizationUserID], + [SponsoredOrganizationId], + [FriendlyName], + [OfferedToEmail], + [PlanSponsorshipType], + [ToDelete], + [LastSyncDate], + [ValidUntil] + ) + VALUES + ( + @Id, + @SponsoringOrganizationId, + @SponsoringOrganizationUserID, + @SponsoredOrganizationId, + @FriendlyName, + @OfferedToEmail, + @PlanSponsorshipType, + @ToDelete, + @LastSyncDate, + @ValidUntil + ) +END +GO + +-- OrganizationSponsorship_Update +IF OBJECT_ID('[dbo].[OrganizationSponsorship_Update]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationSponsorship_Update] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationSponsorship_Update] + @Id UNIQUEIDENTIFIER, + @SponsoringOrganizationId UNIQUEIDENTIFIER, + @SponsoringOrganizationUserID UNIQUEIDENTIFIER, + @SponsoredOrganizationId UNIQUEIDENTIFIER, + @FriendlyName NVARCHAR(256), + @OfferedToEmail NVARCHAR(256), + @PlanSponsorshipType TINYINT, + @ToDelete BIT, + @LastSyncDate DATETIME2 (7), + @ValidUntil DATETIME2 (7) +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[OrganizationSponsorship] + SET + [SponsoringOrganizationId] = @SponsoringOrganizationId, + [SponsoringOrganizationUserID] = @SponsoringOrganizationUserID, + [SponsoredOrganizationId] = @SponsoredOrganizationId, + [FriendlyName] = @FriendlyName, + [OfferedToEmail] = @OfferedToEmail, + [PlanSponsorshipType] = @PlanSponsorshipType, + [ToDelete] = @ToDelete, + [LastSyncDate] = @LastSyncDate, + [ValidUntil] = @ValidUntil + WHERE + [Id] = @Id +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadLatestBySponsoringOrganizationId]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationSponsorship_ReadLatestBySponsoringOrganizationId] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationSponsorship_ReadLatestBySponsoringOrganizationId] + @SponsoringOrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SELECT TOP 1 + [LastSyncDate] + FROM + [dbo].[OrganizationSponsorshipView] + WHERE + [SponsoringOrganizationId] = @SponsoringOrganizationId AND + [LastSyncDate] IS NOT NULL + ORDER BY [LastSyncDate] DESC +END +GO + +IF OBJECT_ID('[dbo].[Organization_DeleteById]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Organization_DeleteById] +END +GO + +CREATE PROCEDURE [dbo].[Organization_DeleteById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @Id + + DECLARE @BatchSize INT = 100 + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION Organization_DeleteById_Ciphers + + DELETE TOP(@BatchSize) + FROM + [dbo].[Cipher] + WHERE + [UserId] IS NULL + AND [OrganizationId] = @Id + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION Organization_DeleteById_Ciphers + END + + BEGIN TRANSACTION Organization_DeleteById + + DELETE + FROM + [dbo].[SsoUser] + WHERE + [OrganizationId] = @Id + + DELETE + FROM + [dbo].[SsoConfig] + WHERE + [OrganizationId] = @Id + + DELETE CU + FROM + [dbo].[CollectionUser] CU + INNER JOIN + [dbo].[OrganizationUser] OU ON [CU].[OrganizationUserId] = [OU].[Id] + WHERE + [OU].[OrganizationId] = @Id + + DELETE + FROM + [dbo].[OrganizationUser] + WHERE + [OrganizationId] = @Id + + DELETE + FROM + [dbo].[ProviderOrganization] + WHERE + [OrganizationId] = @Id + + EXEC [dbo].[OrganizationApiKey_OrganizationDeleted] @Id + EXEC [dbo].[OrganizationConnection_OrganizationDeleted] @Id + + DELETE + FROM + [dbo].[Organization] + WHERE + [Id] = @Id + + COMMIT TRANSACTION Organization_DeleteById +END +GO + +-- OrganizationSponsorship have a different delete process for whether or not server is SH or not +IF OBJECT_ID('[dbo].[OrganizationSponsorship_OrganizationDeleted]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationSponsorship_OrganizationDeleted] +END +GO + + +IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'OrganizationUserOrganizationDetailsView') + BEGIN + DROP VIEW [dbo].[OrganizationUserOrganizationDetailsView] + END +GO + +CREATE VIEW [dbo].[OrganizationUserOrganizationDetailsView] +AS +SELECT + OU.[UserId], + OU.[OrganizationId], + O.[Name], + O.[Enabled], + O.[PlanType], + O.[UsePolicies], + O.[UseSso], + O.[UseKeyConnector], + O.[UseGroups], + O.[UseDirectory], + O.[UseEvents], + O.[UseTotp], + O.[Use2fa], + O.[UseApi], + O.[UseResetPassword], + O.[SelfHost], + O.[UsersGetPremium], + O.[Seats], + O.[MaxCollections], + O.[MaxStorageGb], + O.[Identifier], + OU.[Key], + OU.[ResetPasswordKey], + O.[PublicKey], + O.[PrivateKey], + OU.[Status], + OU.[Type], + SU.[ExternalId] SsoExternalId, + OU.[Permissions], + PO.[ProviderId], + P.[Name] ProviderName, + SS.[Data] SsoConfig, + OS.[FriendlyName] FamilySponsorshipFriendlyName, + OS.[LastSyncDate] FamilySponsorshipLastSyncDate, + OS.[ToDelete] FamilySponsorshipToDelete, + OS.[ValidUntil] FamilySponsorshipValidUntil +FROM + [dbo].[OrganizationUser] OU +LEFT JOIN + [dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] +LEFT JOIN + [dbo].[SsoUser] SU ON SU.[UserId] = OU.[UserId] AND SU.[OrganizationId] = OU.[OrganizationId] +LEFT JOIN + [dbo].[ProviderOrganization] PO ON PO.[OrganizationId] = O.[Id] +LEFT JOIN + [dbo].[Provider] P ON P.[Id] = PO.[ProviderId] +LEFT JOIN + [dbo].[SsoConfig] SS ON SS.[OrganizationId] = OU.[OrganizationId] +LEFT JOIN + [dbo].[OrganizationSponsorship] OS ON OS.[SponsoringOrganizationUserID] = OU.[Id] +GO diff --git a/util/Migrator/DbScripts/2022-04-06_00_SponsorshipBulkActions.sql b/util/Migrator/DbScripts/2022-04-06_00_SponsorshipBulkActions.sql new file mode 100644 index 000000000..e71b8d21d --- /dev/null +++ b/util/Migrator/DbScripts/2022-04-06_00_SponsorshipBulkActions.sql @@ -0,0 +1,181 @@ +-- Create OrganizationSponsorshipType +IF NOT EXISTS ( + SELECT + * + FROM + sys.types + WHERE + [Name] = 'OrganizationSponsorshipType' AND + is_user_defined = 1 +) +BEGIN +CREATE TYPE [dbo].[OrganizationSponsorshipType] AS TABLE( + [Id] UNIQUEIDENTIFIER, + [SponsoringOrganizationId] UNIQUEIDENTIFIER, + [SponsoringOrganizationUserID] UNIQUEIDENTIFIER, + [SponsoredOrganizationId] UNIQUEIDENTIFIER, + [FriendlyName] NVARCHAR(256), + [OfferedToEmail] VARCHAR(256), + [PlanSponsorshipType] TINYINT, + [LastSyncDate] DATETIME2(7), + [ValidUntil] DATETIME2(7), + [ToDelete] BIT +) +END +GO + +-- OrganizationSponsorship_CreateMany +IF OBJECT_ID('[dbo].[OrganizationSponsorship_CreateMany]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationSponsorship_CreateMany] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationSponsorship_CreateMany] + @OrganizationSponsorshipsInput [dbo].[OrganizationSponsorshipType] READONLY +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[OrganizationSponsorship] + ( + [Id], + [SponsoringOrganizationId], + [SponsoringOrganizationUserID], + [SponsoredOrganizationId], + [FriendlyName], + [OfferedToEmail], + [PlanSponsorshipType], + [ToDelete], + [LastSyncDate], + [ValidUntil] + ) + SELECT + OS.[Id], + OS.[SponsoringOrganizationId], + OS.[SponsoringOrganizationUserID], + OS.[SponsoredOrganizationId], + OS.[FriendlyName], + OS.[OfferedToEmail], + OS.[PlanSponsorshipType], + OS.[ToDelete], + OS.[LastSyncDate], + OS.[ValidUntil] + FROM + @OrganizationSponsorshipsInput OS +END +GO + +-- OrganizationSponsorship_UpdateMany +IF OBJECT_ID('[dbo].[OrganizationSponsorship_UpdateMany]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationSponsorship_UpdateMany] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationSponsorship_UpdateMany] + @OrganizationSponsorshipsInput [dbo].[OrganizationSponsorshipType] READONLY +AS +BEGIN + SET NOCOUNT ON + + UPDATE + OS + SET + [Id] = OSI.[Id], + [SponsoringOrganizationId] = OSI.[SponsoringOrganizationId], + [SponsoringOrganizationUserID] = OSI.[SponsoringOrganizationUserID], + [SponsoredOrganizationId] = OSI.[SponsoredOrganizationId], + [FriendlyName] = OSI.[FriendlyName], + [OfferedToEmail] = OSI.[OfferedToEmail], + [PlanSponsorshipType] = OSI.[PlanSponsorshipType], + [ToDelete] = OSI.[ToDelete], + [LastSyncDate] = OSI.[LastSyncDate], + [ValidUntil] = OSI.[ValidUntil] + FROM + [dbo].[OrganizationSponsorship] OS + INNER JOIN + @OrganizationSponsorshipsInput OSI ON OS.Id = OSI.Id +END +GO + +-- OrganizationSponsorship_DeleteByIds +IF OBJECT_ID('[dbo].[OrganizationSponsorship_DeleteByIds]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationSponsorship_DeleteByIds] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationSponsorship_DeleteByIds] + @Ids [dbo].[GuidIdArray] READONLY +AS +BEGIN + SET NOCOUNT ON + + DECLARE @BatchSize INT = 100 + + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION OrgSponsorship_DeleteMany + + DELETE TOP(@BatchSize) OS + FROM + [dbo].[OrganizationSponsorship] OS + INNER JOIN + @Ids I ON I.Id = OS.Id + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION OrgSponsorship_DeleteMany + END +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_DeleteExpired]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationSponsorship_DeleteExpired] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationSponsorship_DeleteExpired] + @ValidUntilBeforeDate DATETIME2 (7) +AS +BEGIN + SET NOCOUNT ON + + DECLARE @BatchSize INT = 100 + + WHILE @BatchSize > 0 + BEGIN + DELETE TOP(@BatchSize) + FROM + [dbo].[OrganizationSponsorship] + WHERE + [ValidUntil] < @ValidUntilBeforeDate + + SET @BatchSize = @@ROWCOUNT + END +END +GO + +-- OrganizationSponsorship_ReadBySponsoringOrganizationId +IF OBJECT_ID('[dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationId]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationId] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationSponsorship_ReadBySponsoringOrganizationId] + @SponsoringOrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationSponsorshipView] + WHERE + [SponsoringOrganizationId] = @SponsoringOrganizationId +END +GO \ No newline at end of file diff --git a/util/Migrator/DbScripts/2022-04-13_00_OrganizationSponsorshipUpdates.sql b/util/Migrator/DbScripts/2022-04-13_00_OrganizationSponsorshipUpdates.sql new file mode 100644 index 000000000..63259e0b0 --- /dev/null +++ b/util/Migrator/DbScripts/2022-04-13_00_OrganizationSponsorshipUpdates.sql @@ -0,0 +1,120 @@ +IF COLUMNPROPERTY(OBJECT_ID('[dbo].[OrganizationSponsorship]', 'U'), 'SponsoringOrganizationId', 'AllowsNull') = 0 +BEGIN + DROP INDEX [IX_OrganizationSponsorship_SponsoringOrganizationId] + ON [dbo].[OrganizationSponsorship] + + ALTER TABLE [dbo].[OrganizationSponsorship] + ALTER COLUMN [SponsoringOrganizationId] UNIQUEIDENTIFIER NULL; + + CREATE NONCLUSTERED INDEX [IX_OrganizationSponsorship_SponsoringOrganizationId] + ON [dbo].[OrganizationSponsorship]([SponsoringOrganizationId] ASC) + WHERE [SponsoringOrganizationId] IS NOT NULL; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorship_OrganizationDeleted') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationSponsorship_OrganizationDeleted] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationSponsorship_OrganizationDeleted] + @OrganizationId UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + UPDATE + [dbo].[OrganizationSponsorship] + SET + [SponsoringOrganizationId] = NULL + WHERE + [SponsoringOrganizationId] = @OrganizationId + + UPDATE + [dbo].[OrganizationSponsorship] + SET + [SponsoredOrganizationId] = NULL + WHERE + [SponsoredOrganizationId] = @OrganizationId +END +GO + +IF OBJECT_ID('[dbo].[Organization_DeleteById]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[Organization_DeleteById] +END +GO + +CREATE PROCEDURE [dbo].[Organization_DeleteById] + @Id UNIQUEIDENTIFIER +AS +BEGIN + SET NOCOUNT ON + + EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @Id + + DECLARE @BatchSize INT = 100 + WHILE @BatchSize > 0 + BEGIN + BEGIN TRANSACTION Organization_DeleteById_Ciphers + + DELETE TOP(@BatchSize) + FROM + [dbo].[Cipher] + WHERE + [UserId] IS NULL + AND [OrganizationId] = @Id + + SET @BatchSize = @@ROWCOUNT + + COMMIT TRANSACTION Organization_DeleteById_Ciphers + END + + BEGIN TRANSACTION Organization_DeleteById + + DELETE + FROM + [dbo].[SsoUser] + WHERE + [OrganizationId] = @Id + + DELETE + FROM + [dbo].[SsoConfig] + WHERE + [OrganizationId] = @Id + + DELETE CU + FROM + [dbo].[CollectionUser] CU + INNER JOIN + [dbo].[OrganizationUser] OU ON [CU].[OrganizationUserId] = [OU].[Id] + WHERE + [OU].[OrganizationId] = @Id + + DELETE + FROM + [dbo].[OrganizationUser] + WHERE + [OrganizationId] = @Id + + DELETE + FROM + [dbo].[ProviderOrganization] + WHERE + [OrganizationId] = @Id + + EXEC [dbo].[OrganizationApiKey_OrganizationDeleted] @Id + EXEC [dbo].[OrganizationConnection_OrganizationDeleted] @Id + EXEC [dbo].[OrganizationSponsorship_OrganizationDeleted] @Id + + DELETE + FROM + [dbo].[Organization] + WHERE + [Id] = @Id + + COMMIT TRANSACTION Organization_DeleteById +END +GO diff --git a/util/Migrator/DbScripts/2022-04-14_00_ReadOrganizationConnectionsByOrganizationId.sql b/util/Migrator/DbScripts/2022-04-14_00_ReadOrganizationConnectionsByOrganizationId.sql new file mode 100644 index 000000000..011e67550 --- /dev/null +++ b/util/Migrator/DbScripts/2022-04-14_00_ReadOrganizationConnectionsByOrganizationId.sql @@ -0,0 +1,28 @@ +IF OBJECT_ID('[dbo].[OrganizationConnection_ReadEnabledByOrganizationIdType]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationConnection_ReadEnabledByOrganizationIdType]; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationConnection_ReadByOrganizationIdType]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationConnection_ReadByOrganizationIdType]; +END +GO + +CREATE PROCEDURE [dbo].[OrganizationConnection_ReadByOrganizationIdType] + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT +AS +BEGIN + SET NOCOUNT ON + + SELECT + * + FROM + [dbo].[OrganizationConnectionView] + WHERE + [OrganizationId] = @OrganizationId AND + [Type] = @Type +END +GO diff --git a/util/Migrator/DbScripts/2022-04-15_00_FixOrganizationConnectionCreate.sql b/util/Migrator/DbScripts/2022-04-15_00_FixOrganizationConnectionCreate.sql new file mode 100644 index 000000000..8ab102668 --- /dev/null +++ b/util/Migrator/DbScripts/2022-04-15_00_FixOrganizationConnectionCreate.sql @@ -0,0 +1,34 @@ +IF OBJECT_ID('[dbo].[OrganizationConnection_Create]') IS NOT NULL +BEGIN + DROP PROCEDURE [dbo].[OrganizationConnection_Create] +END +GO + +CREATE PROCEDURE [dbo].[OrganizationConnection_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @OrganizationId UNIQUEIDENTIFIER, + @Type TINYINT, + @Enabled BIT, + @Config NVARCHAR(MAX) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[OrganizationConnection] + ( + [Id], + [OrganizationId], + [Type], + [Enabled], + [Config] + ) + VALUES + ( + @Id, + @OrganizationId, + @Type, + @Enabled, + @Config + ) +END +GO diff --git a/util/Migrator/DbScripts/2022-04-20_00_AddInstalltionIdToEvents.sql b/util/Migrator/DbScripts/2022-04-20_00_AddInstalltionIdToEvents.sql new file mode 100644 index 000000000..b298a6fd4 --- /dev/null +++ b/util/Migrator/DbScripts/2022-04-20_00_AddInstalltionIdToEvents.sql @@ -0,0 +1,95 @@ +IF COL_LENGTH('[dbo].[Event]', 'InstallationId') IS NULL + BEGIN + ALTER TABLE + [dbo].[Event] + ADD + [InstallationId] UNIQUEIDENTIFIER NULL + END +GO + +-- Sprocs: Create +IF OBJECT_ID('[dbo].[Event_Create]') IS NOT NULL + BEGIN + DROP PROCEDURE [dbo].[Event_Create] + END +GO + +CREATE PROCEDURE [dbo].[Event_Create] + @Id UNIQUEIDENTIFIER OUTPUT, + @Type INT, + @UserId UNIQUEIDENTIFIER, + @OrganizationId UNIQUEIDENTIFIER, + @InstallationId UNIQUEIDENTIFIER, + @ProviderId UNIQUEIDENTIFIER, + @CipherId UNIQUEIDENTIFIER, + @CollectionId UNIQUEIDENTIFIER, + @PolicyId UNIQUEIDENTIFIER, + @GroupId UNIQUEIDENTIFIER, + @OrganizationUserId UNIQUEIDENTIFIER, + @ProviderUserId UNIQUEIDENTIFIER, + @ProviderOrganizationId UNIQUEIDENTIFIER = null, + @ActingUserId UNIQUEIDENTIFIER, + @DeviceType SMALLINT, + @IpAddress VARCHAR(50), + @Date DATETIME2(7) +AS +BEGIN + SET NOCOUNT ON + + INSERT INTO [dbo].[Event] + ( + [Id], + [Type], + [UserId], + [OrganizationId], + [InstallationId], + [ProviderId], + [CipherId], + [CollectionId], + [PolicyId], + [GroupId], + [OrganizationUserId], + [ProviderUserId], + [ProviderOrganizationId], + [ActingUserId], + [DeviceType], + [IpAddress], + [Date] + ) + VALUES + ( + @Id, + @Type, + @UserId, + @OrganizationId, + @InstallationId, + @ProviderId, + @CipherId, + @CollectionId, + @PolicyId, + @GroupId, + @OrganizationUserId, + @ProviderUserId, + @ProviderOrganizationId, + @ActingUserId, + @DeviceType, + @IpAddress, + @Date + ) +END +GO + +-- View: Event +IF EXISTS(SELECT * FROM sys.views WHERE [Name] = 'EventView') +BEGIN + DROP VIEW [dbo].[EventView] +END +GO + +CREATE VIEW [dbo].[EventView] +AS +SELECT + * +FROM + [dbo].[Event] +GO diff --git a/util/Migrator/DbScripts/2022-05-10_00_RebuildMetadata.sql b/util/Migrator/DbScripts/2022-05-10_00_RebuildMetadata.sql new file mode 100644 index 000000000..9db31d518 --- /dev/null +++ b/util/Migrator/DbScripts/2022-05-10_00_RebuildMetadata.sql @@ -0,0 +1,23 @@ +IF EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_OrganizationApiKey_ApiKey') +BEGIN + DROP INDEX [IX_OrganizationApiKey_ApiKey] ON [dbo].[OrganizationApiKey] +END +GO + +IF OBJECT_ID('[dbo].[OrganizationView]') IS NOT NULL +BEGIN + EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationView]'; +END +GO + +IF OBJECT_ID('[dbo].[OrganizationSponsorshipView]') IS NOT NULL +BEGIN + EXECUTE sp_refreshsqlmodule N'[dbo].[OrganizationSponsorshipView]'; +END +GO + +IF OBJECT_ID('[dbo].[EventView]') IS NOT NULL +BEGIN + EXECUTE sp_refreshsqlmodule N'[dbo].[EventView]'; +END +GO diff --git a/util/MySqlMigrations/HelperScripts/2022-03-01_00_Down_MigrateOrganizationApiKeys.sql b/util/MySqlMigrations/HelperScripts/2022-03-01_00_Down_MigrateOrganizationApiKeys.sql new file mode 100644 index 000000000..ac84bed35 --- /dev/null +++ b/util/MySqlMigrations/HelperScripts/2022-03-01_00_Down_MigrateOrganizationApiKeys.sql @@ -0,0 +1,3 @@ +UPDATE Organization +INNER JOIN OrganizationApiKey ON (Organization.Id = OrganizationApiKey.OrganizationId) +SET Organization.ApiKey = OrganizationApiKey.ApiKey; diff --git a/util/MySqlMigrations/HelperScripts/2022-03-01_00_Up_MigrateOrganizationApiKeys.sql b/util/MySqlMigrations/HelperScripts/2022-03-01_00_Up_MigrateOrganizationApiKeys.sql new file mode 100644 index 000000000..926121529 --- /dev/null +++ b/util/MySqlMigrations/HelperScripts/2022-03-01_00_Up_MigrateOrganizationApiKeys.sql @@ -0,0 +1,3 @@ +INSERT INTO OrganizationApiKey(Id, OrganizationId, Type, ApiKey, RevisionDate) +SELECT UUID(), Id, 0, ApiKey, RevisionDate +FROM Organization; diff --git a/util/MySqlMigrations/Migrations/20220322191314_SelfHostF4E.Designer.cs b/util/MySqlMigrations/Migrations/20220322191314_SelfHostF4E.Designer.cs new file mode 100644 index 000000000..196574154 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20220322191314_SelfHostF4E.Designer.cs @@ -0,0 +1,1579 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Bit.MySqlMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20220322191314_SelfHostF4E")] + partial class SelfHostF4E + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 64) + .HasAnnotation("ProductVersion", "5.0.12"); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Attachments") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Favorites") + .HasColumnType("longtext"); + + b.Property("Folders") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Reprompt") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.HasIndex("UserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("GranteeId") + .HasColumnType("char(36)"); + + b.Property("GrantorId") + .HasColumnType("char(36)"); + + b.Property("KeyEncrypted") + .HasColumnType("longtext"); + + b.Property("LastNotificationDate") + .HasColumnType("datetime(6)"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("WaitTimeDays") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ActingUserId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("DeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("PolicyId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("ProviderOrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderUserId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("Event"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Grant", b => + { + b.Property("Key") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ClientId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ConsumedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Key"); + + b.ToTable("Grant"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessAll") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.HasIndex("UserId"); + + b.ToTable("GroupUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.HasKey("Id"); + + b.ToTable("Installation"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property("Plan") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("SelfHost") + .HasColumnType("tinyint(1)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property("UseSso") + .HasColumnType("tinyint(1)"); + + b.Property("UseTotp") + .HasColumnType("tinyint(1)"); + + b.Property("UsersGetPremium") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.ToTable("OrganizationSponsorship"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessAll") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Policy"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Provider", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasColumnType("longtext"); + + b.Property("BusinessAddress1") + .HasColumnType("longtext"); + + b.Property("BusinessAddress2") + .HasColumnType("longtext"); + + b.Property("BusinessAddress3") + .HasColumnType("longtext"); + + b.Property("BusinessCountry") + .HasColumnType("longtext"); + + b.Property("BusinessName") + .HasColumnType("longtext"); + + b.Property("BusinessTaxNumber") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Settings") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletionDate") + .HasColumnType("datetime(6)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("HideEmail") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MaxAccessCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Send"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("SsoUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Rate") + .HasColumnType("decimal(65,30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PaymentMethodType") + .HasColumnType("tinyint unsigned"); + + b.Property("Refunded") + .HasColumnType("tinyint(1)"); + + b.Property("RefundedAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Transaction"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccountRevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailVerified") + .HasColumnType("tinyint(1)"); + + b.Property("EquivalentDomains") + .HasColumnType("longtext"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("longtext"); + + b.Property("ForcePasswordReset") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Kdf") + .HasColumnType("tinyint unsigned"); + + b.Property("KdfIterations") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Premium") + .HasColumnType("tinyint(1)"); + + b.Property("PremiumExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RenewalReminderDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", null) + .WithMany("CollectionUsers") + .HasForeignKey("UserId"); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", null) + .WithMany("GroupUsers") + .HasForeignKey("UserId"); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Connections"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("CollectionUsers"); + + b.Navigation("Folders"); + + b.Navigation("GroupUsers"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/MySqlMigrations/Migrations/20220322191314_SelfHostF4E.cs b/util/MySqlMigrations/Migrations/20220322191314_SelfHostF4E.cs new file mode 100644 index 000000000..b3d12c752 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20220322191314_SelfHostF4E.cs @@ -0,0 +1,159 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Bit.MySqlMigrations.Migrations +{ + public partial class SelfHostF4E : Migration + { + private const string _scriptLocationTemplate = "2022-03-01_00_{0}_MigrateOrganizationApiKeys.sql"; + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_OrganizationSponsorship_Installation_InstallationId", + table: "OrganizationSponsorship"); + + migrationBuilder.DropIndex( + name: "IX_OrganizationSponsorship_InstallationId", + table: "OrganizationSponsorship"); + + migrationBuilder.DropColumn( + name: "InstallationId", + table: "OrganizationSponsorship"); + + migrationBuilder.DropColumn( + name: "TimesRenewedWithoutValidation", + table: "OrganizationSponsorship"); + + migrationBuilder.CreateTable( + name: "OrganizationApiKey", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + OrganizationId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Type = table.Column(type: "tinyint unsigned", nullable: false), + ApiKey = table.Column(type: "varchar(30)", maxLength: 30, nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + RevisionDate = table.Column(type: "datetime(6)", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationApiKey", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationApiKey_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.SqlResource(_scriptLocationTemplate); + + migrationBuilder.DropColumn( + name: "ApiKey", + table: "Organization"); + + migrationBuilder.RenameColumn( + name: "SponsorshipLapsedDate", + table: "OrganizationSponsorship", + newName: "ValidUntil"); + + migrationBuilder.RenameColumn( + name: "CloudSponsor", + table: "OrganizationSponsorship", + newName: "ToDelete"); + + + + migrationBuilder.CreateTable( + name: "OrganizationConnection", + columns: table => new + { + Id = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Type = table.Column(type: "tinyint unsigned", nullable: false), + OrganizationId = table.Column(type: "char(36)", nullable: false, collation: "ascii_general_ci"), + Enabled = table.Column(type: "tinyint(1)", nullable: false), + Config = table.Column(type: "longtext", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4") + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationConnection", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationConnection_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationApiKey_OrganizationId", + table: "OrganizationApiKey", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationConnection_OrganizationId", + table: "OrganizationConnection", + column: "OrganizationId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ApiKey", + table: "Organization", + type: "varchar(30)", + maxLength: 30, + nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"); + + migrationBuilder.SqlResource(_scriptLocationTemplate); + + migrationBuilder.DropTable( + name: "OrganizationApiKey"); + + migrationBuilder.DropTable( + name: "OrganizationConnection"); + + migrationBuilder.RenameColumn( + name: "ValidUntil", + table: "OrganizationSponsorship", + newName: "SponsorshipLapsedDate"); + + migrationBuilder.RenameColumn( + name: "ToDelete", + table: "OrganizationSponsorship", + newName: "CloudSponsor"); + + migrationBuilder.AddColumn( + name: "InstallationId", + table: "OrganizationSponsorship", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci"); + + migrationBuilder.AddColumn( + name: "TimesRenewedWithoutValidation", + table: "OrganizationSponsorship", + type: "tinyint unsigned", + nullable: false, + defaultValue: (byte)0); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationSponsorship_InstallationId", + table: "OrganizationSponsorship", + column: "InstallationId"); + + migrationBuilder.AddForeignKey( + name: "FK_OrganizationSponsorship_Installation_InstallationId", + table: "OrganizationSponsorship", + column: "InstallationId", + principalTable: "Installation", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + } +} diff --git a/util/MySqlMigrations/Migrations/20220411191518_SponsorshipBulkActions.Designer.cs b/util/MySqlMigrations/Migrations/20220411191518_SponsorshipBulkActions.Designer.cs new file mode 100644 index 000000000..c5b24c6ba --- /dev/null +++ b/util/MySqlMigrations/Migrations/20220411191518_SponsorshipBulkActions.Designer.cs @@ -0,0 +1,1587 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Bit.MySqlMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20220411191518_SponsorshipBulkActions")] + partial class SponsorshipBulkActions + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 64) + .HasAnnotation("ProductVersion", "5.0.12"); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Attachments") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Favorites") + .HasColumnType("longtext"); + + b.Property("Folders") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Reprompt") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.HasIndex("UserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("GranteeId") + .HasColumnType("char(36)"); + + b.Property("GrantorId") + .HasColumnType("char(36)"); + + b.Property("KeyEncrypted") + .HasColumnType("longtext"); + + b.Property("LastNotificationDate") + .HasColumnType("datetime(6)"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("WaitTimeDays") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ActingUserId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("DeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("PolicyId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("ProviderOrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderUserId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("Event"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Grant", b => + { + b.Property("Key") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ClientId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ConsumedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Key"); + + b.ToTable("Grant"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessAll") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.HasIndex("UserId"); + + b.ToTable("GroupUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.HasKey("Id"); + + b.ToTable("Installation"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property("Plan") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("SelfHost") + .HasColumnType("tinyint(1)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property("UseSso") + .HasColumnType("tinyint(1)"); + + b.Property("UseTotp") + .HasColumnType("tinyint(1)"); + + b.Property("UsersGetPremium") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.ToTable("OrganizationSponsorship"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessAll") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Policy"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Provider", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasColumnType("longtext"); + + b.Property("BusinessAddress1") + .HasColumnType("longtext"); + + b.Property("BusinessAddress2") + .HasColumnType("longtext"); + + b.Property("BusinessAddress3") + .HasColumnType("longtext"); + + b.Property("BusinessCountry") + .HasColumnType("longtext"); + + b.Property("BusinessName") + .HasColumnType("longtext"); + + b.Property("BusinessTaxNumber") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Settings") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletionDate") + .HasColumnType("datetime(6)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("HideEmail") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MaxAccessCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Send"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("SsoUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Rate") + .HasColumnType("decimal(65,30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PaymentMethodType") + .HasColumnType("tinyint unsigned"); + + b.Property("Refunded") + .HasColumnType("tinyint(1)"); + + b.Property("RefundedAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Transaction"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccountRevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailVerified") + .HasColumnType("tinyint(1)"); + + b.Property("EquivalentDomains") + .HasColumnType("longtext"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("longtext"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("ForcePasswordReset") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Kdf") + .HasColumnType("tinyint unsigned"); + + b.Property("KdfIterations") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LastFailedLoginDate") + .HasColumnType("datetime(6)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Premium") + .HasColumnType("tinyint(1)"); + + b.Property("PremiumExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RenewalReminderDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", null) + .WithMany("CollectionUsers") + .HasForeignKey("UserId"); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", null) + .WithMany("GroupUsers") + .HasForeignKey("UserId"); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Connections"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("CollectionUsers"); + + b.Navigation("Folders"); + + b.Navigation("GroupUsers"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/MySqlMigrations/Migrations/20220411191518_SponsorshipBulkActions.cs b/util/MySqlMigrations/Migrations/20220411191518_SponsorshipBulkActions.cs new file mode 100644 index 000000000..b1b41f4a5 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20220411191518_SponsorshipBulkActions.cs @@ -0,0 +1,82 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Bit.MySqlMigrations.Migrations +{ + public partial class SponsorshipBulkActions : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_OrganizationSponsorship_Organization_SponsoringOrganizationId", + table: "OrganizationSponsorship"); + + migrationBuilder.AlterColumn( + name: "SponsoringOrganizationUserId", + table: "OrganizationSponsorship", + type: "char(36)", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + collation: "ascii_general_ci", + oldClrType: typeof(Guid), + oldType: "char(36)", + oldNullable: true) + .OldAnnotation("Relational:Collation", "ascii_general_ci"); + + migrationBuilder.AlterColumn( + name: "SponsoringOrganizationId", + table: "OrganizationSponsorship", + type: "char(36)", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + collation: "ascii_general_ci", + oldClrType: typeof(Guid), + oldType: "char(36)", + oldNullable: true) + .OldAnnotation("Relational:Collation", "ascii_general_ci"); + + migrationBuilder.AddForeignKey( + name: "FK_OrganizationSponsorship_Organization_SponsoringOrganizationId", + table: "OrganizationSponsorship", + column: "SponsoringOrganizationId", + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_OrganizationSponsorship_Organization_SponsoringOrganizationId", + table: "OrganizationSponsorship"); + + migrationBuilder.AlterColumn( + name: "SponsoringOrganizationUserId", + table: "OrganizationSponsorship", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci", + oldClrType: typeof(Guid), + oldType: "char(36)") + .OldAnnotation("Relational:Collation", "ascii_general_ci"); + + migrationBuilder.AlterColumn( + name: "SponsoringOrganizationId", + table: "OrganizationSponsorship", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci", + oldClrType: typeof(Guid), + oldType: "char(36)") + .OldAnnotation("Relational:Collation", "ascii_general_ci"); + + migrationBuilder.AddForeignKey( + name: "FK_OrganizationSponsorship_Organization_SponsoringOrganizationId", + table: "OrganizationSponsorship", + column: "SponsoringOrganizationId", + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + } +} diff --git a/util/MySqlMigrations/Migrations/20220420170738_AddInstallationIdToEvents.Designer.cs b/util/MySqlMigrations/Migrations/20220420170738_AddInstallationIdToEvents.Designer.cs new file mode 100644 index 000000000..f25ccd264 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20220420170738_AddInstallationIdToEvents.Designer.cs @@ -0,0 +1,1588 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace Bit.MySqlMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20220420170738_AddInstallationIdToEvents")] + partial class AddInstallationIdToEvents + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Relational:MaxIdentifierLength", 64) + .HasAnnotation("ProductVersion", "5.0.12"); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Attachments") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletedDate") + .HasColumnType("datetime(6)"); + + b.Property("Favorites") + .HasColumnType("longtext"); + + b.Property("Folders") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Reprompt") + .HasColumnType("tinyint unsigned"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("HidePasswords") + .HasColumnType("tinyint(1)"); + + b.Property("ReadOnly") + .HasColumnType("tinyint(1)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.HasIndex("UserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("varchar(255)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("GranteeId") + .HasColumnType("char(36)"); + + b.Property("GrantorId") + .HasColumnType("char(36)"); + + b.Property("KeyEncrypted") + .HasColumnType("longtext"); + + b.Property("LastNotificationDate") + .HasColumnType("datetime(6)"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("WaitTimeDays") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ActingUserId") + .HasColumnType("char(36)"); + + b.Property("CipherId") + .HasColumnType("char(36)"); + + b.Property("CollectionId") + .HasColumnType("char(36)"); + + b.Property("Date") + .HasColumnType("datetime(6)"); + + b.Property("DeviceType") + .HasColumnType("tinyint unsigned"); + + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("InstallationId") + .HasColumnType("char(36)"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("PolicyId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("ProviderOrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderUserId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.ToTable("Event"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Grant", b => + { + b.Property("Key") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ClientId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ConsumedDate") + .HasColumnType("datetime(6)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("varchar(200)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.HasKey("Key"); + + b.ToTable("Grant"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessAll") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("char(36)"); + + b.Property("OrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.HasIndex("UserId"); + + b.ToTable("GroupUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Installation", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasMaxLength(150) + .HasColumnType("varchar(150)"); + + b.HasKey("Id"); + + b.ToTable("Installation"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("int"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("datetime(6)"); + + b.Property("Plan") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PlanType") + .HasColumnType("tinyint unsigned"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Seats") + .HasColumnType("int"); + + b.Property("SelfHost") + .HasColumnType("tinyint(1)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("Use2fa") + .HasColumnType("tinyint(1)"); + + b.Property("UseApi") + .HasColumnType("tinyint(1)"); + + b.Property("UseDirectory") + .HasColumnType("tinyint(1)"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.Property("UseGroups") + .HasColumnType("tinyint(1)"); + + b.Property("UseKeyConnector") + .HasColumnType("tinyint(1)"); + + b.Property("UsePolicies") + .HasColumnType("tinyint(1)"); + + b.Property("UseResetPassword") + .HasColumnType("tinyint(1)"); + + b.Property("UseSso") + .HasColumnType("tinyint(1)"); + + b.Property("UseTotp") + .HasColumnType("tinyint(1)"); + + b.Property("UsersGetPremium") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("LastSyncDate") + .HasColumnType("datetime(6)"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("tinyint unsigned"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("char(36)"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("char(36)"); + + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.ToTable("OrganizationSponsorship"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessAll") + .HasColumnType("tinyint(1)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ResetPasswordKey") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Policy"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Provider", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("BillingEmail") + .HasColumnType("longtext"); + + b.Property("BusinessAddress1") + .HasColumnType("longtext"); + + b.Property("BusinessAddress2") + .HasColumnType("longtext"); + + b.Property("BusinessAddress3") + .HasColumnType("longtext"); + + b.Property("BusinessCountry") + .HasColumnType("longtext"); + + b.Property("BusinessName") + .HasColumnType("longtext"); + + b.Property("BusinessTaxNumber") + .HasColumnType("longtext"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("Name") + .HasColumnType("longtext"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("UseEvents") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Settings") + .HasColumnType("longtext"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Email") + .HasColumnType("longtext"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("Permissions") + .HasColumnType("longtext"); + + b.Property("ProviderId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Status") + .HasColumnType("tinyint unsigned"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("DeletionDate") + .HasColumnType("datetime(6)"); + + b.Property("Disabled") + .HasColumnType("tinyint(1)"); + + b.Property("ExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("HideEmail") + .HasColumnType("tinyint(1)"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("MaxAccessCount") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Send"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Data") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("SsoUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("varchar(40)"); + + b.Property("Active") + .HasColumnType("tinyint(1)"); + + b.Property("Country") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("PostalCode") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Rate") + .HasColumnType("decimal(65,30)"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("varchar(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Amount") + .HasColumnType("decimal(65,30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("PaymentMethodType") + .HasColumnType("tinyint unsigned"); + + b.Property("Refunded") + .HasColumnType("tinyint(1)"); + + b.Property("RefundedAmount") + .HasColumnType("decimal(65,30)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.Property("UserId") + .HasColumnType("char(36)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Transaction"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("AccountRevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("CreationDate") + .HasColumnType("datetime(6)"); + + b.Property("Culture") + .HasMaxLength(10) + .HasColumnType("varchar(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("varchar(256)"); + + b.Property("EmailVerified") + .HasColumnType("tinyint(1)"); + + b.Property("EquivalentDomains") + .HasColumnType("longtext"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("longtext"); + + b.Property("FailedLoginCount") + .HasColumnType("int"); + + b.Property("ForcePasswordReset") + .HasColumnType("tinyint(1)"); + + b.Property("Gateway") + .HasColumnType("tinyint unsigned"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Kdf") + .HasColumnType("tinyint unsigned"); + + b.Property("KdfIterations") + .HasColumnType("int"); + + b.Property("Key") + .HasColumnType("longtext"); + + b.Property("LastFailedLoginDate") + .HasColumnType("datetime(6)"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("varchar(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("varchar(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Premium") + .HasColumnType("tinyint(1)"); + + b.Property("PremiumExpirationDate") + .HasColumnType("datetime(6)"); + + b.Property("PrivateKey") + .HasColumnType("longtext"); + + b.Property("PublicKey") + .HasColumnType("longtext"); + + b.Property("ReferenceData") + .HasColumnType("longtext"); + + b.Property("RenewalReminderDate") + .HasColumnType("datetime(6)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("varchar(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("longtext"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("varchar(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("tinyint(1)"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", null) + .WithMany("CollectionUsers") + .HasForeignKey("UserId"); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", null) + .WithMany("GroupUsers") + .HasForeignKey("UserId"); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Connections"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("CollectionUsers"); + + b.Navigation("Folders"); + + b.Navigation("GroupUsers"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/MySqlMigrations/Migrations/20220420170738_AddInstallationIdToEvents.cs b/util/MySqlMigrations/Migrations/20220420170738_AddInstallationIdToEvents.cs new file mode 100644 index 000000000..650a919e8 --- /dev/null +++ b/util/MySqlMigrations/Migrations/20220420170738_AddInstallationIdToEvents.cs @@ -0,0 +1,71 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Bit.MySqlMigrations.Migrations +{ + public partial class AddInstallationIdToEvents : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_OrganizationSponsorship_Organization_SponsoringOrganizationId", + table: "OrganizationSponsorship"); + + migrationBuilder.AlterColumn( + name: "SponsoringOrganizationId", + table: "OrganizationSponsorship", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci", + oldClrType: typeof(Guid), + oldType: "char(36)") + .OldAnnotation("Relational:Collation", "ascii_general_ci"); + + migrationBuilder.AddColumn( + name: "InstallationId", + table: "Event", + type: "char(36)", + nullable: true, + collation: "ascii_general_ci"); + + migrationBuilder.AddForeignKey( + name: "FK_OrganizationSponsorship_Organization_SponsoringOrganizationId", + table: "OrganizationSponsorship", + column: "SponsoringOrganizationId", + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_OrganizationSponsorship_Organization_SponsoringOrganizationId", + table: "OrganizationSponsorship"); + + migrationBuilder.DropColumn( + name: "InstallationId", + table: "Event"); + + migrationBuilder.AlterColumn( + name: "SponsoringOrganizationId", + table: "OrganizationSponsorship", + type: "char(36)", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + collation: "ascii_general_ci", + oldClrType: typeof(Guid), + oldType: "char(36)", + oldNullable: true) + .OldAnnotation("Relational:Collation", "ascii_general_ci"); + + migrationBuilder.AddForeignKey( + name: "FK_OrganizationSponsorship_Organization_SponsoringOrganizationId", + table: "OrganizationSponsorship", + column: "SponsoringOrganizationId", + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs index cda7e5d93..316229042 100644 --- a/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/MySqlMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -262,6 +262,9 @@ namespace Bit.MySqlMigrations.Migrations b.Property("GroupId") .HasColumnType("char(36)"); + b.Property("InstallationId") + .HasColumnType("char(36)"); + b.Property("IpAddress") .HasMaxLength(50) .HasColumnType("varchar(50)"); @@ -443,10 +446,6 @@ namespace Bit.MySqlMigrations.Migrations b.Property("Id") .HasColumnType("char(36)"); - b.Property("ApiKey") - .HasMaxLength(30) - .HasColumnType("varchar(30)"); - b.Property("BillingEmail") .HasMaxLength(256) .HasColumnType("varchar(256)"); @@ -588,21 +587,64 @@ namespace Bit.MySqlMigrations.Migrations b.ToTable("Organization"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("ApiKey") + .HasMaxLength(30) + .HasColumnType("varchar(30)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("RevisionDate") + .HasColumnType("datetime(6)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("char(36)"); + + b.Property("Config") + .HasColumnType("longtext"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)"); + + b.Property("OrganizationId") + .HasColumnType("char(36)"); + + b.Property("Type") + .HasColumnType("tinyint unsigned"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => { b.Property("Id") .HasColumnType("char(36)"); - b.Property("CloudSponsor") - .HasColumnType("tinyint(1)"); - b.Property("FriendlyName") .HasMaxLength(256) .HasColumnType("varchar(256)"); - b.Property("InstallationId") - .HasColumnType("char(36)"); - b.Property("LastSyncDate") .HasColumnType("datetime(6)"); @@ -619,19 +661,17 @@ namespace Bit.MySqlMigrations.Migrations b.Property("SponsoringOrganizationId") .HasColumnType("char(36)"); - b.Property("SponsoringOrganizationUserId") + b.Property("SponsoringOrganizationUserId") .HasColumnType("char(36)"); - b.Property("SponsorshipLapsedDate") + b.Property("ToDelete") + .HasColumnType("tinyint(1)"); + + b.Property("ValidUntil") .HasColumnType("datetime(6)"); - b.Property("TimesRenewedWithoutValidation") - .HasColumnType("tinyint unsigned"); - b.HasKey("Id"); - b.HasIndex("InstallationId"); - b.HasIndex("SponsoredOrganizationId"); b.HasIndex("SponsoringOrganizationId"); @@ -1320,12 +1360,30 @@ namespace Bit.MySqlMigrations.Migrations b.Navigation("OrganizationUser"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => { - b.HasOne("Bit.Infrastructure.EntityFramework.Models.Installation", "Installation") - .WithMany() - .HasForeignKey("InstallationId"); - b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "SponsoredOrganization") .WithMany() .HasForeignKey("SponsoredOrganizationId"); @@ -1334,8 +1392,6 @@ namespace Bit.MySqlMigrations.Migrations .WithMany() .HasForeignKey("SponsoringOrganizationId"); - b.Navigation("Installation"); - b.Navigation("SponsoredOrganization"); b.Navigation("SponsoringOrganization"); @@ -1484,8 +1540,12 @@ namespace Bit.MySqlMigrations.Migrations modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Organization", b => { + b.Navigation("ApiKeys"); + b.Navigation("Ciphers"); + b.Navigation("Connections"); + b.Navigation("Groups"); b.Navigation("OrganizationUsers"); diff --git a/util/MySqlMigrations/MySqlMigrations.csproj b/util/MySqlMigrations/MySqlMigrations.csproj index c05e86d6d..5a7c876e1 100644 --- a/util/MySqlMigrations/MySqlMigrations.csproj +++ b/util/MySqlMigrations/MySqlMigrations.csproj @@ -17,6 +17,10 @@ + + + + @@ -24,5 +28,7 @@ + + diff --git a/util/MySqlMigrations/Scripts/2022-03-25_00_SelfHostF4E.sql b/util/MySqlMigrations/Scripts/2022-03-25_00_SelfHostF4E.sql new file mode 100644 index 000000000..47b44a822 --- /dev/null +++ b/util/MySqlMigrations/Scripts/2022-03-25_00_SelfHostF4E.sql @@ -0,0 +1,50 @@ +START TRANSACTION; + +ALTER TABLE `OrganizationSponsorship` DROP FOREIGN KEY `FK_OrganizationSponsorship_Installation_InstallationId`; + +ALTER TABLE `OrganizationSponsorship` DROP INDEX `IX_OrganizationSponsorship_InstallationId`; + +ALTER TABLE `OrganizationSponsorship` DROP COLUMN `InstallationId`; + +ALTER TABLE `OrganizationSponsorship` DROP COLUMN `TimesRenewedWithoutValidation`; + +CREATE TABLE `OrganizationApiKey` ( + `Id` char(36) COLLATE ascii_general_ci NOT NULL, + `OrganizationId` char(36) COLLATE ascii_general_ci NOT NULL, + `Type` tinyint unsigned NOT NULL, + `ApiKey` varchar(30) CHARACTER SET utf8mb4 NULL, + `RevisionDate` datetime(6) NOT NULL, + CONSTRAINT `PK_OrganizationApiKey` PRIMARY KEY (`Id`), + CONSTRAINT `FK_OrganizationApiKey_Organization_OrganizationId` FOREIGN KEY (`OrganizationId`) REFERENCES `Organization` (`Id`) ON DELETE CASCADE +) CHARACTER SET utf8mb4; + +INSERT INTO OrganizationApiKey(Id, OrganizationId, Type, ApiKey, RevisionDate) +SELECT UUID(), Id, 0, ApiKey, RevisionDate +FROM Organization; + + +ALTER TABLE `Organization` DROP COLUMN `ApiKey`; + +ALTER TABLE `OrganizationSponsorship` RENAME COLUMN `SponsorshipLapsedDate` TO `ValidUntil`; + +ALTER TABLE `OrganizationSponsorship` RENAME COLUMN `CloudSponsor` TO `ToDelete`; + +CREATE TABLE `OrganizationConnection` ( + `Id` char(36) COLLATE ascii_general_ci NOT NULL, + `Type` tinyint unsigned NOT NULL, + `OrganizationId` char(36) COLLATE ascii_general_ci NOT NULL, + `Enabled` tinyint(1) NOT NULL, + `Config` longtext CHARACTER SET utf8mb4 NULL, + CONSTRAINT `PK_OrganizationConnection` PRIMARY KEY (`Id`), + CONSTRAINT `FK_OrganizationConnection_Organization_OrganizationId` FOREIGN KEY (`OrganizationId`) REFERENCES `Organization` (`Id`) ON DELETE CASCADE +) CHARACTER SET utf8mb4; + +CREATE INDEX `IX_OrganizationApiKey_OrganizationId` ON `OrganizationApiKey` (`OrganizationId`); + +CREATE INDEX `IX_OrganizationConnection_OrganizationId` ON `OrganizationConnection` (`OrganizationId`); + +INSERT INTO `__EFMigrationsHistory` (`MigrationId`, `ProductVersion`) +VALUES ('20220322191314_SelfHostF4E', '5.0.12'); + +COMMIT; + diff --git a/util/MySqlMigrations/Scripts/2022-04-11_00_SelfHostF4EModelChanges.sql b/util/MySqlMigrations/Scripts/2022-04-11_00_SelfHostF4EModelChanges.sql new file mode 100644 index 000000000..b1e4a031c --- /dev/null +++ b/util/MySqlMigrations/Scripts/2022-04-11_00_SelfHostF4EModelChanges.sql @@ -0,0 +1,14 @@ +START TRANSACTION; + +ALTER TABLE `OrganizationSponsorship` DROP FOREIGN KEY `FK_OrganizationSponsorship_Organization_SponsoringOrganizationId`; + +ALTER TABLE `OrganizationSponsorship` MODIFY COLUMN `SponsoringOrganizationUserId` char(36) COLLATE ascii_general_ci NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000'; + +ALTER TABLE `OrganizationSponsorship` MODIFY COLUMN `SponsoringOrganizationId` char(36) COLLATE ascii_general_ci NOT NULL DEFAULT '00000000-0000-0000-0000-000000000000'; + +ALTER TABLE `OrganizationSponsorship` ADD CONSTRAINT `FK_OrganizationSponsorship_Organization_SponsoringOrganizationId` FOREIGN KEY (`SponsoringOrganizationId`) REFERENCES `Organization` (`Id`) ON DELETE CASCADE; + +INSERT INTO `__EFMigrationsHistory` (`MigrationId`, `ProductVersion`) +VALUES ('20220411191518_SponsorshipBulkActions', '5.0.12'); + +COMMIT; \ No newline at end of file diff --git a/util/MySqlMigrations/Scripts/2022-04-20_00_AddInstallationIdToEvents.sql b/util/MySqlMigrations/Scripts/2022-04-20_00_AddInstallationIdToEvents.sql new file mode 100644 index 000000000..67e4820d9 --- /dev/null +++ b/util/MySqlMigrations/Scripts/2022-04-20_00_AddInstallationIdToEvents.sql @@ -0,0 +1,14 @@ +START TRANSACTION; + +ALTER TABLE `OrganizationSponsorship` DROP FOREIGN KEY `FK_OrganizationSponsorship_Organization_SponsoringOrganizationId`; + +ALTER TABLE `OrganizationSponsorship` MODIFY COLUMN `SponsoringOrganizationId` char(36) COLLATE ascii_general_ci NULL; + +ALTER TABLE `Event` ADD `InstallationId` char(36) COLLATE ascii_general_ci NULL; + +ALTER TABLE `OrganizationSponsorship` ADD CONSTRAINT `FK_OrganizationSponsorship_Organization_SponsoringOrganizationId` FOREIGN KEY (`SponsoringOrganizationId`) REFERENCES `Organization` (`Id`) ON DELETE RESTRICT; + +INSERT INTO `__EFMigrationsHistory` (`MigrationId`, `ProductVersion`) +VALUES ('20220420170738_AddInstallationIdToEvents', '5.0.12'); + +COMMIT; diff --git a/util/PostgresMigrations/HelperScripts/2022-03-01_00_Down_MigrateOrganizationApiKeys.psql b/util/PostgresMigrations/HelperScripts/2022-03-01_00_Down_MigrateOrganizationApiKeys.psql new file mode 100644 index 000000000..a34a9d798 --- /dev/null +++ b/util/PostgresMigrations/HelperScripts/2022-03-01_00_Down_MigrateOrganizationApiKeys.psql @@ -0,0 +1,4 @@ +UPDATE "Organization" AS o +SET "ApiKey" = oa."ApiKey" +FROM "OrganizationApiKey" AS oa +WHERE oa."OrganizationId" = o."Id" AND oa."Type" = 0; diff --git a/util/PostgresMigrations/HelperScripts/2022-03-01_00_Up_MigrateOrganizationApiKeys.psql b/util/PostgresMigrations/HelperScripts/2022-03-01_00_Up_MigrateOrganizationApiKeys.psql new file mode 100644 index 000000000..748a9ef07 --- /dev/null +++ b/util/PostgresMigrations/HelperScripts/2022-03-01_00_Up_MigrateOrganizationApiKeys.psql @@ -0,0 +1,12 @@ +INSERT INTO "OrganizationApiKey"( + "Id", + "OrganizationId", + "ApiKey", + "Type", + "RevisionDate") +SELECT gen_random_uuid(), + "Id" AS "OrganizationId", + "ApiKey", + 0 AS "Type", + "RevisionDate" +FROM "Organization"; diff --git a/util/PostgresMigrations/Migrations/20220322183505_SelfHostF4E.Designer.cs b/util/PostgresMigrations/Migrations/20220322183505_SelfHostF4E.Designer.cs new file mode 100644 index 000000000..04c60cd18 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20220322183505_SelfHostF4E.Designer.cs @@ -0,0 +1,1587 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Bit.PostgresMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20220322183505_SelfHostF4E")] + partial class SelfHostF4E + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:postgresIndetermanisticCollation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.12") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.HasIndex("UserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GranteeId") + .HasColumnType("uuid"); + + b.Property("GrantorId") + .HasColumnType("uuid"); + + b.Property("KeyEncrypted") + .HasColumnType("text"); + + b.Property("LastNotificationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("WaitTimeDays") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ActingUserId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp without time zone"); + + b.Property("DeviceType") + .HasColumnType("smallint"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("PolicyId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("ProviderOrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderUserId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Event"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Grant", b => + { + b.Property("Key") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ClientId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ConsumedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Key"); + + b.ToTable("Grant"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessAll") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.HasIndex("UserId"); + + b.ToTable("GroupUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.HasKey("Id"); + + b.ToTable("Installation"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp without time zone"); + + b.Property("Plan") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("SelfHost") + .HasColumnType("boolean"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .HasColumnType("boolean"); + + b.Property("UseSso") + .HasColumnType("boolean"); + + b.Property("UseTotp") + .HasColumnType("boolean"); + + b.Property("UsersGetPremium") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp without time zone"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.ToTable("OrganizationSponsorship"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessAll") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Policy"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Provider", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasColumnType("text"); + + b.Property("BusinessAddress1") + .HasColumnType("text"); + + b.Property("BusinessAddress2") + .HasColumnType("text"); + + b.Property("BusinessAddress3") + .HasColumnType("text"); + + b.Property("BusinessCountry") + .HasColumnType("text"); + + b.Property("BusinessName") + .HasColumnType("text"); + + b.Property("BusinessTaxNumber") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("HideEmail") + .HasColumnType("boolean"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MaxAccessCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Send"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("SsoUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PaymentMethodType") + .HasColumnType("smallint"); + + b.Property("Refunded") + .HasColumnType("boolean"); + + b.Property("RefundedAmount") + .HasColumnType("numeric"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Transaction"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Culture") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("EquivalentDomains") + .HasColumnType("text"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("text"); + + b.Property("ForcePasswordReset") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Kdf") + .HasColumnType("smallint"); + + b.Property("KdfIterations") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Premium") + .HasColumnType("boolean"); + + b.Property("PremiumExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", null) + .WithMany("CollectionUsers") + .HasForeignKey("UserId"); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", null) + .WithMany("GroupUsers") + .HasForeignKey("UserId"); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Connections"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("CollectionUsers"); + + b.Navigation("Folders"); + + b.Navigation("GroupUsers"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/PostgresMigrations/Migrations/20220322183505_SelfHostF4E.cs b/util/PostgresMigrations/Migrations/20220322183505_SelfHostF4E.cs new file mode 100644 index 000000000..0965bb340 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20220322183505_SelfHostF4E.cs @@ -0,0 +1,155 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Bit.PostgresMigrations.Migrations +{ + public partial class SelfHostF4E : Migration + { + private const string _scriptLocationTemplate = "2022-03-01_00_{0}_MigrateOrganizationApiKeys.psql"; + + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_OrganizationSponsorship_Installation_InstallationId", + table: "OrganizationSponsorship"); + + migrationBuilder.DropIndex( + name: "IX_OrganizationSponsorship_InstallationId", + table: "OrganizationSponsorship"); + + migrationBuilder.DropColumn( + name: "InstallationId", + table: "OrganizationSponsorship"); + + migrationBuilder.DropColumn( + name: "TimesRenewedWithoutValidation", + table: "OrganizationSponsorship"); + + migrationBuilder.CreateTable( + name: "OrganizationApiKey", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + OrganizationId = table.Column(type: "uuid", nullable: false), + Type = table.Column(type: "smallint", nullable: false), + ApiKey = table.Column(type: "character varying(30)", maxLength: 30, nullable: true), + RevisionDate = table.Column(type: "timestamp without time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationApiKey", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationApiKey_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.SqlResource(_scriptLocationTemplate); + + migrationBuilder.DropColumn( + name: "ApiKey", + table: "Organization"); + + migrationBuilder.RenameColumn( + name: "SponsorshipLapsedDate", + table: "OrganizationSponsorship", + newName: "ValidUntil"); + + migrationBuilder.RenameColumn( + name: "CloudSponsor", + table: "OrganizationSponsorship", + newName: "ToDelete"); + + + + migrationBuilder.CreateTable( + name: "OrganizationConnection", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + Type = table.Column(type: "smallint", nullable: false), + OrganizationId = table.Column(type: "uuid", nullable: false), + Enabled = table.Column(type: "boolean", nullable: false), + Config = table.Column(type: "text", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OrganizationConnection", x => x.Id); + table.ForeignKey( + name: "FK_OrganizationConnection_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationApiKey_OrganizationId", + table: "OrganizationApiKey", + column: "OrganizationId"); + + migrationBuilder.CreateIndex( + name: "IX_OrganizationConnection_OrganizationId", + table: "OrganizationConnection", + column: "OrganizationId"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ApiKey", + table: "Organization", + type: "character varying(30)", + maxLength: 30, + nullable: true); + + migrationBuilder.SqlResource(_scriptLocationTemplate); + + migrationBuilder.DropTable( + name: "OrganizationApiKey"); + + migrationBuilder.DropTable( + name: "OrganizationConnection"); + + migrationBuilder.RenameColumn( + name: "ValidUntil", + table: "OrganizationSponsorship", + newName: "SponsorshipLapsedDate"); + + migrationBuilder.RenameColumn( + name: "ToDelete", + table: "OrganizationSponsorship", + newName: "CloudSponsor"); + + migrationBuilder.AddColumn( + name: "InstallationId", + table: "OrganizationSponsorship", + type: "uuid", + nullable: true); + + migrationBuilder.AddColumn( + name: "TimesRenewedWithoutValidation", + table: "OrganizationSponsorship", + type: "smallint", + nullable: false, + defaultValue: (byte)0); + + + + migrationBuilder.CreateIndex( + name: "IX_OrganizationSponsorship_InstallationId", + table: "OrganizationSponsorship", + column: "InstallationId"); + + migrationBuilder.AddForeignKey( + name: "FK_OrganizationSponsorship_Installation_InstallationId", + table: "OrganizationSponsorship", + column: "InstallationId", + principalTable: "Installation", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + } +} diff --git a/util/PostgresMigrations/Migrations/20220411190525_SponsorshipBulkActions.Designer.cs b/util/PostgresMigrations/Migrations/20220411190525_SponsorshipBulkActions.Designer.cs new file mode 100644 index 000000000..d59ad734e --- /dev/null +++ b/util/PostgresMigrations/Migrations/20220411190525_SponsorshipBulkActions.Designer.cs @@ -0,0 +1,1595 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Bit.PostgresMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20220411190525_SponsorshipBulkActions")] + partial class SponsorshipBulkActions + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:postgresIndetermanisticCollation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.12") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.HasIndex("UserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GranteeId") + .HasColumnType("uuid"); + + b.Property("GrantorId") + .HasColumnType("uuid"); + + b.Property("KeyEncrypted") + .HasColumnType("text"); + + b.Property("LastNotificationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("WaitTimeDays") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ActingUserId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp without time zone"); + + b.Property("DeviceType") + .HasColumnType("smallint"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("PolicyId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("ProviderOrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderUserId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Event"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Grant", b => + { + b.Property("Key") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ClientId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ConsumedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Key"); + + b.ToTable("Grant"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessAll") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.HasIndex("UserId"); + + b.ToTable("GroupUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.HasKey("Id"); + + b.ToTable("Installation"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp without time zone"); + + b.Property("Plan") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("SelfHost") + .HasColumnType("boolean"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .HasColumnType("boolean"); + + b.Property("UseSso") + .HasColumnType("boolean"); + + b.Property("UseTotp") + .HasColumnType("boolean"); + + b.Property("UsersGetPremium") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp without time zone"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.ToTable("OrganizationSponsorship"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessAll") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Policy"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Provider", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasColumnType("text"); + + b.Property("BusinessAddress1") + .HasColumnType("text"); + + b.Property("BusinessAddress2") + .HasColumnType("text"); + + b.Property("BusinessAddress3") + .HasColumnType("text"); + + b.Property("BusinessCountry") + .HasColumnType("text"); + + b.Property("BusinessName") + .HasColumnType("text"); + + b.Property("BusinessTaxNumber") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("HideEmail") + .HasColumnType("boolean"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MaxAccessCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Send"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("SsoUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PaymentMethodType") + .HasColumnType("smallint"); + + b.Property("Refunded") + .HasColumnType("boolean"); + + b.Property("RefundedAmount") + .HasColumnType("numeric"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Transaction"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Culture") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("EquivalentDomains") + .HasColumnType("text"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("text"); + + b.Property("FailedLoginCount") + .HasColumnType("integer"); + + b.Property("ForcePasswordReset") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Kdf") + .HasColumnType("smallint"); + + b.Property("KdfIterations") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LastFailedLoginDate") + .HasColumnType("timestamp without time zone"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Premium") + .HasColumnType("boolean"); + + b.Property("PremiumExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", null) + .WithMany("CollectionUsers") + .HasForeignKey("UserId"); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", null) + .WithMany("GroupUsers") + .HasForeignKey("UserId"); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Connections"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("CollectionUsers"); + + b.Navigation("Folders"); + + b.Navigation("GroupUsers"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/PostgresMigrations/Migrations/20220411190525_SponsorshipBulkActions.cs b/util/PostgresMigrations/Migrations/20220411190525_SponsorshipBulkActions.cs new file mode 100644 index 000000000..027a1ae5f --- /dev/null +++ b/util/PostgresMigrations/Migrations/20220411190525_SponsorshipBulkActions.cs @@ -0,0 +1,74 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Bit.PostgresMigrations.Migrations +{ + public partial class SponsorshipBulkActions : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_OrganizationSponsorship_Organization_SponsoringOrganization~", + table: "OrganizationSponsorship"); + + migrationBuilder.AlterColumn( + name: "SponsoringOrganizationUserId", + table: "OrganizationSponsorship", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AlterColumn( + name: "SponsoringOrganizationId", + table: "OrganizationSponsorship", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_OrganizationSponsorship_Organization_SponsoringOrganization~", + table: "OrganizationSponsorship", + column: "SponsoringOrganizationId", + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_OrganizationSponsorship_Organization_SponsoringOrganization~", + table: "OrganizationSponsorship"); + + migrationBuilder.AlterColumn( + name: "SponsoringOrganizationUserId", + table: "OrganizationSponsorship", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AlterColumn( + name: "SponsoringOrganizationId", + table: "OrganizationSponsorship", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AddForeignKey( + name: "FK_OrganizationSponsorship_Organization_SponsoringOrganization~", + table: "OrganizationSponsorship", + column: "SponsoringOrganizationId", + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + } +} diff --git a/util/PostgresMigrations/Migrations/20220420171153_AddInstallationIdToEvents.Designer.cs b/util/PostgresMigrations/Migrations/20220420171153_AddInstallationIdToEvents.Designer.cs new file mode 100644 index 000000000..1177add41 --- /dev/null +++ b/util/PostgresMigrations/Migrations/20220420171153_AddInstallationIdToEvents.Designer.cs @@ -0,0 +1,1596 @@ +// +using System; +using Bit.Infrastructure.EntityFramework.Repositories; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +namespace Bit.PostgresMigrations.Migrations +{ + [DbContext(typeof(DatabaseContext))] + [Migration("20220420171153_AddInstallationIdToEvents")] + partial class AddInstallationIdToEvents + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("Npgsql:CollationDefinition:postgresIndetermanisticCollation", "en-u-ks-primary,en-u-ks-primary,icu,False") + .HasAnnotation("Relational:MaxIdentifierLength", 63) + .HasAnnotation("ProductVersion", "5.0.12") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cipher", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Attachments") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Favorites") + .HasColumnType("text"); + + b.Property("Folders") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Reprompt") + .HasColumnType("smallint"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Cipher"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "CipherId"); + + b.HasIndex("CipherId"); + + b.ToTable("CollectionCipher"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.HasKey("CollectionId", "GroupId"); + + b.HasIndex("GroupId"); + + b.ToTable("CollectionGroups"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("HidePasswords") + .HasColumnType("boolean"); + + b.Property("ReadOnly") + .HasColumnType("boolean"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("CollectionId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.HasIndex("UserId"); + + b.ToTable("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PushToken") + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Device"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.EmergencyAccess", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("GranteeId") + .HasColumnType("uuid"); + + b.Property("GrantorId") + .HasColumnType("uuid"); + + b.Property("KeyEncrypted") + .HasColumnType("text"); + + b.Property("LastNotificationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RecoveryInitiatedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("WaitTimeDays") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("GranteeId"); + + b.HasIndex("GrantorId"); + + b.ToTable("EmergencyAccess"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Event", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ActingUserId") + .HasColumnType("uuid"); + + b.Property("CipherId") + .HasColumnType("uuid"); + + b.Property("CollectionId") + .HasColumnType("uuid"); + + b.Property("Date") + .HasColumnType("timestamp without time zone"); + + b.Property("DeviceType") + .HasColumnType("smallint"); + + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("InstallationId") + .HasColumnType("uuid"); + + b.Property("IpAddress") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("PolicyId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("ProviderOrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderUserId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.ToTable("Event"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Folder", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("Folder"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Grant", b => + { + b.Property("Key") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ClientId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ConsumedDate") + .HasColumnType("timestamp without time zone"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Description") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SubjectId") + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("Type") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.HasKey("Key"); + + b.ToTable("Grant"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessAll") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Name") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.Property("GroupId") + .HasColumnType("uuid"); + + b.Property("OrganizationUserId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("GroupId", "OrganizationUserId"); + + b.HasIndex("OrganizationUserId"); + + b.HasIndex("UserId"); + + b.ToTable("GroupUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Installation", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Key") + .HasMaxLength(150) + .HasColumnType("character varying(150)"); + + b.HasKey("Id"); + + b.ToTable("Installation"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Organization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("BusinessAddress1") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress2") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessAddress3") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessCountry") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.Property("BusinessName") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("BusinessTaxNumber") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Identifier") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MaxAutoscaleSeats") + .HasColumnType("integer"); + + b.Property("MaxCollections") + .HasColumnType("smallint"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OwnersNotifiedOfAutoscaling") + .HasColumnType("timestamp without time zone"); + + b.Property("Plan") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PlanType") + .HasColumnType("smallint"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Seats") + .HasColumnType("integer"); + + b.Property("SelfHost") + .HasColumnType("boolean"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("Use2fa") + .HasColumnType("boolean"); + + b.Property("UseApi") + .HasColumnType("boolean"); + + b.Property("UseDirectory") + .HasColumnType("boolean"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.Property("UseGroups") + .HasColumnType("boolean"); + + b.Property("UseKeyConnector") + .HasColumnType("boolean"); + + b.Property("UsePolicies") + .HasColumnType("boolean"); + + b.Property("UseResetPassword") + .HasColumnType("boolean"); + + b.Property("UseSso") + .HasColumnType("boolean"); + + b.Property("UseTotp") + .HasColumnType("boolean"); + + b.Property("UsersGetPremium") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("FriendlyName") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("LastSyncDate") + .HasColumnType("timestamp without time zone"); + + b.Property("OfferedToEmail") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("PlanSponsorshipType") + .HasColumnType("smallint"); + + b.Property("SponsoredOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationId") + .HasColumnType("uuid"); + + b.Property("SponsoringOrganizationUserId") + .HasColumnType("uuid"); + + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("SponsoredOrganizationId"); + + b.HasIndex("SponsoringOrganizationId"); + + b.ToTable("OrganizationSponsorship"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessAll") + .HasColumnType("boolean"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ExternalId") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ResetPasswordKey") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Policy", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Policy"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Provider", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("BillingEmail") + .HasColumnType("text"); + + b.Property("BusinessAddress1") + .HasColumnType("text"); + + b.Property("BusinessAddress2") + .HasColumnType("text"); + + b.Property("BusinessAddress3") + .HasColumnType("text"); + + b.Property("BusinessCountry") + .HasColumnType("text"); + + b.Property("BusinessName") + .HasColumnType("text"); + + b.Property("BusinessTaxNumber") + .HasColumnType("text"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("Name") + .HasColumnType("text"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("UseEvents") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderOrganization", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Settings") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("ProviderId"); + + b.ToTable("ProviderOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderUser", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Email") + .HasColumnType("text"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("Permissions") + .HasColumnType("text"); + + b.Property("ProviderId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Status") + .HasColumnType("smallint"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId"); + + b.HasIndex("UserId"); + + b.ToTable("ProviderUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("DeletionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Disabled") + .HasColumnType("boolean"); + + b.Property("ExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("HideEmail") + .HasColumnType("boolean"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("MaxAccessCount") + .HasColumnType("integer"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Password") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Send"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Data") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SsoConfig"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasAnnotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ExternalId") + .HasMaxLength(50) + .HasColumnType("character varying(50)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("SsoUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.TaxRate", b => + { + b.Property("Id") + .HasMaxLength(40) + .HasColumnType("character varying(40)"); + + b.Property("Active") + .HasColumnType("boolean"); + + b.Property("Country") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PostalCode") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Rate") + .HasColumnType("numeric"); + + b.Property("State") + .HasMaxLength(2) + .HasColumnType("character varying(2)"); + + b.HasKey("Id"); + + b.ToTable("TaxRate"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Amount") + .HasColumnType("numeric"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Details") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("PaymentMethodType") + .HasColumnType("smallint"); + + b.Property("Refunded") + .HasColumnType("boolean"); + + b.Property("RefundedAmount") + .HasColumnType("numeric"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.Property("UserId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("UserId"); + + b.ToTable("Transaction"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("AccountRevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("ApiKey") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("CreationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Culture") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .UseCollation("postgresIndetermanisticCollation"); + + b.Property("EmailVerified") + .HasColumnType("boolean"); + + b.Property("EquivalentDomains") + .HasColumnType("text"); + + b.Property("ExcludedGlobalEquivalentDomains") + .HasColumnType("text"); + + b.Property("FailedLoginCount") + .HasColumnType("integer"); + + b.Property("ForcePasswordReset") + .HasColumnType("boolean"); + + b.Property("Gateway") + .HasColumnType("smallint"); + + b.Property("GatewayCustomerId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("GatewaySubscriptionId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Kdf") + .HasColumnType("smallint"); + + b.Property("KdfIterations") + .HasColumnType("integer"); + + b.Property("Key") + .HasColumnType("text"); + + b.Property("LastFailedLoginDate") + .HasColumnType("timestamp without time zone"); + + b.Property("LicenseKey") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MasterPassword") + .HasMaxLength(300) + .HasColumnType("character varying(300)"); + + b.Property("MasterPasswordHint") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("MaxStorageGb") + .HasColumnType("smallint"); + + b.Property("Name") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Premium") + .HasColumnType("boolean"); + + b.Property("PremiumExpirationDate") + .HasColumnType("timestamp without time zone"); + + b.Property("PrivateKey") + .HasColumnType("text"); + + b.Property("PublicKey") + .HasColumnType("text"); + + b.Property("ReferenceData") + .HasColumnType("text"); + + b.Property("RenewalReminderDate") + .HasColumnType("timestamp without time zone"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("SecurityStamp") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Storage") + .HasColumnType("bigint"); + + b.Property("TwoFactorProviders") + .HasColumnType("text"); + + b.Property("TwoFactorRecoveryCode") + .HasMaxLength(32) + .HasColumnType("character varying(32)"); + + b.Property("UsesKeyConnector") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.ToTable("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Ciphers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Ciphers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionCipher", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Cipher", "Cipher") + .WithMany("CollectionCiphers") + .HasForeignKey("CipherId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionCiphers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Cipher"); + + b.Navigation("Collection"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionGroup", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionGroups") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany() + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Collection"); + + b.Navigation("Group"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.CollectionUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Collection", "Collection") + .WithMany("CollectionUsers") + .HasForeignKey("CollectionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany("CollectionUsers") + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", null) + .WithMany("CollectionUsers") + .HasForeignKey("UserId"); + + b.Navigation("Collection"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Device", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.EmergencyAccess", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantee") + .WithMany() + .HasForeignKey("GranteeId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "Grantor") + .WithMany() + .HasForeignKey("GrantorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Grantee"); + + b.Navigation("Grantor"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Folder", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Folders") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Groups") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.GroupUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Group", "Group") + .WithMany("GroupUsers") + .HasForeignKey("GroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", "OrganizationUser") + .WithMany() + .HasForeignKey("OrganizationUserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", null) + .WithMany("GroupUsers") + .HasForeignKey("UserId"); + + b.Navigation("Group"); + + b.Navigation("OrganizationUser"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "SponsoredOrganization") + .WithMany() + .HasForeignKey("SponsoredOrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "SponsoringOrganization") + .WithMany() + .HasForeignKey("SponsoringOrganizationId"); + + b.Navigation("SponsoredOrganization"); + + b.Navigation("SponsoringOrganization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("OrganizationUsers") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("OrganizationUsers") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Policy", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Policies") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderOrganization", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.ProviderUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Provider"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Send", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany() + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoConfig", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("SsoConfigs") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.SsoUser", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("SsoUsers") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("SsoUsers") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Transaction", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Transactions") + .HasForeignKey("OrganizationId"); + + b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") + .WithMany("Transactions") + .HasForeignKey("UserId"); + + b.Navigation("Organization"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Cipher", b => + { + b.Navigation("CollectionCiphers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Collection", b => + { + b.Navigation("CollectionCiphers"); + + b.Navigation("CollectionGroups"); + + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Group", b => + { + b.Navigation("GroupUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Organization", b => + { + b.Navigation("ApiKeys"); + + b.Navigation("Ciphers"); + + b.Navigation("Connections"); + + b.Navigation("Groups"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("Policies"); + + b.Navigation("SsoConfigs"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationUser", b => + { + b.Navigation("CollectionUsers"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.User", b => + { + b.Navigation("Ciphers"); + + b.Navigation("CollectionUsers"); + + b.Navigation("Folders"); + + b.Navigation("GroupUsers"); + + b.Navigation("OrganizationUsers"); + + b.Navigation("SsoUsers"); + + b.Navigation("Transactions"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/util/PostgresMigrations/Migrations/20220420171153_AddInstallationIdToEvents.cs b/util/PostgresMigrations/Migrations/20220420171153_AddInstallationIdToEvents.cs new file mode 100644 index 000000000..c9226d37a --- /dev/null +++ b/util/PostgresMigrations/Migrations/20220420171153_AddInstallationIdToEvents.cs @@ -0,0 +1,66 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +namespace Bit.PostgresMigrations.Migrations +{ + public partial class AddInstallationIdToEvents : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_OrganizationSponsorship_Organization_SponsoringOrganization~", + table: "OrganizationSponsorship"); + + migrationBuilder.AlterColumn( + name: "SponsoringOrganizationId", + table: "OrganizationSponsorship", + type: "uuid", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uuid"); + + migrationBuilder.AddColumn( + name: "InstallationId", + table: "Event", + type: "uuid", + nullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_OrganizationSponsorship_Organization_SponsoringOrganization~", + table: "OrganizationSponsorship", + column: "SponsoringOrganizationId", + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_OrganizationSponsorship_Organization_SponsoringOrganization~", + table: "OrganizationSponsorship"); + + migrationBuilder.DropColumn( + name: "InstallationId", + table: "Event"); + + migrationBuilder.AlterColumn( + name: "SponsoringOrganizationId", + table: "OrganizationSponsorship", + type: "uuid", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uuid", + oldNullable: true); + + migrationBuilder.AddForeignKey( + name: "FK_OrganizationSponsorship_Organization_SponsoringOrganization~", + table: "OrganizationSponsorship", + column: "SponsoringOrganizationId", + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs index 1869364be..b4b6a076e 100644 --- a/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs +++ b/util/PostgresMigrations/Migrations/DatabaseContextModelSnapshot.cs @@ -265,6 +265,9 @@ namespace Bit.PostgresMigrations.Migrations b.Property("GroupId") .HasColumnType("uuid"); + b.Property("InstallationId") + .HasColumnType("uuid"); + b.Property("IpAddress") .HasMaxLength(50) .HasColumnType("character varying(50)"); @@ -446,10 +449,6 @@ namespace Bit.PostgresMigrations.Migrations b.Property("Id") .HasColumnType("uuid"); - b.Property("ApiKey") - .HasMaxLength(30) - .HasColumnType("character varying(30)"); - b.Property("BillingEmail") .HasMaxLength(256) .HasColumnType("character varying(256)"); @@ -592,21 +591,64 @@ namespace Bit.PostgresMigrations.Migrations b.ToTable("Organization"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("ApiKey") + .HasMaxLength(30) + .HasColumnType("character varying(30)"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("RevisionDate") + .HasColumnType("timestamp without time zone"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationApiKey"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.Property("Id") + .HasColumnType("uuid"); + + b.Property("Config") + .HasColumnType("text"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("OrganizationId") + .HasColumnType("uuid"); + + b.Property("Type") + .HasColumnType("smallint"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationConnection"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => { b.Property("Id") .HasColumnType("uuid"); - b.Property("CloudSponsor") - .HasColumnType("boolean"); - b.Property("FriendlyName") .HasMaxLength(256) .HasColumnType("character varying(256)"); - b.Property("InstallationId") - .HasColumnType("uuid"); - b.Property("LastSyncDate") .HasColumnType("timestamp without time zone"); @@ -623,19 +665,17 @@ namespace Bit.PostgresMigrations.Migrations b.Property("SponsoringOrganizationId") .HasColumnType("uuid"); - b.Property("SponsoringOrganizationUserId") + b.Property("SponsoringOrganizationUserId") .HasColumnType("uuid"); - b.Property("SponsorshipLapsedDate") + b.Property("ToDelete") + .HasColumnType("boolean"); + + b.Property("ValidUntil") .HasColumnType("timestamp without time zone"); - b.Property("TimesRenewedWithoutValidation") - .HasColumnType("smallint"); - b.HasKey("Id"); - b.HasIndex("InstallationId"); - b.HasIndex("SponsoredOrganizationId"); b.HasIndex("SponsoringOrganizationId"); @@ -1328,12 +1368,30 @@ namespace Bit.PostgresMigrations.Migrations b.Navigation("OrganizationUser"); }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationApiKey", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("ApiKeys") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationConnection", b => + { + b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "Organization") + .WithMany("Connections") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.OrganizationSponsorship", b => { - b.HasOne("Bit.Infrastructure.EntityFramework.Models.Installation", "Installation") - .WithMany() - .HasForeignKey("InstallationId"); - b.HasOne("Bit.Infrastructure.EntityFramework.Models.Organization", "SponsoredOrganization") .WithMany() .HasForeignKey("SponsoredOrganizationId"); @@ -1342,8 +1400,6 @@ namespace Bit.PostgresMigrations.Migrations .WithMany() .HasForeignKey("SponsoringOrganizationId"); - b.Navigation("Installation"); - b.Navigation("SponsoredOrganization"); b.Navigation("SponsoringOrganization"); @@ -1492,8 +1548,12 @@ namespace Bit.PostgresMigrations.Migrations modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Models.Organization", b => { + b.Navigation("ApiKeys"); + b.Navigation("Ciphers"); + b.Navigation("Connections"); + b.Navigation("Groups"); b.Navigation("OrganizationUsers"); diff --git a/util/PostgresMigrations/PostgresMigrations.csproj b/util/PostgresMigrations/PostgresMigrations.csproj index d020bc409..364378bb2 100644 --- a/util/PostgresMigrations/PostgresMigrations.csproj +++ b/util/PostgresMigrations/PostgresMigrations.csproj @@ -16,6 +16,10 @@ + + + + @@ -23,5 +27,7 @@ + + diff --git a/util/PostgresMigrations/Scripts/2022-03-25_00_SelfHostF4E.psql b/util/PostgresMigrations/Scripts/2022-03-25_00_SelfHostF4E.psql new file mode 100644 index 000000000..3c9390015 --- /dev/null +++ b/util/PostgresMigrations/Scripts/2022-03-25_00_SelfHostF4E.psql @@ -0,0 +1,59 @@ +START TRANSACTION; + +ALTER TABLE "OrganizationSponsorship" DROP CONSTRAINT "FK_OrganizationSponsorship_Installation_InstallationId"; + +DROP INDEX "IX_OrganizationSponsorship_InstallationId"; + +ALTER TABLE "OrganizationSponsorship" DROP COLUMN "InstallationId"; + +ALTER TABLE "OrganizationSponsorship" DROP COLUMN "TimesRenewedWithoutValidation"; + +CREATE TABLE "OrganizationApiKey" ( + "Id" uuid NOT NULL, + "OrganizationId" uuid NOT NULL, + "Type" smallint NOT NULL, + "ApiKey" character varying(30) NULL, + "RevisionDate" timestamp without time zone NOT NULL, + CONSTRAINT "PK_OrganizationApiKey" PRIMARY KEY ("Id"), + CONSTRAINT "FK_OrganizationApiKey_Organization_OrganizationId" FOREIGN KEY ("OrganizationId") REFERENCES "Organization" ("Id") ON DELETE CASCADE +); + +INSERT INTO "OrganizationApiKey"( + "Id", + "OrganizationId", + "ApiKey", + "Type", + "RevisionDate") +SELECT gen_random_uuid(), + "Id" AS "OrganizationId", + "ApiKey", + 0 AS "Type", + "RevisionDate" +FROM "Organization"; + + +ALTER TABLE "Organization" DROP COLUMN "ApiKey"; + +ALTER TABLE "OrganizationSponsorship" RENAME COLUMN "SponsorshipLapsedDate" TO "ValidUntil"; + +ALTER TABLE "OrganizationSponsorship" RENAME COLUMN "CloudSponsor" TO "ToDelete"; + +CREATE TABLE "OrganizationConnection" ( + "Id" uuid NOT NULL, + "Type" smallint NOT NULL, + "OrganizationId" uuid NOT NULL, + "Enabled" boolean NOT NULL, + "Config" text NULL, + CONSTRAINT "PK_OrganizationConnection" PRIMARY KEY ("Id"), + CONSTRAINT "FK_OrganizationConnection_Organization_OrganizationId" FOREIGN KEY ("OrganizationId") REFERENCES "Organization" ("Id") ON DELETE CASCADE +); + +CREATE INDEX "IX_OrganizationApiKey_OrganizationId" ON "OrganizationApiKey" ("OrganizationId"); + +CREATE INDEX "IX_OrganizationConnection_OrganizationId" ON "OrganizationConnection" ("OrganizationId"); + +INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") +VALUES ('20220322183505_SelfHostF4E', '5.0.12'); + +COMMIT; + diff --git a/util/PostgresMigrations/Scripts/2022-04-11_00_SelfHostF4EModelChanges.sql b/util/PostgresMigrations/Scripts/2022-04-11_00_SelfHostF4EModelChanges.sql new file mode 100644 index 000000000..d90e92d34 --- /dev/null +++ b/util/PostgresMigrations/Scripts/2022-04-11_00_SelfHostF4EModelChanges.sql @@ -0,0 +1,16 @@ +START TRANSACTION; + +ALTER TABLE "OrganizationSponsorship" DROP CONSTRAINT "FK_OrganizationSponsorship_Organization_SponsoringOrganization~"; + +ALTER TABLE "OrganizationSponsorship" ALTER COLUMN "SponsoringOrganizationUserId" SET NOT NULL; +ALTER TABLE "OrganizationSponsorship" ALTER COLUMN "SponsoringOrganizationUserId" SET DEFAULT '00000000-0000-0000-0000-000000000000'; + +ALTER TABLE "OrganizationSponsorship" ALTER COLUMN "SponsoringOrganizationId" SET NOT NULL; +ALTER TABLE "OrganizationSponsorship" ALTER COLUMN "SponsoringOrganizationId" SET DEFAULT '00000000-0000-0000-0000-000000000000'; + +ALTER TABLE "OrganizationSponsorship" ADD CONSTRAINT "FK_OrganizationSponsorship_Organization_SponsoringOrganization~" FOREIGN KEY ("SponsoringOrganizationId") REFERENCES "Organization" ("Id") ON DELETE CASCADE; + +INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") +VALUES ('20220411190525_SponsorshipBulkActions', '5.0.12'); + +COMMIT; \ No newline at end of file diff --git a/util/PostgresMigrations/Scripts/2022-04-20_00_AddInstallationToEvents.psql b/util/PostgresMigrations/Scripts/2022-04-20_00_AddInstallationToEvents.psql new file mode 100644 index 000000000..2e4bbdf74 --- /dev/null +++ b/util/PostgresMigrations/Scripts/2022-04-20_00_AddInstallationToEvents.psql @@ -0,0 +1,14 @@ +START TRANSACTION; + +ALTER TABLE "OrganizationSponsorship" DROP CONSTRAINT "FK_OrganizationSponsorship_Organization_SponsoringOrganization~"; + +ALTER TABLE "OrganizationSponsorship" ALTER COLUMN "SponsoringOrganizationId" DROP NOT NULL; + +ALTER TABLE "Event" ADD "InstallationId" uuid NULL; + +ALTER TABLE "OrganizationSponsorship" ADD CONSTRAINT "FK_OrganizationSponsorship_Organization_SponsoringOrganization~" FOREIGN KEY ("SponsoringOrganizationId") REFERENCES "Organization" ("Id") ON DELETE RESTRICT; + +INSERT INTO "__EFMigrationsHistory" ("MigrationId", "ProductVersion") +VALUES ('20220420171153_AddInstallationIdToEvents', '5.0.12'); + +COMMIT;