1
0
mirror of https://github.com/bitwarden/server.git synced 2025-05-04 03:02:18 -05:00

Merge branch 'main' into feat/pm-14496-non-root-self-hosted-images

This commit is contained in:
tangowithfoxtrot 2025-04-30 12:44:44 -07:00 committed by GitHub
commit 761e794f7f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
84 changed files with 20544 additions and 269 deletions

View File

@ -110,9 +110,14 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
IEnumerable<string> organizationOwnerEmails) IEnumerable<string> organizationOwnerEmails)
{ {
if (provider.IsBillable() && if (provider.IsBillable() &&
organization.IsValidClient() && organization.IsValidClient())
!string.IsNullOrEmpty(organization.GatewayCustomerId))
{ {
// An organization converted to a business unit will not have a Customer since it was given to the business unit.
if (string.IsNullOrEmpty(organization.GatewayCustomerId))
{
await _providerBillingService.CreateCustomerForClientOrganization(provider, organization);
}
var customer = await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions var customer = await _stripeAdapter.CustomerUpdateAsync(organization.GatewayCustomerId, new CustomerUpdateOptions
{ {
Description = string.Empty, Description = string.Empty,

View File

@ -21,7 +21,6 @@ using Bit.Core.Models.Business;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities;
using CsvHelper; using CsvHelper;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
@ -46,7 +45,6 @@ public class ProviderBillingService(
[FromKeyedServices(AutomaticTaxFactory.BusinessUse)] IAutomaticTaxStrategy automaticTaxStrategy) [FromKeyedServices(AutomaticTaxFactory.BusinessUse)] IAutomaticTaxStrategy automaticTaxStrategy)
: IProviderBillingService : IProviderBillingService
{ {
[RequireFeature(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal)]
public async Task AddExistingOrganization( public async Task AddExistingOrganization(
Provider provider, Provider provider,
Organization organization, Organization organization,
@ -312,7 +310,6 @@ public class ProviderBillingService(
return memoryStream.ToArray(); return memoryStream.ToArray();
} }
[RequireFeature(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal)]
public async Task<IEnumerable<AddableOrganization>> GetAddableOrganizations( public async Task<IEnumerable<AddableOrganization>> GetAddableOrganizations(
Provider provider, Provider provider,
Guid userId) Guid userId)

View File

@ -441,9 +441,9 @@
} }
}, },
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.6", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -455,9 +455,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.13.5", "version": "22.13.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz",
"integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==", "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -687,9 +687,9 @@
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.14.0", "version": "8.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@ -821,9 +821,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001700", "version": "1.0.30001707",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz",
"integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==", "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -975,9 +975,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.103", "version": "1.5.128",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.103.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz",
"integrity": "sha512-P6+XzIkfndgsrjROJWfSvVEgNHtPgbhVyTkwLjUM2HU/h7pZRORgaTlHqfAikqxKmdJMLW8fftrdGWbd/Ds0FA==", "integrity": "sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@ -1248,9 +1248,9 @@
} }
}, },
"node_modules/immutable": { "node_modules/immutable": {
"version": "5.0.3", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz",
"integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", "integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -1501,9 +1501,9 @@
} }
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.8", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -2107,9 +2107,9 @@
} }
}, },
"node_modules/terser-webpack-plugin": { "node_modules/terser-webpack-plugin": {
"version": "5.3.11", "version": "5.3.14",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz",
"integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -2163,9 +2163,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.1.2", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
"integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {

View File

@ -124,8 +124,20 @@ services:
profiles: profiles:
- servicebus - servicebus
redis:
image: redis:alpine
container_name: bw-redis
ports:
- "6379:6379"
volumes:
- redis_data:/data
command: redis-server --appendonly yes
profiles:
- redis
volumes: volumes:
mssql_dev_data: mssql_dev_data:
postgres_dev_data: postgres_dev_data:
mysql_dev_data: mysql_dev_data:
rabbitmq_data: rabbitmq_data:
redis_data:

View File

@ -470,6 +470,19 @@ public class ProvidersController : Controller
[RequirePermission(Permission.Provider_Edit)] [RequirePermission(Permission.Provider_Edit)]
public async Task<IActionResult> Delete(Guid id, string providerName) public async Task<IActionResult> Delete(Guid id, string providerName)
{ {
var provider = await _providerRepository.GetByIdAsync(id);
if (provider is null)
{
return BadRequest("Provider does not exist");
}
if (provider.Status == ProviderStatusType.Pending)
{
await _providerService.DeleteAsync(provider);
return NoContent();
}
if (string.IsNullOrWhiteSpace(providerName)) if (string.IsNullOrWhiteSpace(providerName))
{ {
return BadRequest("Invalid provider name"); return BadRequest("Invalid provider name");
@ -482,13 +495,6 @@ public class ProvidersController : Controller
return BadRequest("You must unlink all clients before you can delete a provider"); return BadRequest("You must unlink all clients before you can delete a provider");
} }
var provider = await _providerRepository.GetByIdAsync(id);
if (provider is null)
{
return BadRequest("Provider does not exist");
}
if (!string.Equals(providerName.Trim(), provider.DisplayName(), StringComparison.OrdinalIgnoreCase)) if (!string.Equals(providerName.Trim(), provider.DisplayName(), StringComparison.OrdinalIgnoreCase))
{ {
return BadRequest("Invalid provider name"); return BadRequest("Invalid provider name");

View File

@ -183,6 +183,17 @@
<div class="p-3"> <div class="p-3">
<h4 class="fw-bolder" id="exampleModalLabel">Delete provider</h4> <h4 class="fw-bolder" id="exampleModalLabel">Delete provider</h4>
</div> </div>
@if (Model.Provider.Status == ProviderStatusType.Pending)
{
<div class="modal-body">
<span class="fw-light">
This action is permanent and irreversible.
</span>
</div>
}
else
{
<div class="modal-body"> <div class="modal-body">
<span class="fw-light"> <span class="fw-light">
This action is permanent and irreversible. Enter the provider name to complete deletion of the provider and associated data. This action is permanent and irreversible. Enter the provider name to complete deletion of the provider and associated data.
@ -194,6 +205,7 @@
</div> </div>
</form> </form>
</div> </div>
}
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-outline-primary btn-pill" data-bs-dismiss="modal">Cancel</button> <button type="button" class="btn btn-outline-primary btn-pill" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger btn-pill" onclick="deleteProvider('@Model.Provider.Id');">Delete provider</button> <button type="button" class="btn btn-danger btn-pill" onclick="deleteProvider('@Model.Provider.Id');">Delete provider</button>

View File

@ -442,9 +442,9 @@
} }
}, },
"node_modules/@types/estree": { "node_modules/@types/estree": {
"version": "1.0.6", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -456,9 +456,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.13.5", "version": "22.13.14",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz",
"integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==", "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -688,9 +688,9 @@
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.14.0", "version": "8.14.1",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@ -822,9 +822,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001700", "version": "1.0.30001707",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz",
"integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==", "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -976,9 +976,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.103", "version": "1.5.128",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.103.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz",
"integrity": "sha512-P6+XzIkfndgsrjROJWfSvVEgNHtPgbhVyTkwLjUM2HU/h7pZRORgaTlHqfAikqxKmdJMLW8fftrdGWbd/Ds0FA==", "integrity": "sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@ -1249,9 +1249,9 @@
} }
}, },
"node_modules/immutable": { "node_modules/immutable": {
"version": "5.0.3", "version": "5.1.1",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz",
"integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", "integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -1502,9 +1502,9 @@
} }
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.8", "version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -2108,9 +2108,9 @@
} }
}, },
"node_modules/terser-webpack-plugin": { "node_modules/terser-webpack-plugin": {
"version": "5.3.11", "version": "5.3.14",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz",
"integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -2172,9 +2172,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/update-browserslist-db": { "node_modules/update-browserslist-db": {
"version": "1.1.2", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
"integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {

View File

@ -1,13 +1,16 @@
using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Core;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.AdminConsole.Controllers; namespace Bit.Api.AdminConsole.Controllers;
[RequireFeature(FeatureFlagKeys.EventBasedOrganizationIntegrations)]
[Route("organizations/{organizationId:guid}/integrations/{integrationId:guid}/configurations")] [Route("organizations/{organizationId:guid}/integrations/{integrationId:guid}/configurations")]
[Authorize("Application")] [Authorize("Application")]
public class OrganizationIntegrationConfigurationController( public class OrganizationIntegrationConfigurationController(

View File

@ -1,8 +1,10 @@
using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Core;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -10,6 +12,7 @@ using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.AdminConsole.Controllers; namespace Bit.Api.AdminConsole.Controllers;
[RequireFeature(FeatureFlagKeys.EventBasedOrganizationIntegrations)]
[Route("organizations/{organizationId:guid}/integrations")] [Route("organizations/{organizationId:guid}/integrations")]
[Authorize("Application")] [Authorize("Application")]
public class OrganizationIntegrationController( public class OrganizationIntegrationController(

View File

@ -1,6 +1,5 @@
using Bit.Api.Billing.Controllers; using Bit.Api.Billing.Controllers;
using Bit.Api.Billing.Models.Requests; using Bit.Api.Billing.Models.Requests;
using Bit.Core;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
@ -17,7 +16,6 @@ namespace Bit.Api.AdminConsole.Controllers;
[Route("providers/{providerId:guid}/clients")] [Route("providers/{providerId:guid}/clients")]
public class ProviderClientsController( public class ProviderClientsController(
ICurrentContext currentContext, ICurrentContext currentContext,
IFeatureService featureService,
ILogger<BaseProviderController> logger, ILogger<BaseProviderController> logger,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IProviderBillingService providerBillingService, IProviderBillingService providerBillingService,
@ -140,11 +138,6 @@ public class ProviderClientsController(
[SelfHosted(NotSelfHostedOnly = true)] [SelfHosted(NotSelfHostedOnly = true)]
public async Task<IResult> GetAddableOrganizationsAsync([FromRoute] Guid providerId) public async Task<IResult> GetAddableOrganizationsAsync([FromRoute] Guid providerId)
{ {
if (!featureService.IsEnabled(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal))
{
return Error.NotFound();
}
var (provider, result) = await TryGetBillableProviderForServiceUserOperation(providerId); var (provider, result) = await TryGetBillableProviderForServiceUserOperation(providerId);
if (provider == null) if (provider == null)

View File

@ -1,5 +1,6 @@
using System.Text.Json; using System.Text.Json;
using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -7,11 +8,13 @@ using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Integrations; using Bit.Core.Models.Data.Integrations;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.AdminConsole.Controllers; namespace Bit.Api.AdminConsole.Controllers;
[RequireFeature(FeatureFlagKeys.EventBasedOrganizationIntegrations)]
[Route("organizations/{organizationId:guid}/integrations/slack")] [Route("organizations/{organizationId:guid}/integrations/slack")]
[Authorize("Application")] [Authorize("Application")]
public class SlackIntegrationController( public class SlackIntegrationController(

View File

@ -221,8 +221,7 @@ public class MembersController : Controller
/// Remove a member. /// Remove a member.
/// </summary> /// </summary>
/// <remarks> /// <remarks>
/// Permanently removes a member from the organization. This cannot be undone. /// Removes a member from the organization. This cannot be undone. The user account will still remain.
/// The user account will still remain. The user is only removed from the organization.
/// </remarks> /// </remarks>
/// <param name="id">The identifier of the member to be removed.</param> /// <param name="id">The identifier of the member to be removed.</param>
[HttpDelete("{id}")] [HttpDelete("{id}")]

View File

@ -86,9 +86,9 @@ public class OrganizationSponsorshipsController : Controller
if (!_featureService.IsEnabled(Bit.Core.FeatureFlagKeys.PM17772_AdminInitiatedSponsorships)) if (!_featureService.IsEnabled(Bit.Core.FeatureFlagKeys.PM17772_AdminInitiatedSponsorships))
{ {
if (model.SponsoringUserId.HasValue) if (model.IsAdminInitiated.GetValueOrDefault())
{ {
throw new NotFoundException(); throw new BadRequestException();
} }
if (!string.IsNullOrWhiteSpace(model.Notes)) if (!string.IsNullOrWhiteSpace(model.Notes))
@ -97,13 +97,13 @@ public class OrganizationSponsorshipsController : Controller
} }
} }
var targetUser = model.SponsoringUserId ?? _currentContext.UserId!.Value;
var sponsorship = await _createSponsorshipCommand.CreateSponsorshipAsync( var sponsorship = await _createSponsorshipCommand.CreateSponsorshipAsync(
sponsoringOrg, sponsoringOrg,
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, targetUser), await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default),
model.PlanSponsorshipType, model.PlanSponsorshipType,
model.SponsoredEmail, model.SponsoredEmail,
model.FriendlyName, model.FriendlyName,
model.IsAdminInitiated.GetValueOrDefault(),
model.Notes); model.Notes);
await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name); await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name);
} }

View File

@ -0,0 +1,34 @@
using Bit.Core;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Controllers;
[Route("phishing-domains")]
public class PhishingDomainsController(IPhishingDomainRepository phishingDomainRepository, IFeatureService featureService) : Controller
{
[HttpGet]
public async Task<ActionResult<ICollection<string>>> GetPhishingDomainsAsync()
{
if (!featureService.IsEnabled(FeatureFlagKeys.PhishingDetection))
{
return NotFound();
}
var domains = await phishingDomainRepository.GetActivePhishingDomainsAsync();
return Ok(domains);
}
[HttpGet("checksum")]
public async Task<ActionResult<string>> GetChecksumAsync()
{
if (!featureService.IsEnabled(FeatureFlagKeys.PhishingDetection))
{
return NotFound();
}
var checksum = await phishingDomainRepository.GetCurrentChecksumAsync();
return Ok(checksum);
}
}

View File

@ -47,9 +47,9 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
{ {
if (!_featureService.IsEnabled(Bit.Core.FeatureFlagKeys.PM17772_AdminInitiatedSponsorships)) if (!_featureService.IsEnabled(Bit.Core.FeatureFlagKeys.PM17772_AdminInitiatedSponsorships))
{ {
if (model.SponsoringUserId.HasValue) if (model.IsAdminInitiated.GetValueOrDefault())
{ {
throw new NotFoundException(); throw new BadRequestException();
} }
if (!string.IsNullOrWhiteSpace(model.Notes)) if (!string.IsNullOrWhiteSpace(model.Notes))
@ -60,8 +60,12 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
await _offerSponsorshipCommand.CreateSponsorshipAsync( await _offerSponsorshipCommand.CreateSponsorshipAsync(
await _organizationRepository.GetByIdAsync(sponsoringOrgId), await _organizationRepository.GetByIdAsync(sponsoringOrgId),
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, model.SponsoringUserId ?? _currentContext.UserId ?? default), await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default),
model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName, model.Notes); model.PlanSponsorshipType,
model.SponsoredEmail,
model.FriendlyName,
model.IsAdminInitiated.GetValueOrDefault(),
model.Notes);
} }
[HttpDelete("{sponsoringOrgId}")] [HttpDelete("{sponsoringOrgId}")]

View File

@ -58,6 +58,13 @@ public class JobsHostedService : BaseJobsHostedService
.StartNow() .StartNow()
.WithCronSchedule("0 0 * * * ?") .WithCronSchedule("0 0 * * * ?")
.Build(); .Build();
var updatePhishingDomainsTrigger = TriggerBuilder.Create()
.WithIdentity("UpdatePhishingDomainsTrigger")
.StartNow()
.WithSimpleSchedule(x => x
.WithIntervalInHours(24)
.RepeatForever())
.Build();
var jobs = new List<Tuple<Type, ITrigger>> var jobs = new List<Tuple<Type, ITrigger>>
@ -68,6 +75,7 @@ public class JobsHostedService : BaseJobsHostedService
new Tuple<Type, ITrigger>(typeof(ValidateUsersJob), everyTopOfTheSixthHourTrigger), new Tuple<Type, ITrigger>(typeof(ValidateUsersJob), everyTopOfTheSixthHourTrigger),
new Tuple<Type, ITrigger>(typeof(ValidateOrganizationsJob), everyTwelfthHourAndThirtyMinutesTrigger), new Tuple<Type, ITrigger>(typeof(ValidateOrganizationsJob), everyTwelfthHourAndThirtyMinutesTrigger),
new Tuple<Type, ITrigger>(typeof(ValidateOrganizationDomainJob), validateOrganizationDomainTrigger), new Tuple<Type, ITrigger>(typeof(ValidateOrganizationDomainJob), validateOrganizationDomainTrigger),
new Tuple<Type, ITrigger>(typeof(UpdatePhishingDomainsJob), updatePhishingDomainsTrigger),
}; };
if (_globalSettings.SelfHosted && _globalSettings.EnableCloudCommunication) if (_globalSettings.SelfHosted && _globalSettings.EnableCloudCommunication)
@ -96,6 +104,7 @@ public class JobsHostedService : BaseJobsHostedService
services.AddTransient<ValidateUsersJob>(); services.AddTransient<ValidateUsersJob>();
services.AddTransient<ValidateOrganizationsJob>(); services.AddTransient<ValidateOrganizationsJob>();
services.AddTransient<ValidateOrganizationDomainJob>(); services.AddTransient<ValidateOrganizationDomainJob>();
services.AddTransient<UpdatePhishingDomainsJob>();
} }
public static void AddCommercialSecretsManagerJobServices(IServiceCollection services) public static void AddCommercialSecretsManagerJobServices(IServiceCollection services)

View File

@ -0,0 +1,97 @@
using Bit.Core;
using Bit.Core.Jobs;
using Bit.Core.PhishingDomainFeatures.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Quartz;
namespace Bit.Api.Jobs;
public class UpdatePhishingDomainsJob : BaseJob
{
private readonly GlobalSettings _globalSettings;
private readonly IPhishingDomainRepository _phishingDomainRepository;
private readonly ICloudPhishingDomainQuery _cloudPhishingDomainQuery;
private readonly IFeatureService _featureService;
public UpdatePhishingDomainsJob(
GlobalSettings globalSettings,
IPhishingDomainRepository phishingDomainRepository,
ICloudPhishingDomainQuery cloudPhishingDomainQuery,
IFeatureService featureService,
ILogger<UpdatePhishingDomainsJob> logger)
: base(logger)
{
_globalSettings = globalSettings;
_phishingDomainRepository = phishingDomainRepository;
_cloudPhishingDomainQuery = cloudPhishingDomainQuery;
_featureService = featureService;
}
protected override async Task ExecuteJobAsync(IJobExecutionContext context)
{
if (!_featureService.IsEnabled(FeatureFlagKeys.PhishingDetection))
{
_logger.LogInformation(Constants.BypassFiltersEventId, "Skipping phishing domain update. Feature flag is disabled.");
return;
}
if (string.IsNullOrWhiteSpace(_globalSettings.PhishingDomain?.UpdateUrl))
{
_logger.LogInformation(Constants.BypassFiltersEventId, "Skipping phishing domain update. No URL configured.");
return;
}
if (_globalSettings.SelfHosted && !_globalSettings.EnableCloudCommunication)
{
_logger.LogInformation(Constants.BypassFiltersEventId, "Skipping phishing domain update. Cloud communication is disabled in global settings.");
return;
}
var remoteChecksum = await _cloudPhishingDomainQuery.GetRemoteChecksumAsync();
if (string.IsNullOrWhiteSpace(remoteChecksum))
{
_logger.LogWarning(Constants.BypassFiltersEventId, "Could not retrieve remote checksum. Skipping update.");
return;
}
var currentChecksum = await _phishingDomainRepository.GetCurrentChecksumAsync();
if (string.Equals(currentChecksum, remoteChecksum, StringComparison.OrdinalIgnoreCase))
{
_logger.LogInformation(Constants.BypassFiltersEventId,
"Phishing domains list is up to date (checksum: {Checksum}). Skipping update.",
currentChecksum);
return;
}
_logger.LogInformation(Constants.BypassFiltersEventId,
"Checksums differ (current: {CurrentChecksum}, remote: {RemoteChecksum}). Fetching updated domains from {Source}.",
currentChecksum, remoteChecksum, _globalSettings.SelfHosted ? "Bitwarden cloud API" : "external source");
try
{
var domains = await _cloudPhishingDomainQuery.GetPhishingDomainsAsync();
if (!domains.Contains("phishing.testcategory.com", StringComparer.OrdinalIgnoreCase))
{
domains.Add("phishing.testcategory.com");
}
if (domains.Count > 0)
{
_logger.LogInformation(Constants.BypassFiltersEventId, "Updating {Count} phishing domains with checksum {Checksum}.",
domains.Count, remoteChecksum);
await _phishingDomainRepository.UpdatePhishingDomainsAsync(domains, remoteChecksum);
_logger.LogInformation(Constants.BypassFiltersEventId, "Successfully updated phishing domains.");
}
else
{
_logger.LogWarning(Constants.BypassFiltersEventId, "No valid domains found in the response. Skipping update.");
}
}
catch (Exception ex)
{
_logger.LogError(Constants.BypassFiltersEventId, ex, "Error updating phishing domains.");
}
}
}

View File

@ -17,11 +17,7 @@ public class OrganizationSponsorshipCreateRequestModel
[StringLength(256)] [StringLength(256)]
public string FriendlyName { get; set; } public string FriendlyName { get; set; }
/// <summary> public bool? IsAdminInitiated { get; set; }
/// (optional) The user to target for the sponsorship.
/// </summary>
/// <remarks>Left empty when creating a sponsorship for the authenticated user.</remarks>
public Guid? SponsoringUserId { get; set; }
[EncryptedString] [EncryptedString]
[EncryptedStringLength(512)] [EncryptedStringLength(512)]

View File

@ -182,6 +182,7 @@ public class Startup
services.AddBillingOperations(); services.AddBillingOperations();
services.AddReportingServices(); services.AddReportingServices();
services.AddImportServices(); services.AddImportServices();
services.AddPhishingDomainServices(globalSettings);
// Authorization Handlers // Authorization Handlers
services.AddAuthorizationHandlers(); services.AddAuthorizationHandlers();

View File

@ -3,6 +3,10 @@ using Bit.Api.Tools.Authorization;
using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization; using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
using Bit.Core.IdentityServer; using Bit.Core.IdentityServer;
using Bit.Core.PhishingDomainFeatures;
using Bit.Core.PhishingDomainFeatures.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Repositories.Implementations;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.Core.Vault.Authorization.SecurityTasks; using Bit.Core.Vault.Authorization.SecurityTasks;
@ -109,4 +113,25 @@ public static class ServiceCollectionExtensions
services.AddScoped<IAuthorizationHandler, OrganizationRequirementHandler>(); services.AddScoped<IAuthorizationHandler, OrganizationRequirementHandler>();
} }
public static void AddPhishingDomainServices(this IServiceCollection services, GlobalSettings globalSettings)
{
services.AddHttpClient("PhishingDomains", client =>
{
client.DefaultRequestHeaders.Add("User-Agent", globalSettings.SelfHosted ? "Bitwarden Self-Hosted" : "Bitwarden");
client.Timeout = TimeSpan.FromSeconds(1000); // the source list is very slow
});
services.AddSingleton<AzurePhishingDomainStorageService>();
services.AddSingleton<IPhishingDomainRepository, AzurePhishingDomainRepository>();
if (globalSettings.SelfHosted)
{
services.AddScoped<ICloudPhishingDomainQuery, CloudPhishingDomainRelayQuery>();
}
else
{
services.AddScoped<ICloudPhishingDomainQuery, CloudPhishingDomainDirectQuery>();
}
}
} }

