mirror of
https://github.com/bitwarden/server.git
synced 2025-05-03 18:52:22 -05:00
Merge branch 'main' into feat/pm-14496-non-root-self-hosted-images
This commit is contained in:
commit
761e794f7f
@ -110,9 +110,14 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
|
||||
IEnumerable<string> organizationOwnerEmails)
|
||||
{
|
||||
if (provider.IsBillable() &&
|
||||
organization.IsValidClient() &&
|
||||
!string.IsNullOrEmpty(organization.GatewayCustomerId))
|
||||
organization.IsValidClient())
|
||||
{
|
||||
// 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
|
||||
{
|
||||
Description = string.Empty,
|
||||
|
@ -21,7 +21,6 @@ using Bit.Core.Models.Business;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using CsvHelper;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -46,7 +45,6 @@ public class ProviderBillingService(
|
||||
[FromKeyedServices(AutomaticTaxFactory.BusinessUse)] IAutomaticTaxStrategy automaticTaxStrategy)
|
||||
: IProviderBillingService
|
||||
{
|
||||
[RequireFeature(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal)]
|
||||
public async Task AddExistingOrganization(
|
||||
Provider provider,
|
||||
Organization organization,
|
||||
@ -312,7 +310,6 @@ public class ProviderBillingService(
|
||||
return memoryStream.ToArray();
|
||||
}
|
||||
|
||||
[RequireFeature(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal)]
|
||||
public async Task<IEnumerable<AddableOrganization>> GetAddableOrganizations(
|
||||
Provider provider,
|
||||
Guid userId)
|
||||
|
54
bitwarden_license/src/Sso/package-lock.json
generated
54
bitwarden_license/src/Sso/package-lock.json
generated
@ -441,9 +441,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -455,9 +455,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz",
|
||||
"integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==",
|
||||
"version": "22.13.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz",
|
||||
"integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -687,9 +687,9 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
||||
"version": "8.14.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@ -821,9 +821,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001700",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz",
|
||||
"integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==",
|
||||
"version": "1.0.30001707",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz",
|
||||
"integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -975,9 +975,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.103",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.103.tgz",
|
||||
"integrity": "sha512-P6+XzIkfndgsrjROJWfSvVEgNHtPgbhVyTkwLjUM2HU/h7pZRORgaTlHqfAikqxKmdJMLW8fftrdGWbd/Ds0FA==",
|
||||
"version": "1.5.128",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz",
|
||||
"integrity": "sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@ -1248,9 +1248,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz",
|
||||
"integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==",
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz",
|
||||
"integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -1501,9 +1501,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -2107,9 +2107,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin": {
|
||||
"version": "5.3.11",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz",
|
||||
"integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==",
|
||||
"version": "5.3.14",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz",
|
||||
"integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -2163,9 +2163,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz",
|
||||
"integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==",
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -124,8 +124,20 @@ services:
|
||||
profiles:
|
||||
- servicebus
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
container_name: bw-redis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
command: redis-server --appendonly yes
|
||||
profiles:
|
||||
- redis
|
||||
|
||||
volumes:
|
||||
mssql_dev_data:
|
||||
postgres_dev_data:
|
||||
mysql_dev_data:
|
||||
rabbitmq_data:
|
||||
redis_data:
|
||||
|
@ -470,6 +470,19 @@ public class ProvidersController : Controller
|
||||
[RequirePermission(Permission.Provider_Edit)]
|
||||
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))
|
||||
{
|
||||
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");
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
return BadRequest("Invalid provider name");
|
||||
|
@ -183,17 +183,29 @@
|
||||
<div class="p-3">
|
||||
<h4 class="fw-bolder" id="exampleModalLabel">Delete provider</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<span class="fw-light">
|
||||
This action is permanent and irreversible. Enter the provider name to complete deletion of the provider and associated data.
|
||||
</span>
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label for="provider-name" class="col-form-label">Provider name</label>
|
||||
<input type="text" class="form-control" id="provider-name">
|
||||
</div>
|
||||
</form>
|
||||
</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">
|
||||
<span class="fw-light">
|
||||
This action is permanent and irreversible. Enter the provider name to complete deletion of the provider and associated data.
|
||||
</span>
|
||||
<form>
|
||||
<div class="mb-3">
|
||||
<label for="provider-name" class="col-form-label">Provider name</label>
|
||||
<input type="text" class="form-control" id="provider-name">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
<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-danger btn-pill" onclick="deleteProvider('@Model.Provider.Id');">Delete provider</button>
|
||||
|
54
src/Admin/package-lock.json
generated
54
src/Admin/package-lock.json
generated
@ -442,9 +442,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
|
||||
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -456,9 +456,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.13.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.5.tgz",
|
||||
"integrity": "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg==",
|
||||
"version": "22.13.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz",
|
||||
"integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -688,9 +688,9 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||
"integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==",
|
||||
"version": "8.14.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@ -822,9 +822,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001700",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz",
|
||||
"integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==",
|
||||
"version": "1.0.30001707",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz",
|
||||
"integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -976,9 +976,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.103",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.103.tgz",
|
||||
"integrity": "sha512-P6+XzIkfndgsrjROJWfSvVEgNHtPgbhVyTkwLjUM2HU/h7pZRORgaTlHqfAikqxKmdJMLW8fftrdGWbd/Ds0FA==",
|
||||
"version": "1.5.128",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz",
|
||||
"integrity": "sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@ -1249,9 +1249,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz",
|
||||
"integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==",
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz",
|
||||
"integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@ -1502,9 +1502,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.8",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
|
||||
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@ -2108,9 +2108,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/terser-webpack-plugin": {
|
||||
"version": "5.3.11",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz",
|
||||
"integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==",
|
||||
"version": "5.3.14",
|
||||
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz",
|
||||
"integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@ -2172,9 +2172,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz",
|
||||
"integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==",
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||
"integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -1,13 +1,16 @@
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Controllers;
|
||||
|
||||
[RequireFeature(FeatureFlagKeys.EventBasedOrganizationIntegrations)]
|
||||
[Route("organizations/{organizationId:guid}/integrations/{integrationId:guid}/configurations")]
|
||||
[Authorize("Application")]
|
||||
public class OrganizationIntegrationConfigurationController(
|
||||
|
@ -1,8 +1,10 @@
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@ -10,6 +12,7 @@ using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Controllers;
|
||||
|
||||
[RequireFeature(FeatureFlagKeys.EventBasedOrganizationIntegrations)]
|
||||
[Route("organizations/{organizationId:guid}/integrations")]
|
||||
[Authorize("Application")]
|
||||
public class OrganizationIntegrationController(
|
||||
|
@ -1,6 +1,5 @@
|
||||
using Bit.Api.Billing.Controllers;
|
||||
using Bit.Api.Billing.Models.Requests;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Billing.Services;
|
||||
@ -17,7 +16,6 @@ namespace Bit.Api.AdminConsole.Controllers;
|
||||
[Route("providers/{providerId:guid}/clients")]
|
||||
public class ProviderClientsController(
|
||||
ICurrentContext currentContext,
|
||||
IFeatureService featureService,
|
||||
ILogger<BaseProviderController> logger,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IProviderBillingService providerBillingService,
|
||||
@ -140,11 +138,6 @@ public class ProviderClientsController(
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<IResult> GetAddableOrganizationsAsync([FromRoute] Guid providerId)
|
||||
{
|
||||
if (!featureService.IsEnabled(FeatureFlagKeys.P15179_AddExistingOrgsFromProviderPortal))
|
||||
{
|
||||
return Error.NotFound();
|
||||
}
|
||||
|
||||
var (provider, result) = await TryGetBillableProviderForServiceUserOperation(providerId);
|
||||
|
||||
if (provider == null)
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
@ -7,11 +8,13 @@ using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Integrations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Controllers;
|
||||
|
||||
[RequireFeature(FeatureFlagKeys.EventBasedOrganizationIntegrations)]
|
||||
[Route("organizations/{organizationId:guid}/integrations/slack")]
|
||||
[Authorize("Application")]
|
||||
public class SlackIntegrationController(
|
||||
|
@ -221,8 +221,7 @@ public class MembersController : Controller
|
||||
/// Remove a member.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Permanently removes a member from the organization. This cannot be undone.
|
||||
/// The user account will still remain. The user is only removed from the organization.
|
||||
/// Removes a member from the organization. This cannot be undone. The user account will still remain.
|
||||
/// </remarks>
|
||||
/// <param name="id">The identifier of the member to be removed.</param>
|
||||
[HttpDelete("{id}")]
|
||||
|
@ -86,9 +86,9 @@ public class OrganizationSponsorshipsController : Controller
|
||||
|
||||
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))
|
||||
@ -97,13 +97,13 @@ public class OrganizationSponsorshipsController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
var targetUser = model.SponsoringUserId ?? _currentContext.UserId!.Value;
|
||||
var sponsorship = await _createSponsorshipCommand.CreateSponsorshipAsync(
|
||||
sponsoringOrg,
|
||||
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, targetUser),
|
||||
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default),
|
||||
model.PlanSponsorshipType,
|
||||
model.SponsoredEmail,
|
||||
model.FriendlyName,
|
||||
model.IsAdminInitiated.GetValueOrDefault(),
|
||||
model.Notes);
|
||||
await _sendSponsorshipOfferCommand.SendSponsorshipOfferAsync(sponsorship, sponsoringOrg.Name);
|
||||
}
|
||||
|
34
src/Api/Controllers/PhishingDomainsController.cs
Normal file
34
src/Api/Controllers/PhishingDomainsController.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -47,9 +47,9 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
|
||||
{
|
||||
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))
|
||||
@ -60,8 +60,12 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
|
||||
|
||||
await _offerSponsorshipCommand.CreateSponsorshipAsync(
|
||||
await _organizationRepository.GetByIdAsync(sponsoringOrgId),
|
||||
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, model.SponsoringUserId ?? _currentContext.UserId ?? default),
|
||||
model.PlanSponsorshipType, model.SponsoredEmail, model.FriendlyName, model.Notes);
|
||||
await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default),
|
||||
model.PlanSponsorshipType,
|
||||
model.SponsoredEmail,
|
||||
model.FriendlyName,
|
||||
model.IsAdminInitiated.GetValueOrDefault(),
|
||||
model.Notes);
|
||||
}
|
||||
|
||||
[HttpDelete("{sponsoringOrgId}")]
|
||||
|
@ -58,6 +58,13 @@ public class JobsHostedService : BaseJobsHostedService
|
||||
.StartNow()
|
||||
.WithCronSchedule("0 0 * * * ?")
|
||||
.Build();
|
||||
var updatePhishingDomainsTrigger = TriggerBuilder.Create()
|
||||
.WithIdentity("UpdatePhishingDomainsTrigger")
|
||||
.StartNow()
|
||||
.WithSimpleSchedule(x => x
|
||||
.WithIntervalInHours(24)
|
||||
.RepeatForever())
|
||||
.Build();
|
||||
|
||||
|
||||
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(ValidateOrganizationsJob), everyTwelfthHourAndThirtyMinutesTrigger),
|
||||
new Tuple<Type, ITrigger>(typeof(ValidateOrganizationDomainJob), validateOrganizationDomainTrigger),
|
||||
new Tuple<Type, ITrigger>(typeof(UpdatePhishingDomainsJob), updatePhishingDomainsTrigger),
|
||||
};
|
||||
|
||||
if (_globalSettings.SelfHosted && _globalSettings.EnableCloudCommunication)
|
||||
@ -96,6 +104,7 @@ public class JobsHostedService : BaseJobsHostedService
|
||||
services.AddTransient<ValidateUsersJob>();
|
||||
services.AddTransient<ValidateOrganizationsJob>();
|
||||
services.AddTransient<ValidateOrganizationDomainJob>();
|
||||
services.AddTransient<UpdatePhishingDomainsJob>();
|
||||
}
|
||||
|
||||
public static void AddCommercialSecretsManagerJobServices(IServiceCollection services)
|
||||
|
97
src/Api/Jobs/UpdatePhishingDomainsJob.cs
Normal file
97
src/Api/Jobs/UpdatePhishingDomainsJob.cs
Normal 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.");
|
||||
}
|
||||
}
|
||||
}
|
@ -17,11 +17,7 @@ public class OrganizationSponsorshipCreateRequestModel
|
||||
[StringLength(256)]
|
||||
public string FriendlyName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// (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; }
|
||||
public bool? IsAdminInitiated { get; set; }
|
||||
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(512)]
|
||||
|
@ -182,6 +182,7 @@ public class Startup
|
||||
services.AddBillingOperations();
|
||||
services.AddReportingServices();
|
||||
services.AddImportServices();
|
||||
services.AddPhishingDomainServices(globalSettings);
|
||||
|
||||
// Authorization Handlers
|
||||
services.AddAuthorizationHandlers();
|
||||
|
@ -3,6 +3,10 @@ using Bit.Api.Tools.Authorization;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
|
||||
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.Utilities;
|
||||
using Bit.Core.Vault.Authorization.SecurityTasks;
|
||||
@ -109,4 +113,25 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
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>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -37,6 +37,10 @@
|
||||
},
|
||||
"storage": {
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,6 +71,9 @@
|
||||
"accessKeySecret": "SECRET",
|
||||
"region": "SECRET"
|
||||
},
|
||||
"phishingDomain": {
|
||||
"updateUrl": "SECRET"
|
||||
},
|
||||
"distributedIpRateLimiting": {
|
||||
"enabled": true,
|
||||
"maxRedisTimeoutsThreshold": 10,
|
||||
|
@ -44,7 +44,7 @@ public class InviteUsersPasswordManagerValidator(
|
||||
return new Invalid<PasswordManagerSubscriptionUpdate>(new PasswordManagerMustHaveSeatsError(subscriptionUpdate));
|
||||
}
|
||||
|
||||
if (subscriptionUpdate.MaxSeatsReached)
|
||||
if (subscriptionUpdate.MaxSeatsExceeded)
|
||||
{
|
||||
return new Invalid<PasswordManagerSubscriptionUpdate>(
|
||||
new PasswordManagerSeatLimitHasBeenReachedError(subscriptionUpdate));
|
||||
|
@ -48,6 +48,11 @@ public class PasswordManagerSubscriptionUpdate
|
||||
/// </summary>
|
||||
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 InviteOrganization InviteOrganization { get; }
|
||||
|
@ -18,6 +18,15 @@ public interface IOrganizationUserRepository : IRepository<OrganizationUser, Gui
|
||||
Task<ICollection<OrganizationUser>> GetManyByUserAsync(Guid userId);
|
||||
Task<ICollection<OrganizationUser>> GetManyByOrganizationAsync(Guid organizationId, OrganizationUserType? type);
|
||||
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<ICollection<string>> SelectKnownEmailsAsync(Guid organizationId, IEnumerable<string> emails, bool onlyRegisteredUsers);
|
||||
Task<OrganizationUser?> GetByOrganizationAsync(Guid organizationId, Guid userId);
|
||||
|
@ -41,6 +41,7 @@ public static class OrganizationLicenseConstants
|
||||
public const string Refresh = nameof(Refresh);
|
||||
public const string ExpirationWithoutGracePeriod = nameof(ExpirationWithoutGracePeriod);
|
||||
public const string Trial = nameof(Trial);
|
||||
public const string UseAdminSponsoredFamilies = nameof(UseAdminSponsoredFamilies);
|
||||
}
|
||||
|
||||
public static class UserLicenseConstants
|
||||
|
@ -53,6 +53,7 @@ public class OrganizationLicenseClaimsFactory : ILicenseClaimsFactory<Organizati
|
||||
new(nameof(OrganizationLicenseConstants.Refresh), refresh.ToString(CultureInfo.InvariantCulture)),
|
||||
new(nameof(OrganizationLicenseConstants.ExpirationWithoutGracePeriod), expirationWithoutGracePeriod.ToString(CultureInfo.InvariantCulture)),
|
||||
new(nameof(OrganizationLicenseConstants.Trial), trial.ToString()),
|
||||
new(nameof(OrganizationLicenseConstants.UseAdminSponsoredFamilies), entity.UseAdminSponsoredFamilies.ToString()),
|
||||
};
|
||||
|
||||
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.UseAdminSponsoredFamilies), entity.UseAdminSponsoredFamilies.ToString()));
|
||||
|
||||
return Task.FromResult(claims);
|
||||
}
|
||||
|
@ -108,6 +108,7 @@ public static class FeatureFlagKeys
|
||||
public const string PolicyRequirements = "pm-14439-policy-requirements";
|
||||
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 EventBasedOrganizationIntegrations = "event-based-organization-integrations";
|
||||
|
||||
/* Auth Team */
|
||||
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 PM17772_AdminInitiatedSponsorships = "pm-17772-admin-initiated-sponsorships";
|
||||
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 PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method";
|
||||
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 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 */
|
||||
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
||||
public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service";
|
||||
@ -186,8 +190,6 @@ public static class FeatureFlagKeys
|
||||
|
||||
/* Tools Team */
|
||||
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";
|
||||
|
||||
/* Vault Team */
|
||||
@ -195,13 +197,13 @@ public static class FeatureFlagKeys
|
||||
public const string PM9111ExtensionPersistAddEditForm = "pm-9111-extension-persist-add-edit-form";
|
||||
public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-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 SecurityTasks = "security-tasks";
|
||||
public const string CipherKeyEncryption = "cipher-key-encryption";
|
||||
public const string DesktopCipherForms = "pm-18520-desktop-cipher-forms";
|
||||
public const string PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk";
|
||||
public const string EndUserNotifications = "pm-10609-end-user-notifications";
|
||||
public const string PhishingDetection = "phishing-detection";
|
||||
|
||||
public static List<string> GetAllKeys()
|
||||
{
|
||||
|
@ -23,8 +23,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.402.61" />
|
||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.118" />
|
||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.402.79" />
|
||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.136" />
|
||||
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
|
||||
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
|
||||
@ -38,7 +38,7 @@
|
||||
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
|
||||
<PackageReference Include="MailKit" Version="4.11.0" />
|
||||
<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.Data.SqlClient" Version="5.2.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.7.0" />
|
||||
|
@ -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,
|
||||
ILicensingService licenseService, int? version = null)
|
||||
{
|
||||
@ -105,6 +133,7 @@ public class OrganizationLicense : ILicense
|
||||
Trial = false;
|
||||
}
|
||||
|
||||
UseAdminSponsoredFamilies = org.UseAdminSponsoredFamilies;
|
||||
Hash = Convert.ToBase64String(ComputeHash());
|
||||
Signature = Convert.ToBase64String(licenseService.SignLicense(this));
|
||||
}
|
||||
@ -153,6 +182,7 @@ public class OrganizationLicense : ILicense
|
||||
|
||||
public bool Trial { get; set; }
|
||||
public LicenseType? LicenseType { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
public string Hash { get; set; }
|
||||
public string Signature { get; set; }
|
||||
public string Token { get; set; }
|
||||
@ -292,13 +322,35 @@ public class OrganizationLicense : ILicense
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Do not extend this method. It is only here for backwards compatibility with old licenses.
|
||||
/// Instead, extend the CanUse method using the ClaimsPrincipal.
|
||||
/// Validates an obsolete license format using property-based validation.
|
||||
/// </summary>
|
||||
/// <param name="globalSettings"></param>
|
||||
/// <param name="licensingService"></param>
|
||||
/// <param name="exception"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// ⚠️ DEPRECATED: This method is deprecated and should not be extended or modified.
|
||||
/// 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)
|
||||
{
|
||||
// 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 smSeats = claimsPrincipal.GetValue<int?>(nameof(SmSeats));
|
||||
var smServiceAccounts = claimsPrincipal.GetValue<int?>(nameof(SmServiceAccounts));
|
||||
var useAdminSponsoredFamilies = claimsPrincipal.GetValue<bool>(nameof(UseAdminSponsoredFamilies));
|
||||
|
||||
return issued <= DateTime.UtcNow &&
|
||||
expires >= DateTime.UtcNow &&
|
||||
@ -419,7 +472,9 @@ public class OrganizationLicense : ILicense
|
||||
useSecretsManager == organization.UseSecretsManager &&
|
||||
usePasswordManager == organization.UsePasswordManager &&
|
||||
smSeats == organization.SmSeats &&
|
||||
smServiceAccounts == organization.SmServiceAccounts;
|
||||
smServiceAccounts == organization.SmServiceAccounts &&
|
||||
useAdminSponsoredFamilies == organization.UseAdminSponsoredFamilies;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
@ -14,11 +14,17 @@ namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnte
|
||||
public class CreateSponsorshipCommand(
|
||||
ICurrentContext currentContext,
|
||||
IOrganizationSponsorshipRepository organizationSponsorshipRepository,
|
||||
IUserService userService) : ICreateSponsorshipCommand
|
||||
IUserService userService,
|
||||
IOrganizationService organizationService) : ICreateSponsorshipCommand
|
||||
{
|
||||
public async Task<OrganizationSponsorship> CreateSponsorshipAsync(Organization sponsoringOrganization,
|
||||
OrganizationUser sponsoringMember, PlanSponsorshipType sponsorshipType, string sponsoredEmail,
|
||||
string friendlyName, string notes)
|
||||
public async Task<OrganizationSponsorship> CreateSponsorshipAsync(
|
||||
Organization sponsoringOrganization,
|
||||
OrganizationUser sponsoringMember,
|
||||
PlanSponsorshipType sponsorshipType,
|
||||
string sponsoredEmail,
|
||||
string friendlyName,
|
||||
bool isAdminInitiated,
|
||||
string notes)
|
||||
{
|
||||
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.");
|
||||
}
|
||||
|
||||
var sponsorship = new OrganizationSponsorship();
|
||||
sponsorship.SponsoringOrganizationId = sponsoringOrganization.Id;
|
||||
sponsorship.SponsoringOrganizationUserId = sponsoringMember.Id;
|
||||
sponsorship.FriendlyName = friendlyName;
|
||||
sponsorship.OfferedToEmail = sponsoredEmail;
|
||||
sponsorship.PlanSponsorshipType = sponsorshipType;
|
||||
if (isAdminInitiated)
|
||||
{
|
||||
ValidateAdminInitiatedSponsorship(sponsoringOrganization);
|
||||
}
|
||||
|
||||
var sponsorship = new OrganizationSponsorship
|
||||
{
|
||||
SponsoringOrganizationId = sponsoringOrganization.Id,
|
||||
SponsoringOrganizationUserId = sponsoringMember.Id,
|
||||
FriendlyName = friendlyName,
|
||||
OfferedToEmail = sponsoredEmail,
|
||||
PlanSponsorshipType = sponsorshipType,
|
||||
IsAdminInitiated = isAdminInitiated,
|
||||
Notes = notes
|
||||
};
|
||||
|
||||
if (existingOrgSponsorship != null)
|
||||
{
|
||||
@ -61,35 +76,22 @@ public class CreateSponsorshipCommand(
|
||||
sponsorship.Id = existingOrgSponsorship.Id;
|
||||
}
|
||||
|
||||
var isAdminInitiated = false;
|
||||
if (currentContext.UserId != sponsoringMember.UserId)
|
||||
if (isAdminInitiated && sponsoringOrganization.Seats.HasValue)
|
||||
{
|
||||
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 sponsor other Family organizations.");
|
||||
}
|
||||
|
||||
isAdminInitiated = true;
|
||||
await organizationService.AutoAddSeatsAsync(sponsoringOrganization, 1);
|
||||
}
|
||||
|
||||
sponsorship.IsAdminInitiated = isAdminInitiated;
|
||||
sponsorship.Notes = notes;
|
||||
|
||||
try
|
||||
{
|
||||
await organizationSponsorshipRepository.UpsertAsync(sponsorship);
|
||||
if (isAdminInitiated)
|
||||
{
|
||||
await organizationSponsorshipRepository.CreateAsync(sponsorship);
|
||||
}
|
||||
else
|
||||
{
|
||||
await organizationSponsorshipRepository.UpsertAsync(sponsorship);
|
||||
}
|
||||
|
||||
return sponsorship;
|
||||
}
|
||||
catch
|
||||
@ -101,4 +103,24 @@ public class CreateSponsorshipCommand(
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,12 @@ namespace Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnte
|
||||
|
||||
public interface ICreateSponsorshipCommand
|
||||
{
|
||||
Task<OrganizationSponsorship> CreateSponsorshipAsync(Organization sponsoringOrg, OrganizationUser sponsoringOrgUser,
|
||||
PlanSponsorshipType sponsorshipType, string sponsoredEmail, string friendlyName, string notes);
|
||||
Task<OrganizationSponsorship> CreateSponsorshipAsync(
|
||||
Organization sponsoringOrg,
|
||||
OrganizationUser sponsoringOrgUser,
|
||||
PlanSponsorshipType sponsorshipType,
|
||||
string sponsoredEmail,
|
||||
string friendlyName,
|
||||
bool isAdminInitiated,
|
||||
string notes);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.PhishingDomainFeatures.Interfaces;
|
||||
|
||||
public interface ICloudPhishingDomainQuery
|
||||
{
|
||||
Task<List<string>> GetPhishingDomainsAsync();
|
||||
Task<string> GetRemoteChecksumAsync();
|
||||
}
|
8
src/Core/Repositories/IPhishingDomainRepository.cs
Normal file
8
src/Core/Repositories/IPhishingDomainRepository.cs
Normal 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();
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@ -85,6 +85,7 @@ public class GlobalSettings : IGlobalSettings
|
||||
public virtual ILaunchDarklySettings LaunchDarkly { get; set; } = new LaunchDarklySettings();
|
||||
public virtual string DevelopmentDirectory { get; set; }
|
||||
public virtual IWebPushSettings WebPush { get; set; } = new WebPushSettings();
|
||||
public virtual IPhishingDomainSettings PhishingDomain { get; set; } = new PhishingDomainSettings();
|
||||
|
||||
public virtual bool EnableEmailVerification { get; set; }
|
||||
public virtual string KdfDefaultHashKey { get; set; }
|
||||
@ -645,6 +646,12 @@ public class GlobalSettings : IGlobalSettings
|
||||
public int MaxNetworkRetries { get; set; } = 2;
|
||||
}
|
||||
|
||||
public class PhishingDomainSettings : IPhishingDomainSettings
|
||||
{
|
||||
public string UpdateUrl { get; set; }
|
||||
public string ChecksumUrl { get; set; }
|
||||
}
|
||||
|
||||
public class DistributedIpRateLimitingSettings
|
||||
{
|
||||
public string RedisConnectionString { get; set; }
|
||||
|
@ -29,4 +29,5 @@ public interface IGlobalSettings
|
||||
string DevelopmentDirectory { get; set; }
|
||||
IWebPushSettings WebPush { get; set; }
|
||||
GlobalSettings.EventLoggingSettings EventLogging { get; set; }
|
||||
IPhishingDomainSettings PhishingDomain { get; set; }
|
||||
}
|
||||
|
7
src/Core/Settings/IPhishingDomainSettings.cs
Normal file
7
src/Core/Settings/IPhishingDomainSettings.cs
Normal file
@ -0,0 +1,7 @@
|
||||
namespace Bit.Core.Settings;
|
||||
|
||||
public interface IPhishingDomainSettings
|
||||
{
|
||||
string UpdateUrl { get; set; }
|
||||
string ChecksumUrl { get; set; }
|
||||
}
|
@ -1,4 +1,5 @@
|
||||
using System.Globalization;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Services.Implementations;
|
||||
using Bit.Core.AdminConsole.Services.NoopImplementations;
|
||||
using Bit.Core.Context;
|
||||
@ -62,33 +63,45 @@ public class Startup
|
||||
{
|
||||
services.AddSingleton<IApplicationCacheService, InMemoryApplicationCacheService>();
|
||||
}
|
||||
services.AddScoped<IEventService, EventService>();
|
||||
|
||||
if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString))
|
||||
{
|
||||
services.AddKeyedSingleton<IEventWriteService, AzureQueueEventWriteService>("storage");
|
||||
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName))
|
||||
{
|
||||
services.AddSingleton<IEventWriteService, AzureServiceBusEventWriteService>();
|
||||
services.AddKeyedSingleton<IEventWriteService, AzureServiceBusEventWriteService>("broadcast");
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<IEventWriteService, AzureQueueEventWriteService>();
|
||||
services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("storage");
|
||||
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName))
|
||||
{
|
||||
services.AddSingleton<IEventWriteService, RabbitMqEventWriteService>();
|
||||
services.AddKeyedSingleton<IEventWriteService, RabbitMqEventWriteService>("broadcast");
|
||||
}
|
||||
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();
|
||||
|
||||
|
@ -150,6 +150,8 @@ public static class DapperHelpers
|
||||
os => os.LastSyncDate,
|
||||
os => os.ValidUntil,
|
||||
os => os.ToDelete,
|
||||
os => os.IsAdminInitiated,
|
||||
os => os.Notes,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -10,7 +10,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Dapper" Version="2.1.35" />
|
||||
<PackageReference Include="Dapper" Version="2.1.66" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -14,9 +14,35 @@ public class OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery : IQuery
|
||||
|
||||
public IQueryable<OrganizationUser> Run(DatabaseContext dbContext)
|
||||
{
|
||||
var query = from ou in dbContext.OrganizationUsers
|
||||
where ou.OrganizationId == _organizationId && ou.Status >= OrganizationUserStatusType.Invited
|
||||
select ou;
|
||||
return query;
|
||||
var orgUsersQuery = from ou in dbContext.OrganizationUsers
|
||||
where ou.OrganizationId == _organizationId && ou.Status >= OrganizationUserStatusType.Invited
|
||||
select new OrganizationUser { Id = ou.Id, OrganizationId = ou.OrganizationId, Status = ou.Status };
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
@ -8,12 +8,12 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" />
|
||||
<PackageReference Include="linq2db" Version="5.4.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="8.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.8" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="8.0.4" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.2" />
|
||||
<PackageReference Include="linq2db.EntityFrameworkCore" Version="8.1.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="[8.0.8]" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="[8.0.8]" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="[8.0.8]" />
|
||||
<PackageReference Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="[8.0.4]" />
|
||||
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="[8.0.2]" />
|
||||
<PackageReference Include="linq2db.EntityFrameworkCore" Version="[8.1.0]" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -34,6 +34,12 @@ public class NotificationEntityTypeConfiguration : IEntityTypeConfiguration<Noti
|
||||
.HasIndex(n => n.TaskId)
|
||||
.IsClustered(false);
|
||||
|
||||
builder
|
||||
.HasOne(n => n.Task)
|
||||
.WithMany()
|
||||
.HasForeignKey(n => n.TaskId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.ToTable(nameof(Notification));
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,18 @@ public class SecurityTaskEntityTypeConfiguration : IEntityTypeConfiguration<Secu
|
||||
.HasIndex(s => s.CipherId)
|
||||
.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
|
||||
.ToTable(nameof(SecurityTask));
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ using System.Security.Claims;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using AspNetCoreRateLimit;
|
||||
using Azure.Storage.Queues;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
@ -333,34 +334,46 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString))
|
||||
{
|
||||
services.AddKeyedSingleton<IEventWriteService, AzureQueueEventWriteService>("storage");
|
||||
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName))
|
||||
{
|
||||
services.AddSingleton<IEventWriteService, AzureServiceBusEventWriteService>();
|
||||
services.AddKeyedSingleton<IEventWriteService, AzureServiceBusEventWriteService>("broadcast");
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<IEventWriteService, AzureQueueEventWriteService>();
|
||||
services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
|
||||
}
|
||||
}
|
||||
else if (globalSettings.SelfHosted)
|
||||
{
|
||||
services.AddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("storage");
|
||||
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName))
|
||||
{
|
||||
services.AddSingleton<IEventWriteService, RabbitMqEventWriteService>();
|
||||
services.AddKeyedSingleton<IEventWriteService, RabbitMqEventWriteService>("broadcast");
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<IEventWriteService, RepositoryEventWriteService>();
|
||||
services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
|
||||
}
|
||||
}
|
||||
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))
|
||||
{
|
||||
|
@ -14,7 +14,7 @@ CREATE TABLE [dbo].[Notification]
|
||||
CONSTRAINT [PK_Notification] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||
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_SecurityTask] FOREIGN KEY ([TaskId]) REFERENCES [dbo].[SecurityTask] ([Id])
|
||||
CONSTRAINT [FK_Notification_SecurityTask] FOREIGN KEY ([TaskId]) REFERENCES [dbo].[SecurityTask] ([Id]) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
|
||||
|
@ -6,7 +6,7 @@
|
||||
@Data NVARCHAR(MAX),
|
||||
@Favorites NVARCHAR(MAX), -- not used
|
||||
@Folders NVARCHAR(MAX), -- not used
|
||||
@Attachments NVARCHAR(MAX),
|
||||
@Attachments NVARCHAR(MAX), -- not used
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7),
|
||||
@FolderId UNIQUEIDENTIFIER,
|
||||
@ -50,7 +50,6 @@ BEGIN
|
||||
ELSE
|
||||
JSON_MODIFY([Favorites], @UserIdPath, NULL)
|
||||
END,
|
||||
[Attachments] = @Attachments,
|
||||
[Reprompt] = @Reprompt,
|
||||
[CreationDate] = @CreationDate,
|
||||
[RevisionDate] = @RevisionDate,
|
||||
|
@ -6,7 +6,7 @@
|
||||
@Data NVARCHAR(MAX),
|
||||
@Favorites NVARCHAR(MAX),
|
||||
@Folders NVARCHAR(MAX),
|
||||
@Attachments NVARCHAR(MAX),
|
||||
@Attachments NVARCHAR(MAX), -- not used
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7),
|
||||
@DeletedDate DATETIME2(7),
|
||||
@ -25,7 +25,6 @@ BEGIN
|
||||
[Data],
|
||||
[Favorites],
|
||||
[Folders],
|
||||
[Attachments],
|
||||
[CreationDate],
|
||||
[RevisionDate],
|
||||
[DeletedDate],
|
||||
@ -41,7 +40,6 @@ BEGIN
|
||||
@Data,
|
||||
@Favorites,
|
||||
@Folders,
|
||||
@Attachments,
|
||||
@CreationDate,
|
||||
@RevisionDate,
|
||||
@DeletedDate,
|
||||
|
@ -10,20 +10,59 @@ BEGIN
|
||||
|
||||
DECLARE @UserId UNIQUEIDENTIFIER
|
||||
DECLARE @OrganizationId UNIQUEIDENTIFIER
|
||||
DECLARE @CurrentAttachments NVARCHAR(MAX)
|
||||
DECLARE @NewAttachments NVARCHAR(MAX)
|
||||
|
||||
-- Get current cipher data
|
||||
SELECT
|
||||
@UserId = [UserId],
|
||||
@OrganizationId = [OrganizationId]
|
||||
@OrganizationId = [OrganizationId],
|
||||
@CurrentAttachments = [Attachments]
|
||||
FROM
|
||||
[dbo].[Cipher]
|
||||
WHERE [Id] = @Id
|
||||
|
||||
UPDATE
|
||||
[dbo].[Cipher]
|
||||
SET
|
||||
[Attachments] = JSON_MODIFY([Attachments], @AttachmentIdPath, NULL)
|
||||
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
|
||||
|
@ -6,7 +6,7 @@
|
||||
@Data NVARCHAR(MAX),
|
||||
@Favorites NVARCHAR(MAX),
|
||||
@Folders NVARCHAR(MAX),
|
||||
@Attachments NVARCHAR(MAX),
|
||||
@Attachments NVARCHAR(MAX), -- not used
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7),
|
||||
@DeletedDate DATETIME2(7),
|
||||
@ -25,7 +25,6 @@ BEGIN
|
||||
[Data] = @Data,
|
||||
[Favorites] = @Favorites,
|
||||
[Folders] = @Folders,
|
||||
[Attachments] = @Attachments,
|
||||
[CreationDate] = @CreationDate,
|
||||
[RevisionDate] = @RevisionDate,
|
||||
[DeletedDate] = @DeletedDate,
|
||||
|
@ -8,21 +8,75 @@ 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)
|
||||
|
||||
UPDATE
|
||||
[dbo].[Cipher]
|
||||
SET
|
||||
[Attachments] =
|
||||
CASE
|
||||
WHEN [Attachments] IS NULL THEN
|
||||
CONCAT('{', @AttachmentIdKey, ':', @AttachmentData, '}')
|
||||
ELSE
|
||||
JSON_MODIFY([Attachments], @AttachmentIdPath, JSON_QUERY(@AttachmentData, '$'))
|
||||
END
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
-- 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
|
||||
|
@ -5,10 +5,33 @@ BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
COUNT(1)
|
||||
FROM
|
||||
[dbo].[OrganizationUserView]
|
||||
WHERE
|
||||
OrganizationId = @OrganizationId
|
||||
AND Status >= 0 --Invited
|
||||
(
|
||||
-- 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
|
||||
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
|
||||
|
@ -6,7 +6,7 @@ CREATE TABLE [dbo].[PasswordHealthReportApplication]
|
||||
CreationDate DATETIME2(7) NOT NULL,
|
||||
RevisionDate DATETIME2(7) NOT NULL,
|
||||
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
|
||||
|
||||
|
@ -1,9 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
@ -12,22 +9,6 @@ namespace Bit.Core.Test.Models.Business;
|
||||
|
||||
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>
|
||||
/// 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));
|
||||
}
|
||||
|
||||
/// <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
|
||||
}
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
||||
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).ReturnsNull();
|
||||
|
||||
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);
|
||||
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()
|
||||
@ -55,7 +55,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
||||
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).Returns(user);
|
||||
|
||||
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);
|
||||
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()
|
||||
@ -72,7 +72,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
||||
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).Returns(user);
|
||||
|
||||
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);
|
||||
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()
|
||||
@ -91,7 +91,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
||||
sutProvider.GetDependency<IUserService>().GetUserByIdAsync(orgUser.UserId!.Value).Returns(user);
|
||||
|
||||
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);
|
||||
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()
|
||||
@ -115,7 +115,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(orgUser.UserId.Value);
|
||||
|
||||
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);
|
||||
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().DidNotReceiveWithAnyArgs()
|
||||
@ -147,7 +147,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
@ -170,7 +170,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
||||
|
||||
|
||||
await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
|
||||
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, null);
|
||||
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, false, null);
|
||||
|
||||
var expectedSponsorship = new OrganizationSponsorship
|
||||
{
|
||||
@ -209,7 +209,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
||||
|
||||
var actualException = await Assert.ThrowsAsync<Exception>(() =>
|
||||
sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
|
||||
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, null));
|
||||
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, false, null));
|
||||
Assert.Same(expectedException, actualException);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1)
|
||||
@ -244,9 +244,9 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
||||
|
||||
var actual = await Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>
|
||||
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]
|
||||
@ -278,9 +278,9 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
||||
|
||||
var actual = await Assert.ThrowsAsync<UnauthorizedAccessException>(async () =>
|
||||
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]
|
||||
@ -312,7 +312,7 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
||||
]);
|
||||
|
||||
var actual = await sutProvider.Sut.CreateSponsorshipAsync(sponsoringOrg, sponsoringOrgUser,
|
||||
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, notes);
|
||||
PlanSponsorshipType.FamiliesForEnterprise, sponsoredEmail, friendlyName, true, notes);
|
||||
|
||||
|
||||
var expectedSponsorship = new OrganizationSponsorship
|
||||
@ -330,6 +330,6 @@ public class CreateSponsorshipCommandTests : FamiliesForEnterpriseTestsBase
|
||||
Assert.True(SponsorshipValidator(expectedSponsorship, actual));
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationSponsorshipRepository>().Received(1)
|
||||
.UpsertAsync(Arg.Is<OrganizationSponsorship>(s => SponsorshipValidator(s, expectedSponsorship)));
|
||||
.CreateAsync(Arg.Is<OrganizationSponsorship>(s => SponsorshipValidator(s, expectedSponsorship)));
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
@ -912,4 +913,59 @@ public class CipherRepositoryTests
|
||||
Assert.Equal(CipherType.SecureNote, updatedCipher1.Type);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
@ -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
|
3111
util/MySqlMigrations/Migrations/20250422181736_NotificationCascadeDelete.Designer.cs
generated
Normal file
3111
util/MySqlMigrations/Migrations/20250422181736_NotificationCascadeDelete.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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");
|
||||
}
|
||||
}
|
3112
util/MySqlMigrations/Migrations/20250422183835_SecurityTaskCascadeDelete.Designer.cs
generated
Normal file
3112
util/MySqlMigrations/Migrations/20250422183835_SecurityTaskCascadeDelete.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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");
|
||||
}
|
||||
}
|
@ -2737,7 +2737,8 @@ namespace Bit.MySqlMigrations.Migrations
|
||||
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task")
|
||||
.WithMany()
|
||||
.HasForeignKey("TaskId");
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User")
|
||||
.WithMany()
|
||||
@ -2852,7 +2853,8 @@ namespace Bit.MySqlMigrations.Migrations
|
||||
{
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher")
|
||||
.WithMany()
|
||||
.HasForeignKey("CipherId");
|
||||
.HasForeignKey("CipherId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
||||
.WithMany()
|
||||
|
@ -10,7 +10,7 @@
|
||||
</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>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
3117
util/PostgresMigrations/Migrations/20250422181741_NotificationCascadeDelete.Designer.cs
generated
Normal file
3117
util/PostgresMigrations/Migrations/20250422181741_NotificationCascadeDelete.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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");
|
||||
}
|
||||
}
|
3118
util/PostgresMigrations/Migrations/20250422183847_SecurityTaskCascadeDelete.Designer.cs
generated
Normal file
3118
util/PostgresMigrations/Migrations/20250422183847_SecurityTaskCascadeDelete.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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");
|
||||
}
|
||||
}
|
@ -2743,7 +2743,8 @@ namespace Bit.PostgresMigrations.Migrations
|
||||
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task")
|
||||
.WithMany()
|
||||
.HasForeignKey("TaskId");
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User")
|
||||
.WithMany()
|
||||
@ -2858,7 +2859,8 @@ namespace Bit.PostgresMigrations.Migrations
|
||||
{
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher")
|
||||
.WithMany()
|
||||
.HasForeignKey("CipherId");
|
||||
.HasForeignKey("CipherId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
||||
.WithMany()
|
||||
|
@ -6,7 +6,7 @@
|
||||
</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>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
@ -1,6 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<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>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
3100
util/SqliteMigrations/Migrations/20250422181747_NotificationCascadeDelete.Designer.cs
generated
Normal file
3100
util/SqliteMigrations/Migrations/20250422181747_NotificationCascadeDelete.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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");
|
||||
}
|
||||
}
|
3101
util/SqliteMigrations/Migrations/20250422183841_SecurityTaskCascadeDelete.Designer.cs
generated
Normal file
3101
util/SqliteMigrations/Migrations/20250422183841_SecurityTaskCascadeDelete.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -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");
|
||||
}
|
||||
}
|
@ -2726,7 +2726,8 @@ namespace Bit.SqliteMigrations.Migrations
|
||||
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.SecurityTask", "Task")
|
||||
.WithMany()
|
||||
.HasForeignKey("TaskId");
|
||||
.HasForeignKey("TaskId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.Models.User", "User")
|
||||
.WithMany()
|
||||
@ -2841,7 +2842,8 @@ namespace Bit.SqliteMigrations.Migrations
|
||||
{
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.Vault.Models.Cipher", "Cipher")
|
||||
.WithMany()
|
||||
.HasForeignKey("CipherId");
|
||||
.HasForeignKey("CipherId")
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
||||
.WithMany()
|
||||
|
@ -11,7 +11,7 @@
|
||||
</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>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
|
Loading…
x
Reference in New Issue
Block a user