View File

@ -37,6 +37,10 @@
}, },
"storage": { "storage": {
"connectionString": "UseDevelopmentStorage=true" "connectionString": "UseDevelopmentStorage=true"
},
"phishingDomain": {
"updateUrl": "https://phish.co.za/latest/phishing-domains-ACTIVE.txt",
"checksumUrl": "https://raw.githubusercontent.com/Phishing-Database/checksums/refs/heads/master/phishing-domains-ACTIVE.txt.sha256"
} }
} }
} }

View File

@ -71,6 +71,9 @@
"accessKeySecret": "SECRET", "accessKeySecret": "SECRET",
"region": "SECRET" "region": "SECRET"
}, },
"phishingDomain": {
"updateUrl": "SECRET"
},
"distributedIpRateLimiting": { "distributedIpRateLimiting": {
"enabled": true, "enabled": true,
"maxRedisTimeoutsThreshold": 10, "maxRedisTimeoutsThreshold": 10,

View File

@ -44,7 +44,7 @@ public class InviteUsersPasswordManagerValidator(
return new Invalid<PasswordManagerSubscriptionUpdate>(new PasswordManagerMustHaveSeatsError(subscriptionUpdate)); return new Invalid<PasswordManagerSubscriptionUpdate>(new PasswordManagerMustHaveSeatsError(subscriptionUpdate));
} }
if (subscriptionUpdate.MaxSeatsReached) if (subscriptionUpdate.MaxSeatsExceeded)
{ {
return new Invalid<PasswordManagerSubscriptionUpdate>( return new Invalid<PasswordManagerSubscriptionUpdate>(
new PasswordManagerSeatLimitHasBeenReachedError(subscriptionUpdate)); new PasswordManagerSeatLimitHasBeenReachedError(subscriptionUpdate));

View File

@ -48,6 +48,11 @@ public class PasswordManagerSubscriptionUpdate
/// </summary> /// </summary>
public bool MaxSeatsReached => UpdatedSeatTotal.HasValue && MaxAutoScaleSeats.HasValue && UpdatedSeatTotal.Value >= MaxAutoScaleSeats.Value; public bool MaxSeatsReached => UpdatedSeatTotal.HasValue && MaxAutoScaleSeats.HasValue && UpdatedSeatTotal.Value >= MaxAutoScaleSeats.Value;
/// <summary>
/// If the new seat total exceeds the organization's auto-scale seat limit
/// </summary>
public bool MaxSeatsExceeded => UpdatedSeatTotal.HasValue && MaxAutoScaleSeats.HasValue && UpdatedSeatTotal.Value > MaxAutoScaleSeats.Value;
public Plan.PasswordManagerPlanFeatures PasswordManagerPlan { get; } public Plan.PasswordManagerPlanFeatures PasswordManagerPlan { get; }
public InviteOrganization InviteOrganization { get; } public InviteOrganization InviteOrganization { get; }

View File

@ -18,6 +18,15 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
Task<ICollection<OrganizationUser>> GetManyByUserAsync(Guid userId); Task<ICollection<OrganizationUser>> GetManyByUserAsync(Guid userId);
Task<ICollection<OrganizationUser>> GetManyByOrganizationAsync(Guid organizationId, OrganizationUserType? type); Task<ICollection<OrganizationUser>> GetManyByOrganizationAsync(Guid organizationId, OrganizationUserType? type);
Task<int> GetCountByOrganizationAsync(Guid organizationId, string email, bool onlyRegisteredUsers); Task<int> GetCountByOrganizationAsync(Guid organizationId, string email, bool onlyRegisteredUsers);
/// <summary>
/// Returns the number of occupied seats for an organization.
/// Occupied seats are OrganizationUsers that have at least been invited.
/// As of https://bitwarden.atlassian.net/browse/PM-17772, a seat is also occupied by a Families for Enterprise sponsorship sent by an
/// organization admin, even if the user sent the invitation doesn't have a corresponding OrganizationUser in the Enterprise organization.
/// </summary>
/// <param name="organizationId">The ID of the organization to get the occupied seat count for.</param>
/// <returns>The number of occupied seats for the organization.</returns>
Task<int> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId); Task<int> GetOccupiedSeatCountByOrganizationIdAsync(Guid organizationId);
Task<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails, bool onlyRegisteredUsers); Task<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails, bool onlyRegisteredUsers);
Task<OrganizationUser?> GetByOrganizationAsync(Guid organizationId, Guid userId); Task<OrganizationUser?> GetByOrganizationAsync(Guid organizationId, Guid userId);

View File

@ -41,6 +41,7 @@ public static class OrganizationLicenseConstants
public const string Refresh = nameof(Refresh); public const string Refresh = nameof(Refresh);
public const string ExpirationWithoutGracePeriod = nameof(ExpirationWithoutGracePeriod); public const string ExpirationWithoutGracePeriod = nameof(ExpirationWithoutGracePeriod);
public const string Trial = nameof(Trial); public const string Trial = nameof(Trial);
public const string UseAdminSponsoredFamilies = nameof(UseAdminSponsoredFamilies);
} }
public static class UserLicenseConstants public static class UserLicenseConstants

View File

@ -53,6 +53,7 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory<Organizati
new(nameof(OrganizationLicenseConstants.Refresh), refresh.ToString(CultureInfo.InvariantCulture)), new(nameof(OrganizationLicenseConstants.Refresh), refresh.ToString(CultureInfo.InvariantCulture)),
new(nameof(OrganizationLicenseConstants.ExpirationWithoutGracePeriod), expirationWithoutGracePeriod.ToString(CultureInfo.InvariantCulture)), new(nameof(OrganizationLicenseConstants.ExpirationWithoutGracePeriod), expirationWithoutGracePeriod.ToString(CultureInfo.InvariantCulture)),
new(nameof(OrganizationLicenseConstants.Trial), trial.ToString()), new(nameof(OrganizationLicenseConstants.Trial), trial.ToString()),
new(nameof(OrganizationLicenseConstants.UseAdminSponsoredFamilies), entity.UseAdminSponsoredFamilies.ToString()),
}; };
if (entity.Name is not null) if (entity.Name is not null)
@ -109,6 +110,7 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory<Organizati
{ {
claims.Add(new Claim(nameof(OrganizationLicenseConstants.SmServiceAccounts), entity.SmServiceAccounts.ToString())); claims.Add(new Claim(nameof(OrganizationLicenseConstants.SmServiceAccounts), entity.SmServiceAccounts.ToString()));
} }
claims.Add(new Claim(nameof(OrganizationLicenseConstants.UseAdminSponsoredFamilies), entity.UseAdminSponsoredFamilies.ToString()));
return Task.FromResult(claims); return Task.FromResult(claims);
} }

View File

@ -108,6 +108,7 @@ public static class FeatureFlagKeys
public const string PolicyRequirements = "pm-14439-policy-requirements"; public const string PolicyRequirements = "pm-14439-policy-requirements";
public const string SsoExternalIdVisibility = "pm-18630-sso-external-id-visibility"; public const string SsoExternalIdVisibility = "pm-18630-sso-external-id-visibility";
public const string ScimInviteUserOptimization = "pm-16811-optimize-invite-user-flow-to-fail-fast"; public const string ScimInviteUserOptimization = "pm-16811-optimize-invite-user-flow-to-fail-fast";
public const string EventBasedOrganizationIntegrations = "event-based-organization-integrations";
/* Auth Team */ /* Auth Team */
public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence"; public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence";
@ -142,7 +143,6 @@ public static class FeatureFlagKeys
public const string TrialPayment = "PM-8163-trial-payment"; public const string TrialPayment = "PM-8163-trial-payment";
public const string PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships"; public const string PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships";
public const string UsePricingService = "use-pricing-service"; public const string UsePricingService = "use-pricing-service";
public const string P15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal";
public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features"; public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features";
public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method"; public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method";
public const string PM19147_AutomaticTaxImprovements = "pm-19147-automatic-tax-improvements"; public const string PM19147_AutomaticTaxImprovements = "pm-19147-automatic-tax-improvements";
@ -150,6 +150,10 @@ public static class FeatureFlagKeys
public const string PM18770_EnableOrganizationBusinessUnitConversion = "pm-18770-enable-organization-business-unit-conversion"; public const string PM18770_EnableOrganizationBusinessUnitConversion = "pm-18770-enable-organization-business-unit-conversion";
public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically"; public const string PM199566_UpdateMSPToChargeAutomatically = "pm-199566-update-msp-to-charge-automatically";
/* Data Insights and Reporting Team */
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications";
/* Key Management Team */ /* Key Management Team */
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair"; public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service"; public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service";
@ -186,8 +190,6 @@ public static class FeatureFlagKeys
/* Tools Team */ /* Tools Team */
public const string ItemShare = "item-share"; public const string ItemShare = "item-share";
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications";
public const string DesktopSendUIRefresh = "desktop-send-ui-refresh"; public const string DesktopSendUIRefresh = "desktop-send-ui-refresh";
/* Vault Team */ /* Vault Team */
@ -195,13 +197,13 @@ public static class FeatureFlagKeys
public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form"; public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form";
public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss"; public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss";
public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss"; public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss";
public const string VaultBulkManagementAction = "vault-bulk-management-action";
public const string RestrictProviderAccess = "restrict-provider-access"; public const string RestrictProviderAccess = "restrict-provider-access";
public const string SecurityTasks = "security-tasks"; public const string SecurityTasks = "security-tasks";
public const string CipherKeyEncryption = "cipher-key-encryption"; public const string CipherKeyEncryption = "cipher-key-encryption";
public const string DesktopCipherForms = "pm-18520-desktop-cipher-forms"; public const string DesktopCipherForms = "pm-18520-desktop-cipher-forms";
public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk"; public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk";
public const string EndUserNotifications = "pm-10609-end-user-notifications"; public const string EndUserNotifications = "pm-10609-end-user-notifications";
public const string PhishingDetection = "phishing-detection";
public static List<string> GetAllKeys() public static List<string> GetAllKeys()
{ {

View File

@ -23,8 +23,8 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" /> <PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.402.61" /> <PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.402.79" />
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.118" /> <PackageReference Include="AWSSDK.SQS" Version="3.7.400.136" />
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" /> <PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" /> <PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" /> <PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
@ -38,7 +38,7 @@
<PackageReference Include="Handlebars.Net" Version="2.1.6" /> <PackageReference Include="Handlebars.Net" Version="2.1.6" />
<PackageReference Include="MailKit" Version="4.11.0" /> <PackageReference Include="MailKit" Version="4.11.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" /> <PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.46.1" /> <PackageReference Include="Microsoft.Azure.Cosmos" Version="3.49.0" />
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" /> <PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" /> <PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.7.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.7.0" />

View File

@ -19,6 +19,34 @@ public class OrganizationLicense : ILicense
{ {
} }
/// <summary>
/// Initializes a new instance of the <see cref="OrganizationLicense"/> class.
/// </summary>
/// <remarks>
/// <para>
/// ⚠️ DEPRECATED: This constructor and the entire property-based licensing system is deprecated.
/// Do not add new properties to this constructor or extend its functionality.
/// </para>
/// <para>
/// This implementation has been replaced by a new claims-based licensing system that provides better security
/// and flexibility. The new system uses JWT claims to store and validate license information, making it more
/// secure and easier to extend without requiring changes to the license format.
/// </para>
/// <para>
/// For new license-related features or modifications:
/// 1. Use the claims-based system instead of adding properties here
/// 2. Add new claims to the license token
/// 3. Validate claims in the <see cref="CanUse"/> and <see cref="VerifyData"/> methods
/// </para>
/// <para>
/// This constructor is maintained only for backward compatibility with existing licenses.
/// </para>
/// </remarks>
/// <param name="org">The organization to create the license for.</param>
/// <param name="subscriptionInfo">Information about the organization's subscription.</param>
/// <param name="installationId">The ID of the current installation.</param>
/// <param name="licenseService">The service used to sign the license.</param>
/// <param name="version">Optional version number for the license format.</param>
public OrganizationLicense(Organization org, SubscriptionInfo subscriptionInfo, Guid installationId, public OrganizationLicense(Organization org, SubscriptionInfo subscriptionInfo, Guid installationId,
ILicensingService licenseService, int? version = null) ILicensingService licenseService, int? version = null)
{ {
@ -105,6 +133,7 @@ public class OrganizationLicense : ILicense
Trial = false; Trial = false;
} }
UseAdminSponsoredFamilies = org.UseAdminSponsoredFamilies;
Hash = Convert.ToBase64String(ComputeHash()); Hash = Convert.ToBase64String(ComputeHash());
Signature = Convert.ToBase64String(licenseService.SignLicense(this)); Signature = Convert.ToBase64String(licenseService.SignLicense(this));
} }
@ -153,6 +182,7 @@ public class OrganizationLicense : ILicense
public bool Trial { get; set; } public bool Trial { get; set; }
public LicenseType? LicenseType { get; set; } public LicenseType? LicenseType { get; set; }
public bool UseAdminSponsoredFamilies { get; set; }
public string Hash { get; set; } public string Hash { get; set; }
public string Signature { get; set; } public string Signature { get; set; }
public string Token { get; set; } public string Token { get; set; }
@ -292,13 +322,35 @@ public class OrganizationLicense : ILicense
} }
/// <summary> /// <summary>
/// Do not extend this method. It is only here for backwards compatibility with old licenses. /// Validates an obsolete license format using property-based validation.
/// Instead, extend the CanUse method using the ClaimsPrincipal.
/// </summary> /// </summary>
/// <param name="globalSettings"></param> /// <remarks>
/// <param name="licensingService"></param> /// <para>
/// <param name="exception"></param> /// ⚠️ DEPRECATED: This method is deprecated and should not be extended or modified.
/// <returns></returns> /// It is maintained only for backward compatibility with old license formats.
/// </para>
/// <para>
/// This method has been replaced by a new claims-based validation system that provides:
/// - Better security through JWT claims
/// - More flexible validation rules
/// - Easier extensibility without changing the license format
/// - Better separation of concerns
/// </para>
/// <para>
/// To add new license validation rules:
/// 1. Add new claims to the license token in the claims-based system
/// 2. Extend the <see cref="CanUse(IGlobalSettings, ILicensingService, ClaimsPrincipal, out string)"/> method
/// 3. Validate the new claims using the ClaimsPrincipal parameter
/// </para>
/// <para>
/// This method will be removed in a future version once all old licenses have been migrated
/// to the new claims-based system.
/// </para>
/// </remarks>
/// <param name="globalSettings">The global settings containing installation information.</param>
/// <param name="licensingService">The service used to verify the license signature.</param>
/// <param name="exception">When the method returns false, contains the error message explaining why the license is invalid.</param>
/// <returns>True if the license is valid, false otherwise.</returns>
private bool ObsoleteCanUse(IGlobalSettings globalSettings, ILicensingService licensingService, out string exception) private bool ObsoleteCanUse(IGlobalSettings globalSettings, ILicensingService licensingService, out string exception)
{ {
// Do not extend this method. It is only here for backwards compatibility with old licenses. // Do not extend this method. It is only here for backwards compatibility with old licenses.
@ -392,6 +444,7 @@ public class OrganizationLicense : ILicense
var usePasswordManager = claimsPrincipal.GetValue<bool>(nameof(UsePasswordManager)); var usePasswordManager = claimsPrincipal.GetValue<bool>(nameof(UsePasswordManager));
var smSeats = claimsPrincipal.GetValue<int?>(nameof(SmSeats)); var smSeats = claimsPrincipal.GetValue<int?>(nameof(SmSeats));
var smServiceAccounts = claimsPrincipal.GetValue<int?>(nameof(SmServiceAccounts)); var smServiceAccounts = claimsPrincipal.GetValue<int?>(nameof(SmServiceAccounts));
var useAdminSponsoredFamilies = claimsPrincipal.GetValue<bool>(nameof(UseAdminSponsoredFamilies));
return issued <= DateTime.UtcNow && return issued <= DateTime.UtcNow &&
expires >= DateTime.UtcNow && expires >= DateTime.UtcNow &&
@ -419,7 +472,9 @@ public class OrganizationLicense : ILicense
useSecretsManager == organization.UseSecretsManager && useSecretsManager == organization.UseSecretsManager &&
usePasswordManager == organization.UsePasswordManager && usePasswordManager == organization.UsePasswordManager &&
smSeats == organization.SmSeats && smSeats == organization.SmSeats &&
smServiceAccounts == organization.SmServiceAccounts; smServiceAccounts == organization.SmServiceAccounts &&
useAdminSponsoredFamilies == organization.UseAdminSponsoredFamilies;
} }
/// <summary> /// <summary>

View File

@ -14,11 +14,17 @@ namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnte
public class CreateSponsorshipCommand( public class CreateSponsorshipCommand(
ICurrentContext currentContext, ICurrentContext currentContext,
IOrganizationSponsorshipRepository organizationSponsorshipRepository, IOrganizationSponsorshipRepository organizationSponsorshipRepository,
IUserService userService) : ICreateSponsorshipCommand IUserService userService,
IOrganizationService organizationService) : ICreateSponsorshipCommand
{ {
public async Task<OrganizationSponsorship> CreateSponsorshipAsync(Organization sponsoringOrganization, public async Task<OrganizationSponsorship> CreateSponsorshipAsync(
OrganizationUser sponsoringMember, PlanSponsorshipType sponsorshipType, string sponsoredEmail, Organization sponsoringOrganization,
string friendlyName, string notes) OrganizationUser sponsoringMember,
PlanSponsorshipType sponsorshipType,
string sponsoredEmail,
string friendlyName,
bool isAdminInitiated,
string notes)
{ {
var sponsoringUser = await userService.GetUserByIdAsync(sponsoringMember.UserId!.Value); var sponsoringUser = await userService.GetUserByIdAsync(sponsoringMember.UserId!.Value);
@ -48,12 +54,21 @@ public class CreateSponsorshipCommand(
throw new BadRequestException("Can only sponsor one organization per Organization User."); throw new BadRequestException("Can only sponsor one organization per Organization User.");
} }
var sponsorship = new OrganizationSponsorship(); if (isAdminInitiated)
sponsorship.SponsoringOrganizationId = sponsoringOrganization.Id; {
sponsorship.SponsoringOrganizationUserId = sponsoringMember.Id; ValidateAdminInitiatedSponsorship(sponsoringOrganization);
sponsorship.FriendlyName = friendlyName; }
sponsorship.OfferedToEmail = sponsoredEmail;
sponsorship.PlanSponsorshipType = sponsorshipType; var sponsorship = new OrganizationSponsorship
{
SponsoringOrganizationId = sponsoringOrganization.Id,
SponsoringOrganizationUserId = sponsoringMember.Id,
FriendlyName = friendlyName,
OfferedToEmail = sponsoredEmail,
PlanSponsorshipType = sponsorshipType,
IsAdminInitiated = isAdminInitiated,
Notes = notes
};
if (existingOrgSponsorship != null) if (existingOrgSponsorship != null)
{ {
@ -61,35 +76,22 @@ public class CreateSponsorshipCommand(
sponsorship.Id = existingOrgSponsorship.Id; sponsorship.Id = existingOrgSponsorship.Id;
} }
var isAdminInitiated = false; if (isAdminInitiated && sponsoringOrganization.Seats.HasValue)
if (currentContext.UserId != sponsoringMember.UserId)
{ {
var organization = currentContext.Organizations.First(x => x.Id == sponsoringOrganization.Id); await organizationService.AutoAddSeatsAsync(sponsoringOrganization, 1);
OrganizationUserType[] allowedUserTypes =
[
OrganizationUserType.Admin,
OrganizationUserType.Owner
];
if (!organization.Permissions.ManageUsers && allowedUserTypes.All(x => x != organization.Type))
{
throw new UnauthorizedAccessException("You do not have permissions to send sponsorships on behalf of the organization.");
} }
if (!sponsoringOrganization.UseAdminSponsoredFamilies)
{
throw new BadRequestException("Sponsoring organization cannot sponsor other Family organizations.");
}
isAdminInitiated = true;
}
sponsorship.IsAdminInitiated = isAdminInitiated;
sponsorship.Notes = notes;
try try
{
if (isAdminInitiated)
{
await organizationSponsorshipRepository.CreateAsync(sponsorship);
}
else
{ {
await organizationSponsorshipRepository.UpsertAsync(sponsorship); await organizationSponsorshipRepository.UpsertAsync(sponsorship);
}
return sponsorship; return sponsorship;
} }
catch catch
@ -101,4 +103,24 @@ public class CreateSponsorshipCommand(
throw; throw;
} }
} }
private void ValidateAdminInitiatedSponsorship(Organization sponsoringOrganization)
{
var organization = currentContext.Organizations.First(x => x.Id == sponsoringOrganization.Id);
OrganizationUserType[] allowedUserTypes =
[
OrganizationUserType.Admin,
OrganizationUserType.Owner
];
if (!organization.Permissions.ManageUsers && allowedUserTypes.All(x => x != organization.Type))
{
throw new UnauthorizedAccessException("You do not have permissions to send sponsorships on behalf of the organization");
}
if (!sponsoringOrganization.UseAdminSponsoredFamilies)
{
throw new BadRequestException("Sponsoring organization cannot send admin-initiated sponsorship invitations");
}
}
} }

View File

@ -6,6 +6,12 @@ namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnte
public interface ICreateSponsorshipCommand public interface ICreateSponsorshipCommand
{ {
Task<OrganizationSponsorship> CreateSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser, Task<OrganizationSponsorship> CreateSponsorshipAsync(
PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName, string notes); Organization sponsoringOrg,
OrganizationUser sponsoringOrgUser,
PlanSponsorshipType sponsorshipType,
string sponsoredEmail,
string friendlyName,
bool isAdminInitiated,
string notes);
} }

View File

@ -0,0 +1,92 @@
using System.Text;
using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging;
namespace Bit.Core.PhishingDomainFeatures;
public class AzurePhishingDomainStorageService
{
private const string _containerName = "phishingdomains";
private const string _domainsFileName = "domains.txt";
private const string _checksumFileName = "checksum.txt";
private readonly BlobServiceClient _blobServiceClient;
private readonly ILogger<AzurePhishingDomainStorageService> _logger;
private BlobContainerClient _containerClient;
public AzurePhishingDomainStorageService(
GlobalSettings globalSettings,
ILogger<AzurePhishingDomainStorageService> logger)
{
_blobServiceClient = new BlobServiceClient(globalSettings.Storage.ConnectionString);
_logger = logger;
}
public async Task<ICollection<string>> GetDomainsAsync()
{
await InitAsync();
var blobClient = _containerClient.GetBlobClient(_domainsFileName);
if (!await blobClient.ExistsAsync())
{
return [];
}
var response = await blobClient.DownloadAsync();
using var streamReader = new StreamReader(response.Value.Content);
var content = await streamReader.ReadToEndAsync();
return [.. content
.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim())
.Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith('#'))];
}
public async Task<string> GetChecksumAsync()
{
await InitAsync();
var blobClient = _containerClient.GetBlobClient(_checksumFileName);
if (!await blobClient.ExistsAsync())
{
return string.Empty;
}
var response = await blobClient.DownloadAsync();
using var streamReader = new StreamReader(response.Value.Content);
return (await streamReader.ReadToEndAsync()).Trim();
}
public async Task UpdateDomainsAsync(IEnumerable<string> domains, string checksum)
{
await InitAsync();
var domainsContent = string.Join(Environment.NewLine, domains);
var domainsStream = new MemoryStream(Encoding.UTF8.GetBytes(domainsContent));
var domainsBlobClient = _containerClient.GetBlobClient(_domainsFileName);
await domainsBlobClient.UploadAsync(domainsStream, new BlobUploadOptions
{
HttpHeaders = new BlobHttpHeaders { ContentType = "text/plain" }
}, CancellationToken.None);
var checksumStream = new MemoryStream(Encoding.UTF8.GetBytes(checksum));
var checksumBlobClient = _containerClient.GetBlobClient(_checksumFileName);
await checksumBlobClient.UploadAsync(checksumStream, new BlobUploadOptions
{
HttpHeaders = new BlobHttpHeaders { ContentType = "text/plain" }
}, CancellationToken.None);
}
private async Task InitAsync()
{
if (_containerClient is null)
{
_containerClient = _blobServiceClient.GetBlobContainerClient(_containerName);
await _containerClient.CreateIfNotExistsAsync();
}
}
}

View File

@ -0,0 +1,100 @@
using Bit.Core.PhishingDomainFeatures.Interfaces;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging;
namespace Bit.Core.PhishingDomainFeatures;
/// <summary>
/// Implementation of ICloudPhishingDomainQuery for cloud environments
/// that directly calls the external phishing domain source
/// </summary>
public class CloudPhishingDomainDirectQuery : ICloudPhishingDomainQuery
{
private readonly IGlobalSettings _globalSettings;
private readonly IHttpClientFactory _httpClientFactory;
private readonly ILogger<CloudPhishingDomainDirectQuery> _logger;
public CloudPhishingDomainDirectQuery(
IGlobalSettings globalSettings,
IHttpClientFactory httpClientFactory,
ILogger<CloudPhishingDomainDirectQuery> logger)
{
_globalSettings = globalSettings;
_httpClientFactory = httpClientFactory;
_logger = logger;
}
public async Task<List<string>> GetPhishingDomainsAsync()
{
if (string.IsNullOrWhiteSpace(_globalSettings.PhishingDomain?.UpdateUrl))
{
throw new InvalidOperationException("Phishing domain update URL is not configured.");
}
var httpClient = _httpClientFactory.CreateClient("PhishingDomains");
var response = await httpClient.GetAsync(_globalSettings.PhishingDomain.UpdateUrl);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return ParseDomains(content);
}
/// <summary>
/// Gets the SHA256 checksum of the remote phishing domains list
/// </summary>
/// <returns>The SHA256 checksum as a lowercase hex string</returns>
public async Task<string> GetRemoteChecksumAsync()
{
if (string.IsNullOrWhiteSpace(_globalSettings.PhishingDomain?.ChecksumUrl))
{
_logger.LogWarning("Phishing domain checksum URL is not configured.");
return string.Empty;
}
try
{
var httpClient = _httpClientFactory.CreateClient("PhishingDomains");
var response = await httpClient.GetAsync(_globalSettings.PhishingDomain.ChecksumUrl);
response.EnsureSuccessStatusCode();
var content = await response.Content.ReadAsStringAsync();
return ParseChecksumResponse(content);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving phishing domain checksum from {Url}",
_globalSettings.PhishingDomain.ChecksumUrl);
return string.Empty;
}
}
/// <summary>
/// Parses a checksum response in the format "hash *filename"
/// </summary>
private static string ParseChecksumResponse(string checksumContent)
{
if (string.IsNullOrWhiteSpace(checksumContent))
{
return string.Empty;
}
// Format is typically "hash *filename"
var parts = checksumContent.Split(' ', 2);
return parts.Length > 0 ? parts[0].Trim() : string.Empty;
}
private static List<string> ParseDomains(string content)
{
if (string.IsNullOrWhiteSpace(content))
{
return [];
}
return content
.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries)
.Select(line => line.Trim())
.Where(line => !string.IsNullOrWhiteSpace(line) && !line.StartsWith("#"))
.ToList();
}
}

View File

@ -0,0 +1,66 @@
using Bit.Core.PhishingDomainFeatures.Interfaces;
using Bit.Core.Services;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging;
namespace Bit.Core.PhishingDomainFeatures;
/// <summary>
/// Implementation of ICloudPhishingDomainQuery for self-hosted environments
/// that relays the request to the Bitwarden cloud API
/// </summary>
public class CloudPhishingDomainRelayQuery : BaseIdentityClientService, ICloudPhishingDomainQuery
{
private readonly IGlobalSettings _globalSettings;
public CloudPhishingDomainRelayQuery(
IHttpClientFactory httpFactory,
IGlobalSettings globalSettings,
ILogger<CloudPhishingDomainRelayQuery> logger)
: base(
httpFactory,
globalSettings.Installation.ApiUri,
globalSettings.Installation.IdentityUri,
"api.licensing",
$"installation.{globalSettings.Installation.Id}",
globalSettings.Installation.Key,
logger)
{
_globalSettings = globalSettings;
}
public async Task<List<string>> GetPhishingDomainsAsync()
{
if (!_globalSettings.SelfHosted || !_globalSettings.EnableCloudCommunication)
{
throw new InvalidOperationException("This query is only for self-hosted installations with cloud communication enabled.");
}
var result = await SendAsync<object, string[]>(HttpMethod.Get, "phishing-domains", null, true);
return result?.ToList() ?? new List<string>();
}
/// <summary>
/// Gets the SHA256 checksum of the remote phishing domains list
/// </summary>
/// <returns>The SHA256 checksum as a lowercase hex string</returns>
public async Task<string> GetRemoteChecksumAsync()
{
if (!_globalSettings.SelfHosted || !_globalSettings.EnableCloudCommunication)
{
throw new InvalidOperationException("This query is only for self-hosted installations with cloud communication enabled.");
}
try
{
// For self-hosted environments, we get the checksum from the Bitwarden cloud API
var result = await SendAsync<object, string>(HttpMethod.Get, "phishing-domains/checksum", null, true);
return result ?? string.Empty;
}
catch (Exception ex)
{
_logger.LogError(ex, "Error retrieving phishing domain checksum from Bitwarden cloud API");
return string.Empty;
}
}
}

View File

@ -0,0 +1,7 @@
namespace Bit.Core.PhishingDomainFeatures.Interfaces;
public interface ICloudPhishingDomainQuery
{
Task<List<string>> GetPhishingDomainsAsync();
Task<string> GetRemoteChecksumAsync();
}

View File

@ -0,0 +1,8 @@
namespace Bit.Core.Repositories;
public interface IPhishingDomainRepository
{
Task<ICollection<string>> GetActivePhishingDomainsAsync();
Task UpdatePhishingDomainsAsync(IEnumerable<string> domains, string checksum);
Task<string> GetCurrentChecksumAsync();
}

View File

@ -0,0 +1,126 @@
using System.Text.Json;
using Bit.Core.PhishingDomainFeatures;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Repositories.Implementations;
public class AzurePhishingDomainRepository : IPhishingDomainRepository
{
private readonly AzurePhishingDomainStorageService _storageService;
private readonly IDistributedCache _cache;
private readonly ILogger<AzurePhishingDomainRepository> _logger;
private const string _domainsCacheKey = "PhishingDomains_v1";
private const string _checksumCacheKey = "PhishingDomains_Checksum_v1";
private static readonly DistributedCacheEntryOptions _cacheOptions = new()
{
AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24),
SlidingExpiration = TimeSpan.FromHours(1)
};
public AzurePhishingDomainRepository(
AzurePhishingDomainStorageService storageService,
IDistributedCache cache,
ILogger<AzurePhishingDomainRepository> logger)
{
_storageService = storageService;
_cache = cache;
_logger = logger;
}
public async Task<ICollection<string>> GetActivePhishingDomainsAsync()
{
try
{
var cachedDomains = await _cache.GetStringAsync(_domainsCacheKey);
if (!string.IsNullOrEmpty(cachedDomains))
{
_logger.LogDebug("Retrieved phishing domains from cache");
return JsonSerializer.Deserialize<ICollection<string>>(cachedDomains) ?? [];
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to retrieve phishing domains from cache");
}
var domains = await _storageService.GetDomainsAsync();
try
{
await _cache.SetStringAsync(
_domainsCacheKey,
JsonSerializer.Serialize(domains),
_cacheOptions);
_logger.LogDebug("Stored {Count} phishing domains in cache", domains.Count);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to store phishing domains in cache");
}
return domains;
}
public async Task<string> GetCurrentChecksumAsync()
{
try
{
var cachedChecksum = await _cache.GetStringAsync(_checksumCacheKey);
if (!string.IsNullOrEmpty(cachedChecksum))
{
_logger.LogDebug("Retrieved phishing domain checksum from cache");
return cachedChecksum;
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to retrieve phishing domain checksum from cache");
}
var checksum = await _storageService.GetChecksumAsync();
try
{
if (!string.IsNullOrEmpty(checksum))
{
await _cache.SetStringAsync(
_checksumCacheKey,
checksum,
_cacheOptions);
_logger.LogDebug("Stored phishing domain checksum in cache");
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to store phishing domain checksum in cache");
}
return checksum;
}
public async Task UpdatePhishingDomainsAsync(IEnumerable<string> domains, string checksum)
{
var domainsList = domains.ToList();
await _storageService.UpdateDomainsAsync(domainsList, checksum);
try
{
await _cache.SetStringAsync(
_domainsCacheKey,
JsonSerializer.Serialize(domainsList),
_cacheOptions);
await _cache.SetStringAsync(
_checksumCacheKey,
checksum,
_cacheOptions);
_logger.LogDebug("Updated phishing domains cache after update operation");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to update phishing domains in cache");
}
}
}

View File

@ -85,6 +85,7 @@ public class GlobalSettings : IGlobalSettings
public virtual ILaunchDarklySettings LaunchDarkly { get; set; } = new LaunchDarklySettings(); public virtual ILaunchDarklySettings LaunchDarkly { get; set; } = new LaunchDarklySettings();
public virtual string DevelopmentDirectory { get; set; } public virtual string DevelopmentDirectory { get; set; }
public virtual IWebPushSettings WebPush { get; set; } = new WebPushSettings(); public virtual IWebPushSettings WebPush { get; set; } = new WebPushSettings();
public virtual IPhishingDomainSettings PhishingDomain { get; set; } = new PhishingDomainSettings();
public virtual bool EnableEmailVerification { get; set; } public virtual bool EnableEmailVerification { get; set; }
public virtual string KdfDefaultHashKey { get; set; } public virtual string KdfDefaultHashKey { get; set; }
@ -645,6 +646,12 @@ public class GlobalSettings : IGlobalSettings
public int MaxNetworkRetries { get; set; } = 2; public int MaxNetworkRetries { get; set; } = 2;
} }
public class PhishingDomainSettings : IPhishingDomainSettings
{
public string UpdateUrl { get; set; }
public string ChecksumUrl { get; set; }
}
public class DistributedIpRateLimitingSettings public class DistributedIpRateLimitingSettings
{ {
public string RedisConnectionString { get; set; } public string RedisConnectionString { get; set; }

View File

@ -29,4 +29,5 @@ public interface IGlobalSettings
string DevelopmentDirectory { get; set; } string DevelopmentDirectory { get; set; }
IWebPushSettings WebPush { get; set; } IWebPushSettings WebPush { get; set; }
GlobalSettings.EventLoggingSettings EventLogging { get; set; } GlobalSettings.EventLoggingSettings EventLogging { get; set; }
IPhishingDomainSettings PhishingDomain { get; set; }
} }

View File

@ -0,0 +1,7 @@
namespace Bit.Core.Settings;
public interface IPhishingDomainSettings
{
string UpdateUrl { get; set; }
string ChecksumUrl { get; set; }
}

View File

@ -1,4 +1,5 @@
using System.Globalization; using System.Globalization;
using Bit.Core;
using Bit.Core.AdminConsole.Services.Implementations; using Bit.Core.AdminConsole.Services.Implementations;
using Bit.Core.AdminConsole.Services.NoopImplementations; using Bit.Core.AdminConsole.Services.NoopImplementations;
using Bit.Core.Context; using Bit.Core.Context;
@ -62,33 +63,45 @@ public class Startup
{ {
services.AddSingleton<IApplicationCacheService, InMemoryApplicationCacheService>(); services.AddSingleton<IApplicationCacheService, InMemoryApplicationCacheService>();
} }
services.AddScoped<IEventService, EventService>();
if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString)) if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString))
{ {
services.AddKeyedSingleton<IEventWriteService, AzureQueueEventWriteService>("storage");
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) && if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) &&
CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName)) CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName))
{ {
services.AddSingleton<IEventWriteService, AzureServiceBusEventWriteService>(); services.AddKeyedSingleton<IEventWriteService, AzureServiceBusEventWriteService>("broadcast");
} }
else else
{ {
services.AddSingleton<IEventWriteService, AzureQueueEventWriteService>(); services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
} }
} }
else else
{ {
services.AddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("storage");
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) && if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) &&
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) && CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) &&
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) && CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) &&
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName)) CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName))
{ {
services.AddSingleton<IEventWriteService, RabbitMqEventWriteService>(); services.AddKeyedSingleton<IEventWriteService, RabbitMqEventWriteService>("broadcast");
} }
else else
{ {
services.AddSingleton<IEventWriteService, RepositoryEventWriteService>(); services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
} }
} }
services.AddScoped<IEventWriteService>(sp =>
{
var featureService = sp.GetRequiredService<IFeatureService>();
var key = featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations)
? "broadcast" : "storage";
return sp.GetRequiredKeyedService<IEventWriteService>(key);
});
services.AddScoped<IEventService, EventService>();
services.AddOptionality(); services.AddOptionality();

View File

@ -150,6 +150,8 @@ public static class DapperHelpers
os => os.LastSyncDate, os => os.LastSyncDate,
os => os.ValidUntil, os => os.ValidUntil,
os => os.ToDelete, os => os.ToDelete,
os => os.IsAdminInitiated,
os => os.Notes,
] ]
); );

View File

@ -10,7 +10,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Dapper" Version="2.1.35" /> <PackageReference Include="Dapper" Version="2.1.66" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -14,9 +14,35 @@ public class OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery : IQuery
public IQueryable<OrganizationUser> Run(DatabaseContext dbContext) public IQueryable<OrganizationUser> Run(DatabaseContext dbContext)
{ {
var query = from ou in dbContext.OrganizationUsers var orgUsersQuery = from ou in dbContext.OrganizationUsers
where ou.OrganizationId == _organizationId && ou.Status >= OrganizationUserStatusType.Invited where ou.OrganizationId == _organizationId && ou.Status >= OrganizationUserStatusType.Invited
select ou; select new OrganizationUser { Id = ou.Id, OrganizationId = ou.OrganizationId, Status = ou.Status };
return query;
// As of https://bitwarden.atlassian.net/browse/PM-17772, a seat is also occupied by a Families for Enterprise sponsorship sent by an
// organization admin, even if the user sent the invitation doesn't have a corresponding OrganizationUser in the Enterprise organization.
var sponsorshipsQuery = from os in dbContext.OrganizationSponsorships
where os.SponsoringOrganizationId == _organizationId &&
os.IsAdminInitiated &&
(
// Not marked for deletion - always count
(!os.ToDelete) ||
// Marked for deletion but has a valid until date in the future (RevokeWhenExpired status)
(os.ToDelete && os.ValidUntil.HasValue && os.ValidUntil.Value > DateTime.UtcNow)
) &&
(
// SENT status: When SponsoredOrganizationId is null
os.SponsoredOrganizationId == null ||
// ACCEPTED status: When SponsoredOrganizationId is not null and ValidUntil is null or in the future
(os.SponsoredOrganizationId != null &&
(!os.ValidUntil.HasValue || os.ValidUntil.Value > DateTime.UtcNow))
)
select new OrganizationUser
{
Id = os.Id,
OrganizationId = _organizationId,
Status = OrganizationUserStatusType.Invited
};
return orgUsersQuery.Concat(sponsorshipsQuery);
} }
} }

View File

@ -8,12 +8,12 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" /> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
<PackageReference Include="linq2db" Version="5.4.1" /> <PackageReference Include="linq2db" Version="5.4.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="[8.0.8]" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="[8.0.8]" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="[8.0.8]" />
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" /> <PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="[8.0.4]" />
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" /> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="[8.0.2]" />
<PackageReference Include="linq2db.EntityFrameworkCore" Version="8.1.0" /> <PackageReference Include="linq2db.EntityFrameworkCore" Version="[8.1.0]" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -34,6 +34,12 @@ public class NotificationEntityTypeConfiguration : IEntityTypeConfiguration<Noti
.HasIndex(n => n.TaskId) .HasIndex(n => n.TaskId)
.IsClustered(false); .IsClustered(false);
builder
.HasOne(n => n.Task)
.WithMany()
.HasForeignKey(n => n.TaskId)
.OnDelete(DeleteBehavior.Cascade);
builder.ToTable(nameof(Notification)); builder.ToTable(nameof(Notification));
} }
} }

View File

@ -24,6 +24,18 @@ public class SecurityTaskEntityTypeConfiguration : IEntityTypeConfiguration<Secu
.HasIndex(s => s.CipherId) .HasIndex(s => s.CipherId)
.IsClustered(false); .IsClustered(false);
builder
.HasOne(p => p.Organization)
.WithMany()
.HasForeignKey(p => p.OrganizationId)
.OnDelete(DeleteBehavior.Cascade);
builder
.HasOne(p => p.Cipher)
.WithMany()
.HasForeignKey(p => p.CipherId)
.OnDelete(DeleteBehavior.Cascade);
builder builder
.ToTable(nameof(SecurityTask)); .ToTable(nameof(SecurityTask));
} }

View File

@ -5,6 +5,7 @@ using System.Security.Claims;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using AspNetCoreRateLimit; using AspNetCoreRateLimit;
using Azure.Storage.Queues; using Azure.Storage.Queues;
using Bit.Core;
using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
@ -333,34 +334,46 @@ public static class ServiceCollectionExtensions
if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString)) if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString))
{ {
services.AddKeyedSingleton<IEventWriteService, AzureQueueEventWriteService>("storage");
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) && if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) &&
CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName)) CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName))
{ {
services.AddSingleton<IEventWriteService, AzureServiceBusEventWriteService>(); services.AddKeyedSingleton<IEventWriteService, AzureServiceBusEventWriteService>("broadcast");
} }
else else
{ {
services.AddSingleton<IEventWriteService, AzureQueueEventWriteService>(); services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
} }
} }
else if (globalSettings.SelfHosted) else if (globalSettings.SelfHosted)
{ {
services.AddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("storage");
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) && if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) &&
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) && CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) &&
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) && CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) &&
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName)) CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName))
{ {
services.AddSingleton<IEventWriteService, RabbitMqEventWriteService>(); services.AddKeyedSingleton<IEventWriteService, RabbitMqEventWriteService>("broadcast");
} }
else else
{ {
services.AddSingleton<IEventWriteService, RepositoryEventWriteService>(); services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
} }
} }
else else
{ {
services.AddSingleton<IEventWriteService, NoopEventWriteService>(); services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("storage");
services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
} }
services.AddScoped<IEventWriteService>(sp =>
{
var featureService = sp.GetRequiredService<IFeatureService>();
var key = featureService.IsEnabled(FeatureFlagKeys.EventBasedOrganizationIntegrations)
? "broadcast" : "storage";
return sp.GetRequiredKeyedService<IEventWriteService>(key);
});
if (CoreHelpers.SettingHasValue(globalSettings.Attachment.ConnectionString)) if (CoreHelpers.SettingHasValue(globalSettings.Attachment.ConnectionString))
{ {

View File

@ -14,7 +14,7 @@ CREATE TABLE [dbo].[Notification]
CONSTRAINT [PK_Notification] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [PK_Notification] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_Notification_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]), CONSTRAINT [FK_Notification_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]),
CONSTRAINT [FK_Notification_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]), CONSTRAINT [FK_Notification_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]),
CONSTRAINT [FK_Notification_SecurityTask] FOREIGN KEY ([TaskId]) REFERENCES [dbo].[SecurityTask] ([Id]) CONSTRAINT [FK_Notification_SecurityTask] FOREIGN KEY ([TaskId]) REFERENCES [dbo].[SecurityTask] ([Id]) ON DELETE CASCADE
); );

View File

@ -6,7 +6,7 @@
@Data NVARCHAR(MAX), @Data NVARCHAR(MAX),
@Favorites NVARCHAR(MAX), -- not used @Favorites NVARCHAR(MAX), -- not used
@Folders NVARCHAR(MAX), -- not used @Folders NVARCHAR(MAX), -- not used
@Attachments NVARCHAR(MAX), @Attachments NVARCHAR(MAX), -- not used
@CreationDate DATETIME2(7), @CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7), @RevisionDate DATETIME2(7),
@FolderId UNIQUEIDENTIFIER, @FolderId UNIQUEIDENTIFIER,
@ -50,7 +50,6 @@ BEGIN
ELSE ELSE
JSON_MODIFY([Favorites], @UserIdPath, NULL) JSON_MODIFY([Favorites], @UserIdPath, NULL)
END, END,
[Attachments] = @Attachments,
[Reprompt] = @Reprompt, [Reprompt] = @Reprompt,
[CreationDate] = @CreationDate, [CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate, [RevisionDate] = @RevisionDate,

View File

@ -6,7 +6,7 @@
@Data NVARCHAR(MAX), @Data NVARCHAR(MAX),
@Favorites NVARCHAR(MAX), @Favorites NVARCHAR(MAX),
@Folders NVARCHAR(MAX), @Folders NVARCHAR(MAX),
@Attachments NVARCHAR(MAX), @Attachments NVARCHAR(MAX), -- not used
@CreationDate DATETIME2(7), @CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7), @RevisionDate DATETIME2(7),
@DeletedDate DATETIME2(7), @DeletedDate DATETIME2(7),
@ -25,7 +25,6 @@ BEGIN
[Data], [Data],
[Favorites], [Favorites],
[Folders], [Folders],
[Attachments],
[CreationDate], [CreationDate],
[RevisionDate], [RevisionDate],
[DeletedDate], [DeletedDate],
@ -41,7 +40,6 @@ BEGIN
@Data, @Data,
@Favorites, @Favorites,
@Folders, @Folders,
@Attachments,
@CreationDate, @CreationDate,
@RevisionDate, @RevisionDate,
@DeletedDate, @DeletedDate,

View File

@ -10,20 +10,59 @@ BEGIN
DECLARE @UserId UNIQUEIDENTIFIER DECLARE @UserId UNIQUEIDENTIFIER
DECLARE @OrganizationId UNIQUEIDENTIFIER DECLARE @OrganizationId UNIQUEIDENTIFIER
DECLARE @CurrentAttachments NVARCHAR(MAX)
DECLARE @NewAttachments NVARCHAR(MAX)
-- Get current cipher data
SELECT SELECT
@UserId = [UserId], @UserId = [UserId],
@OrganizationId = [OrganizationId] @OrganizationId = [OrganizationId],
@CurrentAttachments = [Attachments]
FROM FROM
[dbo].[Cipher] [dbo].[Cipher]
WHERE [Id] = @Id WHERE [Id] = @Id
UPDATE -- If there are no attachments, nothing to do
[dbo].[Cipher] IF @CurrentAttachments IS NULL
SET BEGIN
[Attachments] = JSON_MODIFY([Attachments], @AttachmentIdPath, NULL) RETURN;
WHERE END
[Id] = @Id
-- Validate the initial JSON
IF ISJSON(@CurrentAttachments) = 0
BEGIN
THROW 50000, 'Current initial attachments data is not valid JSON', 1;
RETURN;
END
-- Check if the attachment exists before trying to remove it
IF JSON_PATH_EXISTS(@CurrentAttachments, @AttachmentIdPath) = 0
BEGIN
-- Attachment doesn't exist, nothing to do
RETURN;
END
-- Create the new attachments JSON with the specified attachment removed
SET @NewAttachments = JSON_MODIFY(@CurrentAttachments, @AttachmentIdPath, NULL)
-- Validate the resulting JSON
IF ISJSON(@NewAttachments) = 0
BEGIN
THROW 50000, 'Failed to create valid JSON when removing attachment', 1;
RETURN;
END
-- Check if we've removed all attachments and have an empty object
IF @NewAttachments = '{}'
BEGIN
-- If we have an empty JSON object, set to NULL instead
SET @NewAttachments = NULL;
END
-- Update with validated JSON
UPDATE [dbo].[Cipher]
SET [Attachments] = @NewAttachments
WHERE [Id] = @Id
IF @OrganizationId IS NOT NULL IF @OrganizationId IS NOT NULL
BEGIN BEGIN

View File

@ -6,7 +6,7 @@
@Data NVARCHAR(MAX), @Data NVARCHAR(MAX),
@Favorites NVARCHAR(MAX), @Favorites NVARCHAR(MAX),
@Folders NVARCHAR(MAX), @Folders NVARCHAR(MAX),
@Attachments NVARCHAR(MAX), @Attachments NVARCHAR(MAX), -- not used
@CreationDate DATETIME2(7), @CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7), @RevisionDate DATETIME2(7),
@DeletedDate DATETIME2(7), @DeletedDate DATETIME2(7),
@ -25,7 +25,6 @@ BEGIN
[Data] = @Data, [Data] = @Data,
[Favorites] = @Favorites, [Favorites] = @Favorites,
[Folders] = @Folders, [Folders] = @Folders,
[Attachments] = @Attachments,
[CreationDate] = @CreationDate, [CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate, [RevisionDate] = @RevisionDate,
[DeletedDate] = @DeletedDate, [DeletedDate] = @DeletedDate,

View File

@ -8,21 +8,75 @@ AS
BEGIN BEGIN
SET NOCOUNT ON SET NOCOUNT ON
-- Validate that AttachmentData is valid JSON
IF ISJSON(@AttachmentData) = 0
BEGIN
THROW 50000, 'Invalid JSON format in AttachmentData parameter', 1;
RETURN;
END
-- Validate that AttachmentData has the expected structure
-- Check for required fields
IF JSON_VALUE(@AttachmentData, '$.FileName') IS NULL OR
JSON_VALUE(@AttachmentData, '$.Size') IS NULL
BEGIN
THROW 50000, 'AttachmentData is missing required fields (FileName, Size)', 1;
RETURN;
END
-- Validate data types for critical fields
DECLARE @Size BIGINT = TRY_CAST(JSON_VALUE(@AttachmentData, '$.Size') AS BIGINT)
IF @Size IS NULL OR @Size <= 0
BEGIN
THROW 50000, 'AttachmentData has invalid Size value', 1;
RETURN;
END
DECLARE @AttachmentIdKey VARCHAR(50) = CONCAT('"', @AttachmentId, '"') DECLARE @AttachmentIdKey VARCHAR(50) = CONCAT('"', @AttachmentId, '"')
DECLARE @AttachmentIdPath VARCHAR(50) = CONCAT('$.', @AttachmentIdKey) DECLARE @AttachmentIdPath VARCHAR(50) = CONCAT('$.', @AttachmentIdKey)
DECLARE @NewAttachments NVARCHAR(MAX)
UPDATE -- Get current attachments
[dbo].[Cipher] DECLARE @CurrentAttachments NVARCHAR(MAX)
SET SELECT @CurrentAttachments = [Attachments] FROM [dbo].[Cipher] WHERE [Id] = @Id
[Attachments] =
CASE -- Prepare the new attachments value based on current state
WHEN [Attachments] IS NULL THEN IF @CurrentAttachments IS NULL
CONCAT('{', @AttachmentIdKey, ':', @AttachmentData, '}') BEGIN
ELSE -- Create new JSON object with the attachment
JSON_MODIFY([Attachments], @AttachmentIdPath, JSON_QUERY(@AttachmentData, '$')) SET @NewAttachments = CONCAT('{', @AttachmentIdKey, ':', @AttachmentData, '}')
-- Validate the constructed JSON
IF ISJSON(@NewAttachments) = 0
BEGIN
THROW 50000, 'Failed to create valid JSON when adding new attachment', 1;
RETURN;
END END
WHERE END
[Id] = @Id ELSE
BEGIN
-- Validate existing attachments
IF ISJSON(@CurrentAttachments) = 0
BEGIN
THROW 50000, 'Current attachments data is not valid JSON', 1;
RETURN;
END
-- Modify existing JSON
SET @NewAttachments = JSON_MODIFY(@CurrentAttachments, @AttachmentIdPath, JSON_QUERY(@AttachmentData, '$'))
-- Validate the modified JSON
IF ISJSON(@NewAttachments) = 0
BEGIN
THROW 50000, 'Failed to create valid JSON when updating existing attachments', 1;
RETURN;
END
END
-- Update with validated JSON
UPDATE [dbo].[Cipher]
SET [Attachments] = @NewAttachments
WHERE [Id] = @Id
IF @OrganizationId IS NOT NULL IF @OrganizationId IS NOT NULL
BEGIN BEGIN

View File

@ -5,10 +5,33 @@ BEGIN
SET NOCOUNT ON SET NOCOUNT ON
SELECT SELECT
COUNT(1) (
FROM -- Count organization users
[dbo].[OrganizationUserView] SELECT COUNT(1)
WHERE FROM [dbo].[OrganizationUserView]
OrganizationId = @OrganizationId WHERE OrganizationId = @OrganizationId
AND Status >= 0 --Invited AND Status >= 0 --Invited
) +
(
-- Count admin-initiated sponsorships towards the seat count
-- Introduced in https://bitwarden.atlassian.net/browse/PM-17772
SELECT COUNT(1)
FROM [dbo].[OrganizationSponsorship]
WHERE SponsoringOrganizationId = @OrganizationId
AND IsAdminInitiated = 1
AND (
-- Not marked for deletion - always count
(ToDelete = 0)
OR
-- Marked for deletion but has a valid until date in the future (RevokeWhenExpired status)
(ToDelete = 1 AND ValidUntil IS NOT NULL AND ValidUntil > GETUTCDATE())
)
AND (
-- SENT status: When SponsoredOrganizationId is null
SponsoredOrganizationId IS NULL
OR
-- ACCEPTED status: When SponsoredOrganizationId is not null and ValidUntil is null or in the future
(SponsoredOrganizationId IS NOT NULL AND (ValidUntil IS NULL OR ValidUntil > GETUTCDATE()))
)
)
END END

View File

@ -6,7 +6,7 @@ CREATE TABLE [dbo].[PasswordHealthReportApplication]
CreationDate DATETIME2(7) NOT NULL, CreationDate DATETIME2(7) NOT NULL,
RevisionDate DATETIME2(7) NOT NULL, RevisionDate DATETIME2(7) NOT NULL,
CONSTRAINT [PK_PasswordHealthReportApplication] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [PK_PasswordHealthReportApplication] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_PasswordHealthReportApplication_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]), CONSTRAINT [FK_PasswordHealthReportApplication_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE
); );
GO GO

View File

@ -1,9 +1,6 @@
using System.Security.Claims; using System.Security.Claims;
using System.Text.Json;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute; using NSubstitute;
using Xunit; using Xunit;
@ -12,22 +9,6 @@ namespace Bit.Core.Test.Models.Business;
public class OrganizationLicenseTests public class OrganizationLicenseTests
{ {
/// <summary>
/// Verifies that when the license file is loaded from disk using the current OrganizationLicense class,
/// its hash does not change.
/// This guards against the risk that properties added in later versions are accidentally included in the hash,
/// or that a property is added without incrementing the version number.
/// </summary>
[Theory]
[BitAutoData(OrganizationLicense.CurrentLicenseFileVersion)] // Previous version (this property is 1 behind)
[BitAutoData(OrganizationLicense.CurrentLicenseFileVersion + 1)] // Current version
public void OrganizationLicense_LoadFromDisk_HashDoesNotChange(int licenseVersion)
{
var license = OrganizationLicenseFileFixtures.GetVersion(licenseVersion);
// Compare the hash loaded from the json to the hash generated by the current class
Assert.Equal(Convert.FromBase64String(license.Hash), license.ComputeHash());
}
/// <summary> /// <summary>
/// Verifies that when the license file is loaded from disk using the current OrganizationLicense class, /// Verifies that when the license file is loaded from disk using the current OrganizationLicense class,
@ -52,22 +33,4 @@ public class OrganizationLicenseTests
}); });
Assert.True(license.VerifyData(organization, claimsPrincipal, globalSettings)); Assert.True(license.VerifyData(organization, claimsPrincipal, globalSettings));
} }
/// <summary>
/// Helper used to generate a new json string to be added in OrganizationLicenseFileFixtures.
/// Uncomment [Fact], run the test and copy the value of the `result` variable into OrganizationLicenseFileFixtures,
/// following the instructions in that class.
/// </summary>
// [Fact]
private void GenerateLicenseFileJsonString()
{
var organization = OrganizationLicenseFileFixtures.OrganizationFactory();
var licensingService = Substitute.For<ILicensingService>();
var installationId = new Guid(OrganizationLicenseFileFixtures.InstallationId);
var license = new OrganizationLicense(organization, null, installationId, licensingService);
var result = JsonSerializer.Serialize(license, JsonHelpers.Indented).Replace("\"", "'");
// Put a break after this line, then copy and paste the value of `result` into OrganizationLicenseFileFixtures
}
} }

View File

@ -41,7 +41,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).ReturnsNull(); sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).ReturnsNull();
var exception = await Assert.ThrowsAsync<BadRequestException>(() => var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CreateSponsorshipAsync(null, orgUser, PlanSponsorshipType.FamiliesForEnterprise, default, default, null)); sutProvider.Sut.CreateSponsorshipAsync(null, orgUser, PlanSponsorshipType.FamiliesForEnterprise, default, default, false, null));
Assert.Contains("Cannot offer a Families Organization Sponsorship to yourself. Choose a different email.", exception.Message); Assert.Contains("Cannot offer a Families Organization Sponsorship to yourself. Choose a different email.", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs() await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()
@ -55,7 +55,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).Returns(user); sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).Returns(user);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CreateSponsorshipAsync(null, orgUser, PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, default, null)); sutProvider.Sut.CreateSponsorshipAsync(null, orgUser, PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, default, false, null));
Assert.Contains("Cannot offer a Families Organization Sponsorship to yourself. Choose a different email.", exception.Message); Assert.Contains("Cannot offer a Families Organization Sponsorship to yourself. Choose a different email.", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs() await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()
@ -72,7 +72,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).Returns(user); sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).Returns(user);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CreateSponsorshipAsync(org, orgUser, PlanSponsorshipType.FamiliesForEnterprise, default, default, null)); sutProvider.Sut.CreateSponsorshipAsync(org, orgUser, PlanSponsorshipType.FamiliesForEnterprise, default, default, false, null));
Assert.Contains("Specified Organization cannot sponsor other organizations.", exception.Message); Assert.Contains("Specified Organization cannot sponsor other organizations.", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs() await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()
@ -91,7 +91,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).Returns(user); sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).Returns(user);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CreateSponsorshipAsync(org, orgUser, PlanSponsorshipType.FamiliesForEnterprise, default, default, null)); sutProvider.Sut.CreateSponsorshipAsync(org, orgUser, PlanSponsorshipType.FamiliesForEnterprise, default, default, false, null));
Assert.Contains("Only confirmed users can sponsor other organizations.", exception.Message); Assert.Contains("Only confirmed users can sponsor other organizations.", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs() await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()
@ -115,7 +115,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(orgUser.UserId.Value); sutProvider.GetDependency<ICurrentContext>().UserId.Returns(orgUser.UserId.Value);
var exception = await Assert.ThrowsAsync<BadRequestException>(() => var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CreateSponsorshipAsync(org, orgUser, sponsorship.PlanSponsorshipType!.Value, null, null, null)); sutProvider.Sut.CreateSponsorshipAsync(org, orgUser, sponsorship.PlanSponsorshipType!.Value, null, null, false, null));
Assert.Contains("Can only sponsor one organization per Organization User.", exception.Message); Assert.Contains("Can only sponsor one organization per Organization User.", exception.Message);
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs() await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()
@ -147,7 +147,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
var actual = await Assert.ThrowsAsync<BadRequestException>(async () => var actual = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, null)); await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, false, null));
Assert.Equal("Only confirmed users can sponsor other organizations.", actual.Message); Assert.Equal("Only confirmed users can sponsor other organizations.", actual.Message);
} }
@ -170,7 +170,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, null); PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, false, null);
var expectedSponsorship = new OrganizationSponsorship var expectedSponsorship = new OrganizationSponsorship
{ {
@ -209,7 +209,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
var actualException = await Assert.ThrowsAsync<Exception>(() => var actualException = await Assert.ThrowsAsync<Exception>(() =>
sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, null)); PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, false, null));
Assert.Same(expectedException, actualException); Assert.Same(expectedException, actualException);
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1) await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1)
@ -244,9 +244,9 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
var actual = await Assert.ThrowsAsync<UnauthorizedAccessException>(async () => var actual = await Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>
await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, notes: null)); PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, null));
Assert.Equal("You do not have permissions to send sponsorships on behalf of the organization.", actual.Message); Assert.Equal("You do not have permissions to send sponsorships on behalf of the organization", actual.Message);
} }
[Theory] [Theory]
@ -278,9 +278,9 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
var actual = await Assert.ThrowsAsync<UnauthorizedAccessException>(async () => var actual = await Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>
await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, notes: null)); PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, null));
Assert.Equal("You do not have permissions to send sponsorships on behalf of the organization.", actual.Message); Assert.Equal("You do not have permissions to send sponsorships on behalf of the organization", actual.Message);
} }
[Theory] [Theory]
@ -312,7 +312,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
]); ]);
var actual = await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser, var actual = await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, notes); PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, notes);
var expectedSponsorship = new OrganizationSponsorship var expectedSponsorship = new OrganizationSponsorship
@ -330,6 +330,6 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
Assert.True(SponsorshipValidator(expectedSponsorship, actual)); Assert.True(SponsorshipValidator(expectedSponsorship, actual));
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1) await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1)
.UpsertAsync(Arg.Is<OrganizationSponsorship>(s => SponsorshipValidator(s, expectedSponsorship))); .CreateAsync(Arg.Is<OrganizationSponsorship>(s => SponsorshipValidator(s, expectedSponsorship)));
} }
} }

View File

@ -1,6 +1,7 @@
using System.Text.Json; using System.Text.Json;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
@ -912,4 +913,59 @@ public class CipherRepositoryTests
Assert.Equal(CipherType.SecureNote, updatedCipher1.Type); Assert.Equal(CipherType.SecureNote, updatedCipher1.Type);
Assert.Equal("new_attachments", updatedCipher2.Attachments); Assert.Equal("new_attachments", updatedCipher2.Attachments);
} }
[DatabaseTheory, DatabaseData]
public async Task DeleteCipherWithSecurityTaskAsync_Works(
IOrganizationRepository organizationRepository,
ICipherRepository cipherRepository,
ISecurityTaskRepository securityTaskRepository)
{
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = "Test Org",
PlanType = PlanType.EnterpriseAnnually,
Plan = "Test Plan",
BillingEmail = ""
});
var cipher1 = new Cipher { Type = CipherType.Login, OrganizationId = organization.Id, Data = "", };
await cipherRepository.CreateAsync(cipher1);
var cipher2 = new Cipher { Type = CipherType.Login, OrganizationId = organization.Id, Data = "", };
await cipherRepository.CreateAsync(cipher2);
var tasks = new List<SecurityTask>
{
new()
{
OrganizationId = organization.Id,
CipherId = cipher1.Id,
Status = SecurityTaskStatus.Pending,
Type = SecurityTaskType.UpdateAtRiskCredential,
},
new()
{
OrganizationId = organization.Id,
CipherId = cipher2.Id,
Status = SecurityTaskStatus.Completed,
Type = SecurityTaskType.UpdateAtRiskCredential,
}
};
await securityTaskRepository.CreateManyAsync(tasks);
// Delete cipher with pending security task
await cipherRepository.DeleteAsync(cipher1);
var deletedCipher1 = await cipherRepository.GetByIdAsync(cipher1.Id);
Assert.Null(deletedCipher1);
// Delete cipher with completed security task
await cipherRepository.DeleteAsync(cipher2);
var deletedCipher2 = await cipherRepository.GetByIdAsync(cipher2.Id);
Assert.Null(deletedCipher2);
}
} }

View File

@ -0,0 +1,11 @@
BEGIN
IF EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = 'FK_Notification_SecurityTask')
BEGIN
ALTER TABLE [dbo].[Notification]
DROP CONSTRAINT [FK_Notification_SecurityTask]
END
ALTER TABLE [dbo].[Notification]
ADD CONSTRAINT [FK_Notification_SecurityTask] FOREIGN KEY ([TaskId]) REFERENCES [dbo].[SecurityTask] ([Id]) ON DELETE CASCADE
END
GO

View File

@ -0,0 +1,350 @@
CREATE OR ALTER PROCEDURE [dbo].[Cipher_UpdateAttachment]
@Id UNIQUEIDENTIFIER,
@UserId UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
@AttachmentId VARCHAR(50),
@AttachmentData NVARCHAR(MAX)
AS
BEGIN
SET NOCOUNT ON
-- Validate that AttachmentData is valid JSON
IF ISJSON(@AttachmentData) = 0
BEGIN
THROW 50000, 'Invalid JSON format in AttachmentData parameter', 1;
RETURN;
END
-- Validate that AttachmentData has the expected structure
-- Check for required fields
IF JSON_VALUE(@AttachmentData, '$.FileName') IS NULL OR
JSON_VALUE(@AttachmentData, '$.Size') IS NULL
BEGIN
THROW 50000, 'AttachmentData is missing required fields (FileName, Size)', 1;
RETURN;
END
-- Validate data types for critical fields
DECLARE @Size BIGINT = TRY_CAST(JSON_VALUE(@AttachmentData, '$.Size') AS BIGINT)
IF @Size IS NULL OR @Size <= 0
BEGIN
THROW 50000, 'AttachmentData has invalid Size value', 1;
RETURN;
END
DECLARE @AttachmentIdKey VARCHAR(50) = CONCAT('"', @AttachmentId, '"')
DECLARE @AttachmentIdPath VARCHAR(50) = CONCAT('$.', @AttachmentIdKey)
DECLARE @NewAttachments NVARCHAR(MAX)
-- Get current attachments
DECLARE @CurrentAttachments NVARCHAR(MAX)
SELECT @CurrentAttachments = [Attachments] FROM [dbo].[Cipher] WHERE [Id] = @Id
-- Prepare the new attachments value based on current state
IF @CurrentAttachments IS NULL
BEGIN
-- Create new JSON object with the attachment
SET @NewAttachments = CONCAT('{', @AttachmentIdKey, ':', @AttachmentData, '}')
-- Validate the constructed JSON
IF ISJSON(@NewAttachments) = 0
BEGIN
THROW 50000, 'Failed to create valid JSON when adding new attachment', 1;
RETURN;
END
END
ELSE
BEGIN
-- Validate existing attachments
IF ISJSON(@CurrentAttachments) = 0
BEGIN
THROW 50000, 'Current attachments data is not valid JSON', 1;
RETURN;
END
-- Modify existing JSON
SET @NewAttachments = JSON_MODIFY(@CurrentAttachments, @AttachmentIdPath, JSON_QUERY(@AttachmentData, '$'))
-- Validate the modified JSON
IF ISJSON(@NewAttachments) = 0
BEGIN
THROW 50000, 'Failed to create valid JSON when updating existing attachments', 1;
RETURN;
END
END
-- Update with validated JSON
UPDATE [dbo].[Cipher]
SET [Attachments] = @NewAttachments
WHERE [Id] = @Id
IF @OrganizationId IS NOT NULL
BEGIN
EXEC [dbo].[Organization_UpdateStorage] @OrganizationId
EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId
END
ELSE IF @UserId IS NOT NULL
BEGIN
EXEC [dbo].[User_UpdateStorage] @UserId
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
END
END
GO
CREATE OR ALTER PROCEDURE [dbo].[Cipher_DeleteAttachment]
@Id UNIQUEIDENTIFIER,
@AttachmentId VARCHAR(50)
AS
BEGIN
SET NOCOUNT ON
DECLARE @AttachmentIdKey VARCHAR(50) = CONCAT('"', @AttachmentId, '"')
DECLARE @AttachmentIdPath VARCHAR(50) = CONCAT('$.', @AttachmentIdKey)
DECLARE @UserId UNIQUEIDENTIFIER
DECLARE @OrganizationId UNIQUEIDENTIFIER
DECLARE @CurrentAttachments NVARCHAR(MAX)
DECLARE @NewAttachments NVARCHAR(MAX)
-- Get current cipher data
SELECT
@UserId = [UserId],
@OrganizationId = [OrganizationId],
@CurrentAttachments = [Attachments]
FROM
[dbo].[Cipher]
WHERE [Id] = @Id
-- If there are no attachments, nothing to do
IF @CurrentAttachments IS NULL
BEGIN
RETURN;
END
-- Validate the initial JSON
IF ISJSON(@CurrentAttachments) = 0
BEGIN
THROW 50000, 'Current initial attachments data is not valid JSON', 1;
RETURN;
END
-- Check if the attachment exists before trying to remove it
IF JSON_PATH_EXISTS(@CurrentAttachments, @AttachmentIdPath) = 0
BEGIN
-- Attachment doesn't exist, nothing to do
RETURN;
END
-- Create the new attachments JSON with the specified attachment removed
SET @NewAttachments = JSON_MODIFY(@CurrentAttachments, @AttachmentIdPath, NULL)
-- Validate the resulting JSON
IF ISJSON(@NewAttachments) = 0
BEGIN
THROW 50000, 'Failed to create valid JSON when removing attachment', 1;
RETURN;
END
-- Check if we've removed all attachments and have an empty object
IF @NewAttachments = '{}'
BEGIN
-- If we have an empty JSON object, set to NULL instead
SET @NewAttachments = NULL;
END
-- Update with validated JSON
UPDATE [dbo].[Cipher]
SET [Attachments] = @NewAttachments
WHERE [Id] = @Id
IF @OrganizationId IS NOT NULL
BEGIN
EXEC [dbo].[Organization_UpdateStorage] @OrganizationId
EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId
END
ELSE IF @UserId IS NOT NULL
BEGIN
EXEC [dbo].[User_UpdateStorage] @UserId
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
END
END
GO
-- Remove [Attachments] assignment from Cipher_Create, Cipher_Update, and CipherDetails_Update procedures
CREATE OR ALTER PROCEDURE [dbo].[Cipher_Create]
@Id UNIQUEIDENTIFIER OUTPUT,
@UserId UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
@Type TINYINT,
@Data NVARCHAR(MAX),
@Favorites NVARCHAR(MAX),
@Folders NVARCHAR(MAX),
@Attachments NVARCHAR(MAX), -- not used
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@DeletedDate DATETIME2(7),
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL
AS
BEGIN
SET NOCOUNT ON
INSERT INTO [dbo].[Cipher]
(
[Id],
[UserId],
[OrganizationId],
[Type],
[Data],
[Favorites],
[Folders],
[CreationDate],
[RevisionDate],
[DeletedDate],
[Reprompt],
[Key]
)
VALUES
(
@Id,
CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END,
@OrganizationId,
@Type,
@Data,
@Favorites,
@Folders,
@CreationDate,
@RevisionDate,
@DeletedDate,
@Reprompt,
@Key
)
IF @OrganizationId IS NOT NULL
BEGIN
EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId
END
ELSE IF @UserId IS NOT NULL
BEGIN
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
END
END
GO
CREATE OR ALTER PROCEDURE [dbo].[CipherDetails_Update]
@Id UNIQUEIDENTIFIER,
@UserId UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
@Type TINYINT,
@Data NVARCHAR(MAX),
@Favorites NVARCHAR(MAX), -- not used
@Folders NVARCHAR(MAX), -- not used
@Attachments NVARCHAR(MAX), -- not used
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@FolderId UNIQUEIDENTIFIER,
@Favorite BIT,
@Edit BIT, -- not used
@ViewPassword BIT, -- not used
@Manage BIT, -- not used
@OrganizationUseTotp BIT, -- not used
@DeletedDate DATETIME2(2),
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL
AS
BEGIN
SET NOCOUNT ON
DECLARE @UserIdKey VARCHAR(50) = CONCAT('"', @UserId, '"')
DECLARE @UserIdPath VARCHAR(50) = CONCAT('$.', @UserIdKey)
UPDATE
[dbo].[Cipher]
SET
[UserId] = CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END,
[OrganizationId] = @OrganizationId,
[Type] = @Type,
[Data] = @Data,
[Folders] =
CASE
WHEN @FolderId IS NOT NULL AND [Folders] IS NULL THEN
CONCAT('{', @UserIdKey, ':"', @FolderId, '"', '}')
WHEN @FolderId IS NOT NULL THEN
JSON_MODIFY([Folders], @UserIdPath, CAST(@FolderId AS VARCHAR(50)))
ELSE
JSON_MODIFY([Folders], @UserIdPath, NULL)
END,
[Favorites] =
CASE
WHEN @Favorite = 1 AND [Favorites] IS NULL THEN
CONCAT('{', @UserIdKey, ':true}')
WHEN @Favorite = 1 THEN
JSON_MODIFY([Favorites], @UserIdPath, CAST(1 AS BIT))
ELSE
JSON_MODIFY([Favorites], @UserIdPath, NULL)
END,
[Reprompt] = @Reprompt,
[CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate,
[DeletedDate] = @DeletedDate,
[Key] = @Key
WHERE
[Id] = @Id
IF @OrganizationId IS NOT NULL
BEGIN
EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId
END
ELSE IF @UserId IS NOT NULL
BEGIN
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
END
END
GO
CREATE OR ALTER PROCEDURE [dbo].[Cipher_Update]
@Id UNIQUEIDENTIFIER,
@UserId UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
@Type TINYINT,
@Data NVARCHAR(MAX),
@Favorites NVARCHAR(MAX),
@Folders NVARCHAR(MAX),
@Attachments NVARCHAR(MAX), -- not used
@CreationDate DATETIME2(7),
@RevisionDate DATETIME2(7),
@DeletedDate DATETIME2(7),
@Reprompt TINYINT,
@Key VARCHAR(MAX) = NULL
AS
BEGIN
SET NOCOUNT ON
UPDATE
[dbo].[Cipher]
SET
[UserId] = CASE WHEN @OrganizationId IS NULL THEN @UserId ELSE NULL END,
[OrganizationId] = @OrganizationId,
[Type] = @Type,
[Data] = @Data,
[Favorites] = @Favorites,
[Folders] = @Folders,
[CreationDate] = @CreationDate,
[RevisionDate] = @RevisionDate,
[DeletedDate] = @DeletedDate,
[Reprompt] = @Reprompt,
[Key] = @Key
WHERE
[Id] = @Id
IF @OrganizationId IS NOT NULL
BEGIN
EXEC [dbo].[User_BumpAccountRevisionDateByCipherId] @Id, @OrganizationId
END
ELSE IF @UserId IS NOT NULL
BEGIN
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
END
END
GO

View File

@ -0,0 +1,11 @@
BEGIN
IF EXISTS (SELECT 1 FROM sys.foreign_keys WHERE name = 'FK_PasswordHealthReportApplication_Organization')
BEGIN
ALTER TABLE [dbo].[PasswordHealthReportApplication]
DROP CONSTRAINT [FK_PasswordHealthReportApplication_Organization]
END
ALTER TABLE [dbo].[PasswordHealthReportApplication]
ADD CONSTRAINT [FK_PasswordHealthReportApplication_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]) ON DELETE CASCADE
END
GO

View File

@ -0,0 +1,32 @@
-- Update OrganizationUser_ReadOccupiedSeatCountByOrganizationId to include admin-initiated sponsorships
-- Based on https://bitwarden.atlassian.net/browse/PM-17772
IF OBJECT_ID('[dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]
END
GO
CREATE PROCEDURE [dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]
@OrganizationId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
(
-- Count organization users
SELECT COUNT(1)
FROM [dbo].[OrganizationUserView]
WHERE OrganizationId = @OrganizationId
AND Status >= 0 --Invited
) +
(
-- Count admin-initiated sponsorships towards the seat count
-- Introduced in https://bitwarden.atlassian.net/browse/PM-17772
SELECT COUNT(1)
FROM [dbo].[OrganizationSponsorship]
WHERE SponsoringOrganizationId = @OrganizationId
AND IsAdminInitiated = 1
)
END
GO

View File

@ -0,0 +1,41 @@
IF OBJECT_ID('[dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]') IS NOT NULL
BEGIN
DROP PROCEDURE [dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]
END
GO
CREATE PROCEDURE [dbo].[OrganizationUser_ReadOccupiedSeatCountByOrganizationId]
@OrganizationId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
(
SELECT COUNT(1)
FROM [dbo].[OrganizationUserView]
WHERE OrganizationId = @OrganizationId
AND Status >= 0 --Invited
) +
(
SELECT COUNT(1)
FROM [dbo].[OrganizationSponsorship]
WHERE SponsoringOrganizationId = @OrganizationId
AND IsAdminInitiated = 1
AND (
-- Not marked for deletion - always count
(ToDelete = 0)
OR
-- Marked for deletion but has a valid until date in the future (RevokeWhenExpired status)
(ToDelete = 1 AND ValidUntil IS NOT NULL AND ValidUntil > GETUTCDATE())
)
AND (
-- SENT status: When SponsoredOrganizationId is null
SponsoredOrganizationId IS NULL
OR
-- ACCEPTED status: When SponsoredOrganizationId is not null and ValidUntil is null or in the future
(SponsoredOrganizationId IS NOT NULL AND (ValidUntil IS NULL OR ValidUntil > GETUTCDATE()))
)
)
END
GO

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.MySqlMigrations.Migrations;
/// <inheritdoc />
public partial class NotificationCascadeDelete : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Notification_SecurityTask_TaskId",
table: "Notification");
migrationBuilder.AddForeignKey(
name: "FK_Notification_SecurityTask_TaskId",
table: "Notification",
column: "TaskId",
principalTable: "SecurityTask",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Notification_SecurityTask_TaskId",
table: "Notification");
migrationBuilder.AddForeignKey(
name: "FK_Notification_SecurityTask_TaskId",
table: "Notification",
column: "TaskId",
principalTable: "SecurityTask",
principalColumn: "Id");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.MySqlMigrations.Migrations;
/// <inheritdoc />
public partial class SecurityTaskCascadeDelete : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_SecurityTask_Cipher_CipherId",
table: "SecurityTask");
migrationBuilder.AddForeignKey(
name: "FK_SecurityTask_Cipher_CipherId",
table: "SecurityTask",
column: "CipherId",
principalTable: "Cipher",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_SecurityTask_Cipher_CipherId",
table: "SecurityTask");
migrationBuilder.AddForeignKey(
name: "FK_SecurityTask_Cipher_CipherId",
table: "SecurityTask",
column: "CipherId",
principalTable: "Cipher",
principalColumn: "Id");
}
}

View File

@ -2737,7 +2737,8 @@ namespace Bit.MySqlMigrations.Migrations
b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task")
.WithMany() .WithMany()
.HasForeignKey("TaskId"); .HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User")
.WithMany() .WithMany()
@ -2852,7 +2853,8 @@ namespace Bit.MySqlMigrations.Migrations
{ {
b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher")
.WithMany() .WithMany()
.HasForeignKey("CipherId"); .HasForeignKey("CipherId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
.WithMany() .WithMany()

View File

@ -10,7 +10,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="[8.0.8]">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.PostgresMigrations.Migrations;
/// <inheritdoc />
public partial class NotificationCascadeDelete : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Notification_SecurityTask_TaskId",
table: "Notification");
migrationBuilder.AddForeignKey(
name: "FK_Notification_SecurityTask_TaskId",
table: "Notification",
column: "TaskId",
principalTable: "SecurityTask",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Notification_SecurityTask_TaskId",
table: "Notification");
migrationBuilder.AddForeignKey(
name: "FK_Notification_SecurityTask_TaskId",
table: "Notification",
column: "TaskId",
principalTable: "SecurityTask",
principalColumn: "Id");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.PostgresMigrations.Migrations;
/// <inheritdoc />
public partial class SecurityTaskCascadeDelete : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_SecurityTask_Cipher_CipherId",
table: "SecurityTask");
migrationBuilder.AddForeignKey(
name: "FK_SecurityTask_Cipher_CipherId",
table: "SecurityTask",
column: "CipherId",
principalTable: "Cipher",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_SecurityTask_Cipher_CipherId",
table: "SecurityTask");
migrationBuilder.AddForeignKey(
name: "FK_SecurityTask_Cipher_CipherId",
table: "SecurityTask",
column: "CipherId",
principalTable: "Cipher",
principalColumn: "Id");
}
}

View File

@ -2743,7 +2743,8 @@ namespace Bit.PostgresMigrations.Migrations
b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task")
.WithMany() .WithMany()
.HasForeignKey("TaskId"); .HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User")
.WithMany() .WithMany()
@ -2858,7 +2859,8 @@ namespace Bit.PostgresMigrations.Migrations
{ {
b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher")
.WithMany() .WithMany()
.HasForeignKey("CipherId"); .HasForeignKey("CipherId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
.WithMany() .WithMany()

View File

@ -6,7 +6,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="[8.0.8]">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

View File

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="[8.0.8]">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.SqliteMigrations.Migrations;
/// <inheritdoc />
public partial class NotificationCascadeDelete : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Notification_SecurityTask_TaskId",
table: "Notification");
migrationBuilder.AddForeignKey(
name: "FK_Notification_SecurityTask_TaskId",
table: "Notification",
column: "TaskId",
principalTable: "SecurityTask",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_Notification_SecurityTask_TaskId",
table: "Notification");
migrationBuilder.AddForeignKey(
name: "FK_Notification_SecurityTask_TaskId",
table: "Notification",
column: "TaskId",
principalTable: "SecurityTask",
principalColumn: "Id");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,40 @@
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace Bit.SqliteMigrations.Migrations;
/// <inheritdoc />
public partial class SecurityTaskCascadeDelete : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_SecurityTask_Cipher_CipherId",
table: "SecurityTask");
migrationBuilder.AddForeignKey(
name: "FK_SecurityTask_Cipher_CipherId",
table: "SecurityTask",
column: "CipherId",
principalTable: "Cipher",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropForeignKey(
name: "FK_SecurityTask_Cipher_CipherId",
table: "SecurityTask");
migrationBuilder.AddForeignKey(
name: "FK_SecurityTask_Cipher_CipherId",
table: "SecurityTask",
column: "CipherId",
principalTable: "Cipher",
principalColumn: "Id");
}
}

View File

@ -2726,7 +2726,8 @@ namespace Bit.SqliteMigrations.Migrations
b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task") b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task")
.WithMany() .WithMany()
.HasForeignKey("TaskId"); .HasForeignKey("TaskId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User") b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User")
.WithMany() .WithMany()
@ -2841,7 +2842,8 @@ namespace Bit.SqliteMigrations.Migrations
{ {
b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher") b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher")
.WithMany() .WithMany()
.HasForeignKey("CipherId"); .HasForeignKey("CipherId")
.OnDelete(DeleteBehavior.Cascade);
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization") b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
.WithMany() .WithMany()

View File

@ -11,7 +11,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="8.0.8"> <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="[8.0.8]">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
</PackageReference> </PackageReference>