mirror of
https://github.com/bitwarden/server.git
synced 2025-04-25 06:42:22 -05:00
Merge branch 'main' into feature/phishing-detection
This commit is contained in:
commit
866144d00e
13
.github/renovate.json5
vendored
13
.github/renovate.json5
vendored
@ -9,6 +9,19 @@
|
||||
"nuget",
|
||||
],
|
||||
packageRules: [
|
||||
{
|
||||
// Group all release-related workflows for GitHub Actions together for BRE.
|
||||
groupName: "github-action",
|
||||
matchManagers: ["github-actions"],
|
||||
matchFileNames: [
|
||||
".github/workflows/publish.yml",
|
||||
".github/workflows/release.yml",
|
||||
".github/workflows/repository-management.yml"
|
||||
],
|
||||
commitMessagePrefix: "[deps] BRE:",
|
||||
reviewers: ["team:dept-bre"],
|
||||
addLabels: ["hold"]
|
||||
},
|
||||
{
|
||||
groupName: "dockerfile minor",
|
||||
matchManagers: ["dockerfile"],
|
||||
|
@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<Version>2025.4.1</Version>
|
||||
<Version>2025.4.3</Version>
|
||||
|
||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
@ -8,7 +8,7 @@ using Bit.Core.Utilities;
|
||||
using Bit.Scim.Context;
|
||||
using Bit.Scim.Utilities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Duende.IdentityModel;
|
||||
using IdentityModel;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using Stripe;
|
||||
|
||||
|
@ -3,7 +3,7 @@ using System.Text.Encodings.Web;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Scim.Context;
|
||||
using Duende.IdentityModel;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
@ -19,10 +19,10 @@ using Bit.Core.Tokens;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Sso.Models;
|
||||
using Bit.Sso.Utilities;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Services;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -7,9 +7,9 @@ using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Sso.Models;
|
||||
using Bit.Sso.Utilities;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Infrastructure;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
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": [
|
||||
{
|
||||
|
0
dev/ef_migrate.ps1
Normal file → Executable file
0
dev/ef_migrate.ps1
Normal file → Executable file
@ -25,6 +25,12 @@
|
||||
"Subscriptions": [
|
||||
{
|
||||
"Name": "events-write-subscription"
|
||||
},
|
||||
{
|
||||
"Name": "events-slack-subscription"
|
||||
},
|
||||
{
|
||||
"Name": "events-webhook-subscription"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
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": [
|
||||
{
|
||||
|
@ -0,0 +1,103 @@
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Controllers;
|
||||
|
||||
[Route("organizations/{organizationId:guid}/integrations/{integrationId:guid}/configurations")]
|
||||
[Authorize("Application")]
|
||||
public class OrganizationIntegrationConfigurationController(
|
||||
ICurrentContext currentContext,
|
||||
IOrganizationIntegrationRepository integrationRepository,
|
||||
IOrganizationIntegrationConfigurationRepository integrationConfigurationRepository) : Controller
|
||||
{
|
||||
[HttpPost("")]
|
||||
public async Task<OrganizationIntegrationConfigurationResponseModel> CreateAsync(
|
||||
Guid organizationId,
|
||||
Guid integrationId,
|
||||
[FromBody] OrganizationIntegrationConfigurationRequestModel model)
|
||||
{
|
||||
if (!await HasPermission(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
var integration = await integrationRepository.GetByIdAsync(integrationId);
|
||||
if (integration == null || integration.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
if (!model.IsValidForType(integration.Type))
|
||||
{
|
||||
throw new BadRequestException($"Invalid Configuration and/or Template for integration type {integration.Type}");
|
||||
}
|
||||
|
||||
var organizationIntegrationConfiguration = model.ToOrganizationIntegrationConfiguration(integrationId);
|
||||
var configuration = await integrationConfigurationRepository.CreateAsync(organizationIntegrationConfiguration);
|
||||
return new OrganizationIntegrationConfigurationResponseModel(configuration);
|
||||
}
|
||||
|
||||
[HttpPut("{configurationId:guid}")]
|
||||
public async Task<OrganizationIntegrationConfigurationResponseModel> UpdateAsync(
|
||||
Guid organizationId,
|
||||
Guid integrationId,
|
||||
Guid configurationId,
|
||||
[FromBody] OrganizationIntegrationConfigurationRequestModel model)
|
||||
{
|
||||
if (!await HasPermission(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
var integration = await integrationRepository.GetByIdAsync(integrationId);
|
||||
if (integration == null || integration.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
if (!model.IsValidForType(integration.Type))
|
||||
{
|
||||
throw new BadRequestException($"Invalid Configuration and/or Template for integration type {integration.Type}");
|
||||
}
|
||||
|
||||
var configuration = await integrationConfigurationRepository.GetByIdAsync(configurationId);
|
||||
if (configuration is null || configuration.OrganizationIntegrationId != integrationId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var newConfiguration = model.ToOrganizationIntegrationConfiguration(configuration);
|
||||
await integrationConfigurationRepository.ReplaceAsync(newConfiguration);
|
||||
|
||||
return new OrganizationIntegrationConfigurationResponseModel(newConfiguration);
|
||||
}
|
||||
|
||||
[HttpDelete("{configurationId:guid}")]
|
||||
[HttpPost("{configurationId:guid}/delete")]
|
||||
public async Task DeleteAsync(Guid organizationId, Guid integrationId, Guid configurationId)
|
||||
{
|
||||
if (!await HasPermission(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
var integration = await integrationRepository.GetByIdAsync(integrationId);
|
||||
if (integration == null || integration.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var configuration = await integrationConfigurationRepository.GetByIdAsync(configurationId);
|
||||
if (configuration is null || configuration.OrganizationIntegrationId != integrationId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await integrationConfigurationRepository.DeleteAsync(configuration);
|
||||
}
|
||||
|
||||
private async Task<bool> HasPermission(Guid organizationId)
|
||||
{
|
||||
return await currentContext.OrganizationOwner(organizationId);
|
||||
}
|
||||
}
|
@ -0,0 +1,71 @@
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Api.AdminConsole.Controllers;
|
||||
|
||||
[Route("organizations/{organizationId:guid}/integrations")]
|
||||
[Authorize("Application")]
|
||||
public class OrganizationIntegrationController(
|
||||
ICurrentContext currentContext,
|
||||
IOrganizationIntegrationRepository integrationRepository) : Controller
|
||||
{
|
||||
[HttpPost("")]
|
||||
public async Task<OrganizationIntegrationResponseModel> CreateAsync(Guid organizationId, [FromBody] OrganizationIntegrationRequestModel model)
|
||||
{
|
||||
if (!await HasPermission(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var integration = await integrationRepository.CreateAsync(model.ToOrganizationIntegration(organizationId));
|
||||
return new OrganizationIntegrationResponseModel(integration);
|
||||
}
|
||||
|
||||
[HttpPut("{integrationId:guid}")]
|
||||
public async Task<OrganizationIntegrationResponseModel> UpdateAsync(Guid organizationId, Guid integrationId, [FromBody] OrganizationIntegrationRequestModel model)
|
||||
{
|
||||
if (!await HasPermission(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var integration = await integrationRepository.GetByIdAsync(integrationId);
|
||||
if (integration is null || integration.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await integrationRepository.ReplaceAsync(model.ToOrganizationIntegration(integration));
|
||||
return new OrganizationIntegrationResponseModel(integration);
|
||||
}
|
||||
|
||||
[HttpDelete("{integrationId:guid}")]
|
||||
[HttpPost("{integrationId:guid}/delete")]
|
||||
public async Task DeleteAsync(Guid organizationId, Guid integrationId)
|
||||
{
|
||||
if (!await HasPermission(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var integration = await integrationRepository.GetByIdAsync(integrationId);
|
||||
if (integration is null || integration.OrganizationId != organizationId)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await integrationRepository.DeleteAsync(integration);
|
||||
}
|
||||
|
||||
private async Task<bool> HasPermission(Guid organizationId)
|
||||
{
|
||||
return await currentContext.OrganizationOwner(organizationId);
|
||||
}
|
||||
}
|
@ -494,7 +494,7 @@ public class OrganizationUsersController : Controller
|
||||
}
|
||||
|
||||
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgId);
|
||||
var isTdeEnrollment = ssoConfig != null && ssoConfig.GetData().MemberDecryptionType == MemberDecryptionType.TrustedDeviceEncryption;
|
||||
var isTdeEnrollment = ssoConfig != null && ssoConfig.Enabled && ssoConfig.GetData().MemberDecryptionType == MemberDecryptionType.TrustedDeviceEncryption;
|
||||
if (!isTdeEnrollment && !string.IsNullOrWhiteSpace(model.ResetPasswordKey) && !await _userService.VerifySecretAsync(user, model.MasterPasswordHash))
|
||||
{
|
||||
throw new BadRequestException("Incorrect password");
|
||||
|
@ -0,0 +1,77 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Integrations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Controllers;
|
||||
|
||||
[Route("organizations/{organizationId:guid}/integrations/slack")]
|
||||
[Authorize("Application")]
|
||||
public class SlackIntegrationController(
|
||||
ICurrentContext currentContext,
|
||||
IOrganizationIntegrationRepository integrationRepository,
|
||||
ISlackService slackService) : Controller
|
||||
{
|
||||
[HttpGet("redirect")]
|
||||
public async Task<IActionResult> RedirectAsync(Guid organizationId)
|
||||
{
|
||||
if (!await currentContext.OrganizationOwner(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
string callbackUrl = Url.RouteUrl(
|
||||
nameof(CreateAsync),
|
||||
new { organizationId },
|
||||
currentContext.HttpContext.Request.Scheme);
|
||||
var redirectUrl = slackService.GetRedirectUrl(callbackUrl);
|
||||
|
||||
if (string.IsNullOrEmpty(redirectUrl))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
return Redirect(redirectUrl);
|
||||
}
|
||||
|
||||
[HttpGet("create", Name = nameof(CreateAsync))]
|
||||
public async Task<IActionResult> CreateAsync(Guid organizationId, [FromQuery] string code)
|
||||
{
|
||||
if (!await currentContext.OrganizationOwner(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(code))
|
||||
{
|
||||
throw new BadRequestException("Missing code from Slack.");
|
||||
}
|
||||
|
||||
string callbackUrl = Url.RouteUrl(
|
||||
nameof(CreateAsync),
|
||||
new { organizationId },
|
||||
currentContext.HttpContext.Request.Scheme);
|
||||
var token = await slackService.ObtainTokenViaOAuth(code, callbackUrl);
|
||||
|
||||
if (string.IsNullOrEmpty(token))
|
||||
{
|
||||
throw new BadRequestException("Invalid response from Slack.");
|
||||
}
|
||||
|
||||
var integration = await integrationRepository.CreateAsync(new OrganizationIntegration
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Type = IntegrationType.Slack,
|
||||
Configuration = JsonSerializer.Serialize(new SlackIntegration(token)),
|
||||
});
|
||||
var location = $"/organizations/{organizationId}/integrations/{integration.Id}";
|
||||
|
||||
return Created(location, new OrganizationIntegrationResponseModel(integration));
|
||||
}
|
||||
}
|
@ -0,0 +1,73 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Integrations;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
|
||||
public class OrganizationIntegrationConfigurationRequestModel
|
||||
{
|
||||
public string? Configuration { get; set; }
|
||||
|
||||
[Required]
|
||||
public EventType EventType { get; set; }
|
||||
|
||||
public string? Template { get; set; }
|
||||
|
||||
public bool IsValidForType(IntegrationType integrationType)
|
||||
{
|
||||
switch (integrationType)
|
||||
{
|
||||
case IntegrationType.CloudBillingSync or IntegrationType.Scim:
|
||||
return false;
|
||||
case IntegrationType.Slack:
|
||||
return !string.IsNullOrWhiteSpace(Template) && IsConfigurationValid<SlackIntegrationConfiguration>();
|
||||
case IntegrationType.Webhook:
|
||||
return !string.IsNullOrWhiteSpace(Template) && IsConfigurationValid<WebhookIntegrationConfiguration>();
|
||||
default:
|
||||
return false;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public OrganizationIntegrationConfiguration ToOrganizationIntegrationConfiguration(Guid organizationIntegrationId)
|
||||
{
|
||||
return new OrganizationIntegrationConfiguration()
|
||||
{
|
||||
OrganizationIntegrationId = organizationIntegrationId,
|
||||
Configuration = Configuration,
|
||||
EventType = EventType,
|
||||
Template = Template
|
||||
};
|
||||
}
|
||||
|
||||
public OrganizationIntegrationConfiguration ToOrganizationIntegrationConfiguration(OrganizationIntegrationConfiguration currentConfiguration)
|
||||
{
|
||||
currentConfiguration.Configuration = Configuration;
|
||||
currentConfiguration.EventType = EventType;
|
||||
currentConfiguration.Template = Template;
|
||||
|
||||
return currentConfiguration;
|
||||
}
|
||||
|
||||
private bool IsConfigurationValid<T>()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(Configuration))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = JsonSerializer.Deserialize<T>(Configuration);
|
||||
return config is not null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,56 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
|
||||
public class OrganizationIntegrationRequestModel : IValidatableObject
|
||||
{
|
||||
public string? Configuration { get; set; }
|
||||
|
||||
public IntegrationType Type { get; set; }
|
||||
|
||||
public OrganizationIntegration ToOrganizationIntegration(Guid organizationId)
|
||||
{
|
||||
return new OrganizationIntegration()
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
Configuration = Configuration,
|
||||
Type = Type,
|
||||
};
|
||||
}
|
||||
|
||||
public OrganizationIntegration ToOrganizationIntegration(OrganizationIntegration currentIntegration)
|
||||
{
|
||||
currentIntegration.Configuration = Configuration;
|
||||
return currentIntegration;
|
||||
}
|
||||
|
||||
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
switch (Type)
|
||||
{
|
||||
case IntegrationType.CloudBillingSync or IntegrationType.Scim:
|
||||
yield return new ValidationResult($"{nameof(Type)} integrations are not yet supported.", new[] { nameof(Type) });
|
||||
break;
|
||||
case IntegrationType.Slack:
|
||||
yield return new ValidationResult($"{nameof(Type)} integrations cannot be created directly.", new[] { nameof(Type) });
|
||||
break;
|
||||
case IntegrationType.Webhook:
|
||||
if (Configuration is not null)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
"Webhook integrations must not include configuration.",
|
||||
new[] { nameof(Configuration) });
|
||||
}
|
||||
break;
|
||||
default:
|
||||
yield return new ValidationResult(
|
||||
$"Integration type '{Type}' is not recognized.",
|
||||
new[] { nameof(Type) });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
|
||||
public class OrganizationIntegrationConfigurationResponseModel : ResponseModel
|
||||
{
|
||||
public OrganizationIntegrationConfigurationResponseModel(OrganizationIntegrationConfiguration organizationIntegrationConfiguration, string obj = "organizationIntegrationConfiguration")
|
||||
: base(obj)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(organizationIntegrationConfiguration);
|
||||
|
||||
Id = organizationIntegrationConfiguration.Id;
|
||||
Configuration = organizationIntegrationConfiguration.Configuration;
|
||||
CreationDate = organizationIntegrationConfiguration.CreationDate;
|
||||
EventType = organizationIntegrationConfiguration.EventType;
|
||||
Template = organizationIntegrationConfiguration.Template;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
public string? Configuration { get; set; }
|
||||
public DateTime CreationDate { get; set; }
|
||||
public EventType EventType { get; set; }
|
||||
public string? Template { get; set; }
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
|
||||
public class OrganizationIntegrationResponseModel : ResponseModel
|
||||
{
|
||||
public OrganizationIntegrationResponseModel(OrganizationIntegration organizationIntegration, string obj = "organizationIntegration")
|
||||
: base(obj)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(organizationIntegration);
|
||||
|
||||
Id = organizationIntegration.Id;
|
||||
Type = organizationIntegration.Type;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
public IntegrationType Type { get; set; }
|
||||
}
|
@ -4,6 +4,7 @@ using Bit.Api.Auth.Models.Response.WebAuthn;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Api.Response.Accounts;
|
||||
@ -31,6 +32,8 @@ public class WebAuthnController : Controller
|
||||
private readonly ICreateWebAuthnLoginCredentialCommand _createWebAuthnLoginCredentialCommand;
|
||||
private readonly IAssertWebAuthnLoginCredentialCommand _assertWebAuthnLoginCredentialCommand;
|
||||
private readonly IGetWebAuthnLoginCredentialAssertionOptionsCommand _getWebAuthnLoginCredentialAssertionOptionsCommand;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public WebAuthnController(
|
||||
IUserService userService,
|
||||
@ -41,7 +44,9 @@ public class WebAuthnController : Controller
|
||||
IGetWebAuthnLoginCredentialCreateOptionsCommand getWebAuthnLoginCredentialCreateOptionsCommand,
|
||||
ICreateWebAuthnLoginCredentialCommand createWebAuthnLoginCredentialCommand,
|
||||
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand,
|
||||
IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand)
|
||||
IGetWebAuthnLoginCredentialAssertionOptionsCommand getWebAuthnLoginCredentialAssertionOptionsCommand,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_userService = userService;
|
||||
_policyService = policyService;
|
||||
@ -52,7 +57,8 @@ public class WebAuthnController : Controller
|
||||
_createWebAuthnLoginCredentialCommand = createWebAuthnLoginCredentialCommand;
|
||||
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;
|
||||
_getWebAuthnLoginCredentialAssertionOptionsCommand = getWebAuthnLoginCredentialAssertionOptionsCommand;
|
||||
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
@ -68,7 +74,7 @@ public class WebAuthnController : Controller
|
||||
public async Task<WebAuthnCredentialCreateOptionsResponseModel> AttestationOptions([FromBody] SecretVerificationRequestModel model)
|
||||
{
|
||||
var user = await VerifyUserAsync(model);
|
||||
await ValidateRequireSsoPolicyDisabledOrNotApplicable(user.Id);
|
||||
await ValidateIfUserCanUsePasskeyLogin(user.Id);
|
||||
var options = await _getWebAuthnLoginCredentialCreateOptionsCommand.GetWebAuthnLoginCredentialCreateOptionsAsync(user);
|
||||
|
||||
var tokenable = new WebAuthnCredentialCreateOptionsTokenable(user, options);
|
||||
@ -101,7 +107,7 @@ public class WebAuthnController : Controller
|
||||
public async Task Post([FromBody] WebAuthnLoginCredentialCreateRequestModel model)
|
||||
{
|
||||
var user = await GetUserAsync();
|
||||
await ValidateRequireSsoPolicyDisabledOrNotApplicable(user.Id);
|
||||
await ValidateIfUserCanUsePasskeyLogin(user.Id);
|
||||
var tokenable = _createOptionsDataProtector.Unprotect(model.Token);
|
||||
|
||||
if (!tokenable.TokenIsValid(user))
|
||||
@ -126,6 +132,22 @@ public class WebAuthnController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ValidateIfUserCanUsePasskeyLogin(Guid userId)
|
||||
{
|
||||
if (!_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements))
|
||||
{
|
||||
await ValidateRequireSsoPolicyDisabledOrNotApplicable(userId);
|
||||
return;
|
||||
}
|
||||
|
||||
var requireSsoPolicyRequirement = await _policyRequirementQuery.GetAsync<RequireSsoPolicyRequirement>(userId);
|
||||
|
||||
if (!requireSsoPolicyRequirement.CanUsePasskeyLogin)
|
||||
{
|
||||
throw new BadRequestException("Passkeys cannot be created for your account. SSO login is required.");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut()]
|
||||
public async Task UpdateCredential([FromBody] WebAuthnLoginCredentialUpdateRequestModel model)
|
||||
{
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -128,6 +128,7 @@ public class DevicesController : Controller
|
||||
}
|
||||
|
||||
[HttpPost("{identifier}/retrieve-keys")]
|
||||
[Obsolete("This endpoint is deprecated. The keys are on the regular device GET endpoints now.")]
|
||||
public async Task<ProtectedDeviceResponseModel> GetDeviceKeys(string identifier)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
|
@ -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}")]
|
||||
|
@ -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)]
|
||||
|
@ -2,6 +2,7 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.Models.Response;
|
||||
|
||||
@ -21,6 +22,8 @@ public class DeviceResponseModel : ResponseModel
|
||||
Identifier = device.Identifier;
|
||||
CreationDate = device.CreationDate;
|
||||
IsTrusted = device.IsTrusted();
|
||||
EncryptedUserKey = device.EncryptedUserKey;
|
||||
EncryptedPublicKey = device.EncryptedPublicKey;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
@ -29,4 +32,10 @@ public class DeviceResponseModel : ResponseModel
|
||||
public string Identifier { get; set; }
|
||||
public DateTime CreationDate { get; set; }
|
||||
public bool IsTrusted { get; set; }
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(2000)]
|
||||
public string EncryptedUserKey { get; set; }
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(2000)]
|
||||
public string EncryptedPublicKey { get; set; }
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ using Bit.Core.Settings;
|
||||
using AspNetCoreRateLimit;
|
||||
using Stripe;
|
||||
using Bit.Core.Utilities;
|
||||
using Duende.IdentityModel;
|
||||
using IdentityModel;
|
||||
using System.Globalization;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.Auth.Models.Request;
|
||||
@ -27,8 +27,10 @@ using Bit.Core.OrganizationFeatures.OrganizationSubscriptions;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||
using Bit.Core.AdminConsole.Services.NoopImplementations;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.Identity.TokenProviders;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.ImportFeatures;
|
||||
using Bit.Core.Tools.ReportFeatures;
|
||||
using Bit.Core.Auth.Models.Api.Request;
|
||||
@ -224,6 +226,19 @@ public class Startup
|
||||
{
|
||||
services.AddHostedService<Core.HostedServices.ApplicationCacheHostedService>();
|
||||
}
|
||||
|
||||
// Slack
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes))
|
||||
{
|
||||
services.AddHttpClient(SlackService.HttpClientName);
|
||||
services.AddSingleton<ISlackService, SlackService>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<ISlackService, NoopSlackService>();
|
||||
}
|
||||
}
|
||||
|
||||
public void Configure(
|
||||
|
@ -1,5 +1,7 @@
|
||||
using Bit.Billing.Constants;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Services;
|
||||
@ -15,19 +17,22 @@ public class PaymentMethodAttachedHandler : IPaymentMethodAttachedHandler
|
||||
private readonly IStripeFacade _stripeFacade;
|
||||
private readonly IStripeEventUtilityService _stripeEventUtilityService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
|
||||
public PaymentMethodAttachedHandler(
|
||||
ILogger<PaymentMethodAttachedHandler> logger,
|
||||
IStripeEventService stripeEventService,
|
||||
IStripeFacade stripeFacade,
|
||||
IStripeEventUtilityService stripeEventUtilityService,
|
||||
IFeatureService featureService)
|
||||
IFeatureService featureService,
|
||||
IProviderRepository providerRepository)
|
||||
{
|
||||
_logger = logger;
|
||||
_stripeEventService = stripeEventService;
|
||||
_stripeFacade = stripeFacade;
|
||||
_stripeEventUtilityService = stripeEventUtilityService;
|
||||
_featureService = featureService;
|
||||
_providerRepository = providerRepository;
|
||||
}
|
||||
|
||||
public async Task HandleAsync(Event parsedEvent)
|
||||
@ -68,7 +73,13 @@ public class PaymentMethodAttachedHandler : IPaymentMethodAttachedHandler
|
||||
* If we have an invoiced provider subscription where the customer hasn't been marked as invoice-approved,
|
||||
* we need to try and set the default payment method and update the collection method to be "charge_automatically".
|
||||
*/
|
||||
if (invoicedProviderSubscription != null && !customer.ApprovedToPayByInvoice())
|
||||
if (invoicedProviderSubscription != null &&
|
||||
!customer.ApprovedToPayByInvoice() &&
|
||||
Guid.TryParse(invoicedProviderSubscription.Metadata[StripeConstants.MetadataKeys.ProviderId], out var providerId))
|
||||
{
|
||||
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||
|
||||
if (provider is { Type: ProviderType.Msp })
|
||||
{
|
||||
if (customer.InvoiceSettings.DefaultPaymentMethodId != paymentMethod.Id)
|
||||
{
|
||||
@ -106,6 +117,7 @@ public class PaymentMethodAttachedHandler : IPaymentMethodAttachedHandler
|
||||
customer.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var unpaidSubscriptions = subscriptions?.Data.Where(subscription =>
|
||||
subscription.Status == StripeConstants.SubscriptionStatus.Unpaid).ToList();
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
public enum IntegrationType : int
|
||||
{
|
||||
Slack = 1,
|
||||
Webhook = 2,
|
||||
CloudBillingSync = 1,
|
||||
Scim = 2,
|
||||
Slack = 3,
|
||||
Webhook = 4,
|
||||
}
|
||||
|
@ -0,0 +1,3 @@
|
||||
namespace Bit.Core.Models.Data.Integrations;
|
||||
|
||||
public record SlackIntegration(string token);
|
@ -0,0 +1,3 @@
|
||||
namespace Bit.Core.Models.Data.Integrations;
|
||||
|
||||
public record SlackIntegrationConfiguration(string channelId);
|
@ -0,0 +1,3 @@
|
||||
namespace Bit.Core.Models.Data.Integrations;
|
||||
|
||||
public record SlackIntegrationConfigurationDetails(string channelId, string token);
|
@ -0,0 +1,3 @@
|
||||
namespace Bit.Core.Models.Data.Integrations;
|
||||
|
||||
public record WebhookIntegrationConfiguration(string url);
|
@ -0,0 +1,3 @@
|
||||
namespace Bit.Core.Models.Data.Integrations;
|
||||
|
||||
public record WebhookIntegrationConfigurationDetils(string url);
|
57
src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs
Normal file
57
src/Core/AdminConsole/Models/Slack/SlackApiResponse.cs
Normal file
@ -0,0 +1,57 @@
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Bit.Core.Models.Slack;
|
||||
|
||||
public abstract class SlackApiResponse
|
||||
{
|
||||
public bool Ok { get; set; }
|
||||
[JsonPropertyName("response_metadata")]
|
||||
public SlackResponseMetadata ResponseMetadata { get; set; } = new();
|
||||
public string Error { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class SlackResponseMetadata
|
||||
{
|
||||
[JsonPropertyName("next_cursor")]
|
||||
public string NextCursor { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class SlackChannelListResponse : SlackApiResponse
|
||||
{
|
||||
public List<SlackChannel> Channels { get; set; } = new();
|
||||
}
|
||||
|
||||
public class SlackUserResponse : SlackApiResponse
|
||||
{
|
||||
public SlackUser User { get; set; } = new();
|
||||
}
|
||||
|
||||
public class SlackOAuthResponse : SlackApiResponse
|
||||
{
|
||||
[JsonPropertyName("access_token")]
|
||||
public string AccessToken { get; set; } = string.Empty;
|
||||
public SlackTeam Team { get; set; } = new();
|
||||
}
|
||||
|
||||
public class SlackTeam
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class SlackChannel
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class SlackUser
|
||||
{
|
||||
public string Id { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class SlackDmResponse : SlackApiResponse
|
||||
{
|
||||
public SlackChannel Channel { get; set; } = new();
|
||||
}
|
@ -159,13 +159,13 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
||||
|
||||
private async Task RevertPasswordManagerChangesAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
||||
{
|
||||
if (validatedResult.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd > 0)
|
||||
if (validatedResult.Value.PasswordManagerSubscriptionUpdate is { Seats: > 0, SeatsRequiredToAdd: > 0 })
|
||||
{
|
||||
// When reverting seats, we have to tell payments service that the seats are going back down by what we attempted to add.
|
||||
// However, this might lead to a problem if we don't actually update stripe but throw any ways.
|
||||
// stripe could not be updated, and then we would decrement the number of seats in stripe accidentally.
|
||||
var seatsToRemove = validatedResult.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd;
|
||||
await paymentService.AdjustSeatsAsync(organization, validatedResult.Value.InviteOrganization.Plan, -seatsToRemove);
|
||||
|
||||
|
||||
await paymentService.AdjustSeatsAsync(organization,
|
||||
validatedResult.Value.InviteOrganization.Plan,
|
||||
validatedResult.Value.PasswordManagerSubscriptionUpdate.Seats.Value);
|
||||
|
||||
organization.Seats = (short?)validatedResult.Value.PasswordManagerSubscriptionUpdate.Seats;
|
||||
|
||||
@ -274,12 +274,11 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
||||
|
||||
private async Task AdjustPasswordManagerSeatsAsync(Valid<InviteOrganizationUsersValidationRequest> validatedResult, Organization organization)
|
||||
{
|
||||
if (validatedResult.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd <= 0)
|
||||
if (validatedResult.Value.PasswordManagerSubscriptionUpdate is { SeatsRequiredToAdd: > 0, UpdatedSeatTotal: > 0 })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await paymentService.AdjustSeatsAsync(organization, validatedResult.Value.InviteOrganization.Plan, validatedResult.Value.PasswordManagerSubscriptionUpdate.SeatsRequiredToAdd);
|
||||
await paymentService.AdjustSeatsAsync(organization,
|
||||
validatedResult.Value.InviteOrganization.Plan,
|
||||
validatedResult.Value.PasswordManagerSubscriptionUpdate.UpdatedSeatTotal.Value);
|
||||
|
||||
organization.Seats = (short?)validatedResult.Value.PasswordManagerSubscriptionUpdate.UpdatedSeatTotal;
|
||||
|
||||
@ -295,4 +294,5 @@ public class InviteOrganizationUsersCommand(IEventService eventService,
|
||||
PreviousSeats = validatedResult.Value.PasswordManagerSubscriptionUpdate.Seats
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,62 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Settings;
|
||||
|
||||
/// <summary>
|
||||
/// Policy requirements for the Require SSO policy.
|
||||
/// </summary>
|
||||
public class RequireSsoPolicyRequirement : IPolicyRequirement
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates whether the user can use passkey login.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The user can use passkey login if they are not a member (Accepted/Confirmed) of an organization
|
||||
/// that has the Require SSO policy enabled.
|
||||
/// </remarks>
|
||||
public bool CanUsePasskeyLogin { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Indicates whether SSO requirement is enforced for the user.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The user is required to login with SSO if they are a confirmed member of an organization
|
||||
/// that has the Require SSO policy enabled.
|
||||
/// </remarks>
|
||||
public bool SsoRequired { get; init; }
|
||||
}
|
||||
|
||||
|
||||
public class RequireSsoPolicyRequirementFactory : BasePolicyRequirementFactory<RequireSsoPolicyRequirement>
|
||||
{
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
|
||||
public RequireSsoPolicyRequirementFactory(GlobalSettings globalSettings)
|
||||
{
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
public override PolicyType PolicyType => PolicyType.RequireSso;
|
||||
|
||||
protected override IEnumerable<OrganizationUserType> ExemptRoles =>
|
||||
_globalSettings.Sso.EnforceSsoPolicyForAllUsers
|
||||
? Array.Empty<OrganizationUserType>()
|
||||
: [OrganizationUserType.Owner, OrganizationUserType.Admin];
|
||||
|
||||
public override RequireSsoPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
|
||||
{
|
||||
var result = new RequireSsoPolicyRequirement
|
||||
{
|
||||
CanUsePasskeyLogin = policyDetails.All(p =>
|
||||
p.OrganizationUserStatus == OrganizationUserStatusType.Revoked ||
|
||||
p.OrganizationUserStatus == OrganizationUserStatusType.Invited),
|
||||
|
||||
SsoRequired = policyDetails.Any(p =>
|
||||
p.OrganizationUserStatus == OrganizationUserStatusType.Confirmed)
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
@ -35,5 +35,6 @@ public static class PolicyServiceCollectionExtensions
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, SendOptionsPolicyRequirementFactory>();
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, ResetPasswordPolicyRequirementFactory>();
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, PersonalOwnershipPolicyRequirementFactory>();
|
||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, RequireSsoPolicyRequirementFactory>();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
11
src/Core/AdminConsole/Services/ISlackService.cs
Normal file
11
src/Core/AdminConsole/Services/ISlackService.cs
Normal file
@ -0,0 +1,11 @@
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
public interface ISlackService
|
||||
{
|
||||
Task<string> GetChannelIdAsync(string token, string channelName);
|
||||
Task<List<string>> GetChannelIdsAsync(string token, List<string> channelNames);
|
||||
Task<string> GetDmChannelByEmailAsync(string token, string email);
|
||||
string GetRedirectUrl(string redirectUrl);
|
||||
Task<string> ObtainTokenViaOAuth(string code, string redirectUrl);
|
||||
Task SendSlackMessageByChannelIdAsync(string token, string message, string channelId);
|
||||
}
|
@ -0,0 +1,46 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Utilities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Integrations;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
public class SlackEventHandler(
|
||||
IOrganizationIntegrationConfigurationRepository configurationRepository,
|
||||
ISlackService slackService)
|
||||
: IEventMessageHandler
|
||||
{
|
||||
public async Task HandleEventAsync(EventMessage eventMessage)
|
||||
{
|
||||
var organizationId = eventMessage.OrganizationId ?? Guid.Empty;
|
||||
var configurations = await configurationRepository.GetConfigurationDetailsAsync(
|
||||
organizationId,
|
||||
IntegrationType.Slack,
|
||||
eventMessage.Type);
|
||||
|
||||
foreach (var configuration in configurations)
|
||||
{
|
||||
var config = configuration.MergedConfiguration.Deserialize<SlackIntegrationConfigurationDetails>();
|
||||
if (config is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
await slackService.SendSlackMessageByChannelIdAsync(
|
||||
config.token,
|
||||
IntegrationTemplateProcessor.ReplaceTokens(configuration.Template, eventMessage),
|
||||
config.channelId
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task HandleManyEventsAsync(IEnumerable<EventMessage> eventMessages)
|
||||
{
|
||||
foreach (var eventMessage in eventMessages)
|
||||
{
|
||||
await HandleEventAsync(eventMessage);
|
||||
}
|
||||
}
|
||||
}
|
162
src/Core/AdminConsole/Services/Implementations/SlackService.cs
Normal file
162
src/Core/AdminConsole/Services/Implementations/SlackService.cs
Normal file
@ -0,0 +1,162 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Web;
|
||||
using Bit.Core.Models.Slack;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
public class SlackService(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
GlobalSettings globalSettings,
|
||||
ILogger<SlackService> logger) : ISlackService
|
||||
{
|
||||
private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName);
|
||||
private readonly string _clientId = globalSettings.Slack.ClientId;
|
||||
private readonly string _clientSecret = globalSettings.Slack.ClientSecret;
|
||||
private readonly string _scopes = globalSettings.Slack.Scopes;
|
||||
private readonly string _slackApiBaseUrl = globalSettings.Slack.ApiBaseUrl;
|
||||
|
||||
public const string HttpClientName = "SlackServiceHttpClient";
|
||||
|
||||
public async Task<string> GetChannelIdAsync(string token, string channelName)
|
||||
{
|
||||
return (await GetChannelIdsAsync(token, [channelName])).FirstOrDefault();
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetChannelIdsAsync(string token, List<string> channelNames)
|
||||
{
|
||||
var matchingChannelIds = new List<string>();
|
||||
var baseUrl = $"{_slackApiBaseUrl}/conversations.list";
|
||||
var nextCursor = string.Empty;
|
||||
|
||||
do
|
||||
{
|
||||
var uriBuilder = new UriBuilder(baseUrl);
|
||||
var queryParameters = HttpUtility.ParseQueryString(uriBuilder.Query);
|
||||
queryParameters["types"] = "public_channel,private_channel";
|
||||
queryParameters["limit"] = "1000";
|
||||
if (!string.IsNullOrEmpty(nextCursor))
|
||||
{
|
||||
queryParameters["cursor"] = nextCursor;
|
||||
}
|
||||
uriBuilder.Query = queryParameters.ToString();
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, uriBuilder.Uri);
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var result = await response.Content.ReadFromJsonAsync<SlackChannelListResponse>();
|
||||
|
||||
if (result is { Ok: true })
|
||||
{
|
||||
matchingChannelIds.AddRange(result.Channels
|
||||
.Where(channel => channelNames.Contains(channel.Name))
|
||||
.Select(channel => channel.Id));
|
||||
nextCursor = result.ResponseMetadata.NextCursor;
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.LogError("Error getting Channel Ids: {Error}", result.Error);
|
||||
nextCursor = string.Empty;
|
||||
}
|
||||
|
||||
} while (!string.IsNullOrEmpty(nextCursor));
|
||||
|
||||
return matchingChannelIds;
|
||||
}
|
||||
|
||||
public async Task<string> GetDmChannelByEmailAsync(string token, string email)
|
||||
{
|
||||
var userId = await GetUserIdByEmailAsync(token, email);
|
||||
return await OpenDmChannel(token, userId);
|
||||
}
|
||||
|
||||
public string GetRedirectUrl(string redirectUrl)
|
||||
{
|
||||
return $"https://slack.com/oauth/v2/authorize?client_id={_clientId}&scope={_scopes}&redirect_uri={redirectUrl}";
|
||||
}
|
||||
|
||||
public async Task<string> ObtainTokenViaOAuth(string code, string redirectUrl)
|
||||
{
|
||||
var tokenResponse = await _httpClient.PostAsync($"{_slackApiBaseUrl}/oauth.v2.access",
|
||||
new FormUrlEncodedContent(new[]
|
||||
{
|
||||
new KeyValuePair<string, string>("client_id", _clientId),
|
||||
new KeyValuePair<string, string>("client_secret", _clientSecret),
|
||||
new KeyValuePair<string, string>("code", code),
|
||||
new KeyValuePair<string, string>("redirect_uri", redirectUrl)
|
||||
}));
|
||||
|
||||
SlackOAuthResponse result;
|
||||
try
|
||||
{
|
||||
result = await tokenResponse.Content.ReadFromJsonAsync<SlackOAuthResponse>();
|
||||
}
|
||||
catch
|
||||
{
|
||||
result = null;
|
||||
}
|
||||
|
||||
if (result == null)
|
||||
{
|
||||
logger.LogError("Error obtaining token via OAuth: Unknown error");
|
||||
return string.Empty;
|
||||
}
|
||||
if (!result.Ok)
|
||||
{
|
||||
logger.LogError("Error obtaining token via OAuth: {Error}", result.Error);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return result.AccessToken;
|
||||
}
|
||||
|
||||
public async Task SendSlackMessageByChannelIdAsync(string token, string message, string channelId)
|
||||
{
|
||||
var payload = JsonContent.Create(new { channel = channelId, text = message });
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, $"{_slackApiBaseUrl}/chat.postMessage");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
request.Content = payload;
|
||||
|
||||
await _httpClient.SendAsync(request);
|
||||
}
|
||||
|
||||
private async Task<string> GetUserIdByEmailAsync(string token, string email)
|
||||
{
|
||||
var request = new HttpRequestMessage(HttpMethod.Get, $"{_slackApiBaseUrl}/users.lookupByEmail?email={email}");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var result = await response.Content.ReadFromJsonAsync<SlackUserResponse>();
|
||||
|
||||
if (!result.Ok)
|
||||
{
|
||||
logger.LogError("Error retrieving Slack user ID: {Error}", result.Error);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return result.User.Id;
|
||||
}
|
||||
|
||||
private async Task<string> OpenDmChannel(string token, string userId)
|
||||
{
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
return string.Empty;
|
||||
|
||||
var payload = JsonContent.Create(new { users = userId });
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, $"{_slackApiBaseUrl}/conversations.open");
|
||||
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
|
||||
request.Content = payload;
|
||||
var response = await _httpClient.SendAsync(request);
|
||||
var result = await response.Content.ReadFromJsonAsync<SlackDmResponse>();
|
||||
|
||||
if (!result.Ok)
|
||||
{
|
||||
logger.LogError("Error opening DM channel: {Error}", result.Error);
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return result.Channel.Id;
|
||||
}
|
||||
}
|
@ -1,30 +1,57 @@
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Utilities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Models.Data.Integrations;
|
||||
using Bit.Core.Repositories;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
public class WebhookEventHandler(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
GlobalSettings globalSettings)
|
||||
IOrganizationIntegrationConfigurationRepository configurationRepository)
|
||||
: IEventMessageHandler
|
||||
{
|
||||
private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName);
|
||||
private readonly string _webhookUrl = globalSettings.EventLogging.WebhookUrl;
|
||||
|
||||
public const string HttpClientName = "WebhookEventHandlerHttpClient";
|
||||
|
||||
public async Task HandleEventAsync(EventMessage eventMessage)
|
||||
{
|
||||
var content = JsonContent.Create(eventMessage);
|
||||
var response = await _httpClient.PostAsync(_webhookUrl, content);
|
||||
var organizationId = eventMessage.OrganizationId ?? Guid.Empty;
|
||||
var configurations = await configurationRepository.GetConfigurationDetailsAsync(
|
||||
organizationId,
|
||||
IntegrationType.Webhook,
|
||||
eventMessage.Type);
|
||||
|
||||
foreach (var configuration in configurations)
|
||||
{
|
||||
var config = configuration.MergedConfiguration.Deserialize<WebhookIntegrationConfigurationDetils>();
|
||||
if (config is null || string.IsNullOrEmpty(config.url))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var content = new StringContent(
|
||||
IntegrationTemplateProcessor.ReplaceTokens(configuration.Template, eventMessage),
|
||||
Encoding.UTF8,
|
||||
"application/json"
|
||||
);
|
||||
var response = await _httpClient.PostAsync(
|
||||
config.url,
|
||||
content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task HandleManyEventsAsync(IEnumerable<EventMessage> eventMessages)
|
||||
{
|
||||
var content = JsonContent.Create(eventMessages);
|
||||
var response = await _httpClient.PostAsync(_webhookUrl, content);
|
||||
response.EnsureSuccessStatusCode();
|
||||
foreach (var eventMessage in eventMessages)
|
||||
{
|
||||
await HandleEventAsync(eventMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,36 @@
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Services.NoopImplementations;
|
||||
|
||||
public class NoopSlackService : ISlackService
|
||||
{
|
||||
public Task<string> GetChannelIdAsync(string token, string channelName)
|
||||
{
|
||||
return Task.FromResult(string.Empty);
|
||||
}
|
||||
|
||||
public Task<List<string>> GetChannelIdsAsync(string token, List<string> channelNames)
|
||||
{
|
||||
return Task.FromResult(new List<string>());
|
||||
}
|
||||
|
||||
public Task<string> GetDmChannelByEmailAsync(string token, string email)
|
||||
{
|
||||
return Task.FromResult(string.Empty);
|
||||
}
|
||||
|
||||
public string GetRedirectUrl(string redirectUrl)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public Task SendSlackMessageByChannelIdAsync(string token, string message, string channelId)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task<string> ObtainTokenViaOAuth(string code, string redirectUrl)
|
||||
{
|
||||
return Task.FromResult(string.Empty);
|
||||
}
|
||||
}
|
@ -0,0 +1,23 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Utilities;
|
||||
|
||||
public static partial class IntegrationTemplateProcessor
|
||||
{
|
||||
[GeneratedRegex(@"#(\w+)#")]
|
||||
private static partial Regex TokenRegex();
|
||||
|
||||
public static string ReplaceTokens(string template, object values)
|
||||
{
|
||||
if (string.IsNullOrEmpty(template) || values == null)
|
||||
return template;
|
||||
|
||||
var type = values.GetType();
|
||||
return TokenRegex().Replace(template, match =>
|
||||
{
|
||||
var propertyName = match.Groups[1].Value;
|
||||
var property = type.GetProperty(propertyName);
|
||||
return property?.GetValue(values)?.ToString() ?? match.Value;
|
||||
});
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Core.Auth.Models.Api.Response;
|
||||
|
||||
@ -19,6 +20,8 @@ public class DeviceAuthRequestResponseModel : ResponseModel
|
||||
Identifier = deviceAuthDetails.Identifier,
|
||||
CreationDate = deviceAuthDetails.CreationDate,
|
||||
IsTrusted = deviceAuthDetails.IsTrusted,
|
||||
EncryptedPublicKey = deviceAuthDetails.EncryptedPublicKey,
|
||||
EncryptedUserKey = deviceAuthDetails.EncryptedUserKey
|
||||
};
|
||||
|
||||
if (deviceAuthDetails.AuthRequestId != null && deviceAuthDetails.AuthRequestCreatedAt != null)
|
||||
@ -39,6 +42,12 @@ public class DeviceAuthRequestResponseModel : ResponseModel
|
||||
public string Identifier { get; set; }
|
||||
public DateTime CreationDate { get; set; }
|
||||
public bool IsTrusted { get; set; }
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(2000)]
|
||||
public string EncryptedUserKey { get; set; }
|
||||
[EncryptedString]
|
||||
[EncryptedStringLength(2000)]
|
||||
public string EncryptedPublicKey { get; set; }
|
||||
|
||||
public PendingAuthRequest DevicePendingAuthRequest { get; set; }
|
||||
|
||||
|
@ -29,6 +29,8 @@ public class DeviceAuthDetails : Device
|
||||
Identifier = device.Identifier;
|
||||
CreationDate = device.CreationDate;
|
||||
IsTrusted = device.IsTrusted();
|
||||
EncryptedPublicKey = device.EncryptedPublicKey;
|
||||
EncryptedUserKey = device.EncryptedUserKey;
|
||||
AuthRequestId = authRequestId;
|
||||
AuthRequestCreatedAt = authRequestCreationDate;
|
||||
}
|
||||
@ -74,6 +76,8 @@ public class DeviceAuthDetails : Device
|
||||
EncryptedPrivateKey = encryptedPrivateKey,
|
||||
Active = active
|
||||
}.IsTrusted();
|
||||
EncryptedPublicKey = encryptedPublicKey;
|
||||
EncryptedUserKey = encryptedUserKey;
|
||||
AuthRequestId = authRequestId != Guid.Empty ? authRequestId : null;
|
||||
AuthRequestCreatedAt =
|
||||
authRequestCreationDate != DateTime.MinValue ? authRequestCreationDate : null;
|
||||
|
@ -112,7 +112,6 @@ public static class FeatureFlagKeys
|
||||
/* Auth Team */
|
||||
public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence";
|
||||
public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence";
|
||||
public const string DuoRedirect = "duo-redirect";
|
||||
public const string EmailVerification = "email-verification";
|
||||
public const string DeviceTrustLogging = "pm-8285-device-trust-logging";
|
||||
public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token";
|
||||
@ -176,6 +175,7 @@ public static class FeatureFlagKeys
|
||||
public const string PM3553_MobileSimpleLoginSelfHostAlias = "simple-login-self-host-alias";
|
||||
public const string EnablePMFlightRecorder = "enable-pm-flight-recorder";
|
||||
public const string MobileErrorReporting = "mobile-error-reporting";
|
||||
public const string AndroidChromeAutofill = "android-chrome-autofill";
|
||||
|
||||
/* Platform Team */
|
||||
public const string PersistPopupView = "persist-popup-view";
|
||||
@ -189,7 +189,6 @@ public static class FeatureFlagKeys
|
||||
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
|
||||
public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications";
|
||||
public const string DesktopSendUIRefresh = "desktop-send-ui-refresh";
|
||||
public const string ExportAttachments = "export-attachments";
|
||||
|
||||
/* Vault Team */
|
||||
public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge";
|
||||
@ -216,9 +215,6 @@ public static class FeatureFlagKeys
|
||||
public static Dictionary<string, string> GetLocalOverrideFlagValues()
|
||||
{
|
||||
// place overriding values when needed locally (offline), or return null
|
||||
return new Dictionary<string, string>()
|
||||
{
|
||||
{ DuoRedirect, "true" },
|
||||
};
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -52,7 +52,7 @@
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging.File" Version="3.0.0" />
|
||||
<PackageReference Include="Sentry.Serilog" Version="5.0.0" />
|
||||
<PackageReference Include="Duende.IdentityServer" Version="7.1.0" />
|
||||
<PackageReference Include="Duende.IdentityServer" Version="7.0.8" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="4.0.0" />
|
||||
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
|
||||
|
@ -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.");
|
||||
await organizationService.AutoAddSeatsAsync(sponsoringOrganization, 1);
|
||||
}
|
||||
|
||||
if (!sponsoringOrganization.UseAdminSponsoredFamilies)
|
||||
{
|
||||
throw new BadRequestException("Sponsoring organization cannot sponsor other Family organizations.");
|
||||
}
|
||||
|
||||
isAdminInitiated = true;
|
||||
}
|
||||
|
||||
sponsorship.IsAdminInitiated = isAdminInitiated;
|
||||
sponsorship.Notes = notes;
|
||||
|
||||
try
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ using Bit.Core.Models.Business;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Duende.IdentityModel;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
@ -53,6 +53,7 @@ public class GlobalSettings : IGlobalSettings
|
||||
public virtual SqlSettings PostgreSql { get; set; } = new SqlSettings();
|
||||
public virtual SqlSettings MySql { get; set; } = new SqlSettings();
|
||||
public virtual SqlSettings Sqlite { get; set; } = new SqlSettings() { ConnectionString = "Data Source=:memory:" };
|
||||
public virtual SlackSettings Slack { get; set; } = new SlackSettings();
|
||||
public virtual EventLoggingSettings EventLogging { get; set; } = new EventLoggingSettings();
|
||||
public virtual MailSettings Mail { get; set; } = new MailSettings();
|
||||
public virtual IConnectionStringSettings Storage { get; set; } = new ConnectionStringSettings();
|
||||
@ -272,10 +273,17 @@ public class GlobalSettings : IGlobalSettings
|
||||
}
|
||||
}
|
||||
|
||||
public class SlackSettings
|
||||
{
|
||||
public virtual string ApiBaseUrl { get; set; } = "https://slack.com/api";
|
||||
public virtual string ClientId { get; set; }
|
||||
public virtual string ClientSecret { get; set; }
|
||||
public virtual string Scopes { get; set; }
|
||||
}
|
||||
|
||||
public class EventLoggingSettings
|
||||
{
|
||||
public AzureServiceBusSettings AzureServiceBus { get; set; } = new AzureServiceBusSettings();
|
||||
public virtual string WebhookUrl { get; set; }
|
||||
public RabbitMqSettings RabbitMq { get; set; } = new RabbitMqSettings();
|
||||
|
||||
public class AzureServiceBusSettings
|
||||
@ -284,6 +292,7 @@ public class GlobalSettings : IGlobalSettings
|
||||
private string _topicName;
|
||||
|
||||
public virtual string EventRepositorySubscriptionName { get; set; } = "events-write-subscription";
|
||||
public virtual string SlackSubscriptionName { get; set; } = "events-slack-subscription";
|
||||
public virtual string WebhookSubscriptionName { get; set; } = "events-webhook-subscription";
|
||||
|
||||
public string ConnectionString
|
||||
@ -308,6 +317,7 @@ public class GlobalSettings : IGlobalSettings
|
||||
|
||||
public virtual string EventRepositoryQueueName { get; set; } = "events-write-queue";
|
||||
public virtual string WebhookQueueName { get; set; } = "events-webhook-queue";
|
||||
public virtual string SlackQueueName { get; set; } = "events-slack-queue";
|
||||
|
||||
public string HostName
|
||||
{
|
||||
|
@ -21,7 +21,7 @@ using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Settings;
|
||||
using Duende.IdentityModel;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using MimeKit;
|
||||
|
||||
|
@ -1,12 +1,13 @@
|
||||
using System.Globalization;
|
||||
using Bit.Core.AdminConsole.Services.Implementations;
|
||||
using Bit.Core.AdminConsole.Services.NoopImplementations;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Duende.IdentityModel;
|
||||
using IdentityModel;
|
||||
|
||||
namespace Bit.Events;
|
||||
|
||||
@ -117,11 +118,27 @@ public class Startup
|
||||
globalSettings,
|
||||
globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName));
|
||||
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.WebhookUrl))
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes))
|
||||
{
|
||||
services.AddSingleton<WebhookEventHandler>();
|
||||
services.AddHttpClient(WebhookEventHandler.HttpClientName);
|
||||
services.AddHttpClient(SlackService.HttpClientName);
|
||||
services.AddSingleton<ISlackService, SlackService>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<ISlackService, NoopSlackService>();
|
||||
}
|
||||
services.AddSingleton<SlackEventHandler>();
|
||||
services.AddSingleton<IHostedService>(provider =>
|
||||
new RabbitMqEventListenerService(
|
||||
provider.GetRequiredService<SlackEventHandler>(),
|
||||
provider.GetRequiredService<ILogger<RabbitMqEventListenerService>>(),
|
||||
globalSettings,
|
||||
globalSettings.EventLogging.RabbitMq.SlackQueueName));
|
||||
|
||||
services.AddHttpClient(WebhookEventHandler.HttpClientName);
|
||||
services.AddSingleton<WebhookEventHandler>();
|
||||
services.AddSingleton<IHostedService>(provider =>
|
||||
new RabbitMqEventListenerService(
|
||||
provider.GetRequiredService<WebhookEventHandler>(),
|
||||
@ -130,7 +147,6 @@ public class Startup
|
||||
globalSettings.EventLogging.RabbitMq.WebhookQueueName));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Configure(
|
||||
IApplicationBuilder app,
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Globalization;
|
||||
using Bit.Core.AdminConsole.Services.NoopImplementations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
@ -29,6 +30,12 @@ public class Startup
|
||||
// Settings
|
||||
var globalSettings = services.AddGlobalSettingsServices(Configuration, Environment);
|
||||
|
||||
// Data Protection
|
||||
services.AddCustomDataProtectionServices(Environment, globalSettings);
|
||||
|
||||
// Repositories
|
||||
services.AddDatabaseRepositories(globalSettings);
|
||||
|
||||
// Hosted Services
|
||||
|
||||
// Optional Azure Service Bus Listeners
|
||||
@ -45,8 +52,25 @@ public class Startup
|
||||
globalSettings,
|
||||
globalSettings.EventLogging.AzureServiceBus.EventRepositorySubscriptionName));
|
||||
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.WebhookUrl))
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes))
|
||||
{
|
||||
services.AddHttpClient(SlackService.HttpClientName);
|
||||
services.AddSingleton<ISlackService, SlackService>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<ISlackService, NoopSlackService>();
|
||||
}
|
||||
services.AddSingleton<SlackEventHandler>();
|
||||
services.AddSingleton<IHostedService>(provider =>
|
||||
new AzureServiceBusEventListenerService(
|
||||
provider.GetRequiredService<SlackEventHandler>(),
|
||||
provider.GetRequiredService<ILogger<AzureServiceBusEventListenerService>>(),
|
||||
globalSettings,
|
||||
globalSettings.EventLogging.AzureServiceBus.SlackSubscriptionName));
|
||||
|
||||
services.AddSingleton<WebhookEventHandler>();
|
||||
services.AddHttpClient(WebhookEventHandler.HttpClientName);
|
||||
|
||||
@ -57,7 +81,6 @@ public class Startup
|
||||
globalSettings,
|
||||
globalSettings.EventLogging.AzureServiceBus.WebhookSubscriptionName));
|
||||
}
|
||||
}
|
||||
services.AddHostedService<AzureQueueHostedService>();
|
||||
}
|
||||
|
||||
|
@ -5,9 +5,9 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Identity.Models;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer;
|
||||
using Duende.IdentityServer.Services;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Localization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
@ -1,7 +1,7 @@
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer.Models;
|
||||
using IdentityModel;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
|
@ -12,9 +12,9 @@ using Bit.Core.SecretsManager.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using IdentityModel;
|
||||
|
||||
namespace Bit.Identity.IdentityServer;
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
@ -39,6 +40,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
protected ISsoConfigRepository SsoConfigRepository { get; }
|
||||
protected IUserService _userService { get; }
|
||||
protected IUserDecryptionOptionsBuilder UserDecryptionOptionsBuilder { get; }
|
||||
protected IPolicyRequirementQuery PolicyRequirementQuery { get; }
|
||||
|
||||
public BaseRequestValidator(
|
||||
UserManager<User> userManager,
|
||||
@ -55,7 +57,8 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
IPolicyService policyService,
|
||||
IFeatureService featureService,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||
IPolicyRequirementQuery policyRequirementQuery)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_userService = userService;
|
||||
@ -72,6 +75,7 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
FeatureService = featureService;
|
||||
SsoConfigRepository = ssoConfigRepository;
|
||||
UserDecryptionOptionsBuilder = userDecryptionOptionsBuilder;
|
||||
PolicyRequirementQuery = policyRequirementQuery;
|
||||
}
|
||||
|
||||
protected async Task ValidateAsync(T context, ValidatedTokenRequest request,
|
||||
@ -348,9 +352,12 @@ public abstract class BaseRequestValidator<T> where T : class
|
||||
}
|
||||
|
||||
// Check if user belongs to any organization with an active SSO policy
|
||||
var anySsoPoliciesApplicableToUser = await PolicyService.AnyPoliciesApplicableToUserAsync(
|
||||
var ssoRequired = FeatureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||
? (await PolicyRequirementQuery.GetAsync<RequireSsoPolicyRequirement>(user.Id))
|
||||
.SsoRequired
|
||||
: await PolicyService.AnyPoliciesApplicableToUserAsync(
|
||||
user.Id, PolicyType.RequireSso, OrganizationUserStatusType.Confirmed);
|
||||
if (anySsoPoliciesApplicableToUser)
|
||||
if (ssoRequired)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Diagnostics;
|
||||
using System.Security.Claims;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Models.Api.Response;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
@ -11,10 +12,10 @@ using Bit.Core.Platform.Installations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer.Extensions;
|
||||
using Duende.IdentityServer.Validation;
|
||||
using HandlebarsDotNet;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
||||
#nullable enable
|
||||
@ -43,8 +44,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
IFeatureService featureService,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||
IUpdateInstallationCommand updateInstallationCommand
|
||||
)
|
||||
IUpdateInstallationCommand updateInstallationCommand,
|
||||
IPolicyRequirementQuery policyRequirementQuery)
|
||||
: base(
|
||||
userManager,
|
||||
userService,
|
||||
@ -60,7 +61,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
|
||||
policyService,
|
||||
featureService,
|
||||
ssoConfigRepository,
|
||||
userDecryptionOptionsBuilder)
|
||||
userDecryptionOptionsBuilder,
|
||||
policyRequirementQuery)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_updateInstallationCommand = updateInstallationCommand;
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Auth.Services;
|
||||
@ -40,7 +41,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
IPolicyService policyService,
|
||||
IFeatureService featureService,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||
IPolicyRequirementQuery policyRequirementQuery)
|
||||
: base(
|
||||
userManager,
|
||||
userService,
|
||||
@ -56,7 +58,8 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
|
||||
policyService,
|
||||
featureService,
|
||||
ssoConfigRepository,
|
||||
userDecryptionOptionsBuilder)
|
||||
userDecryptionOptionsBuilder,
|
||||
policyRequirementQuery)
|
||||
{
|
||||
_userManager = userManager;
|
||||
_currentContext = currentContext;
|
||||
|
@ -1,6 +1,7 @@
|
||||
using System.Security.Claims;
|
||||
using System.Text.Json;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||
@ -44,7 +45,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,
|
||||
IFeatureService featureService,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand)
|
||||
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand,
|
||||
IPolicyRequirementQuery policyRequirementQuery)
|
||||
: base(
|
||||
userManager,
|
||||
userService,
|
||||
@ -60,7 +62,8 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
policyService,
|
||||
featureService,
|
||||
ssoConfigRepository,
|
||||
userDecryptionOptionsBuilder)
|
||||
userDecryptionOptionsBuilder,
|
||||
policyRequirementQuery)
|
||||
{
|
||||
_assertionOptionsDataProtector = assertionOptionsDataProtector;
|
||||
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;
|
||||
|
@ -41,6 +41,8 @@ public static class DapperServiceCollectionExtensions
|
||||
services.AddSingleton<IMaintenanceRepository, MaintenanceRepository>();
|
||||
services.AddSingleton<IOrganizationApiKeyRepository, OrganizationApiKeyRepository>();
|
||||
services.AddSingleton<IOrganizationConnectionRepository, OrganizationConnectionRepository>();
|
||||
services.AddSingleton<IOrganizationIntegrationConfigurationRepository, OrganizationIntegrationConfigurationRepository>();
|
||||
services.AddSingleton<IOrganizationIntegrationRepository, OrganizationIntegrationRepository>();
|
||||
services.AddSingleton<IOrganizationRepository, OrganizationRepository>();
|
||||
services.AddSingleton<IOrganizationSponsorshipRepository, OrganizationSponsorshipRepository>();
|
||||
services.AddSingleton<IOrganizationUserRepository, OrganizationUserRepository>();
|
||||
|
@ -12,6 +12,12 @@ public class OrganizationIntegrationConfigurationEntityTypeConfiguration : IEnti
|
||||
.Property(oic => oic.Id)
|
||||
.ValueGeneratedNever();
|
||||
|
||||
builder
|
||||
.HasOne(oic => oic.OrganizationIntegration)
|
||||
.WithMany()
|
||||
.HasForeignKey(oic => oic.OrganizationIntegrationId)
|
||||
.OnDelete(DeleteBehavior.Cascade);
|
||||
|
||||
builder.ToTable(nameof(OrganizationIntegrationConfiguration));
|
||||
}
|
||||
}
|
||||
|
@ -14,9 +14,23 @@ public class OrganizationUserReadOccupiedSeatCountByOrganizationIdQuery : IQuery
|
||||
|
||||
public IQueryable<OrganizationUser> Run(DatabaseContext dbContext)
|
||||
{
|
||||
var query = from ou in dbContext.OrganizationUsers
|
||||
var orgUsersQuery = from ou in dbContext.OrganizationUsers
|
||||
where ou.OrganizationId == _organizationId && ou.Status >= OrganizationUserStatusType.Invited
|
||||
select ou;
|
||||
return query;
|
||||
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 &&
|
||||
!os.ToDelete
|
||||
select new OrganizationUser
|
||||
{
|
||||
Id = os.Id,
|
||||
OrganizationId = _organizationId,
|
||||
Status = OrganizationUserStatusType.Invited
|
||||
};
|
||||
|
||||
return orgUsersQuery.Concat(sponsorshipsQuery);
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.SharedWeb.Utilities;
|
||||
using Duende.IdentityModel;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.IdentityModel.Logging;
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
using Duende.IdentityModel;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace Bit.Notifications;
|
||||
|
@ -50,7 +50,7 @@ using Bit.Core.Vault.Services;
|
||||
using Bit.Infrastructure.Dapper;
|
||||
using Bit.Infrastructure.EntityFramework;
|
||||
using DnsClient;
|
||||
using Duende.IdentityModel;
|
||||
using IdentityModel;
|
||||
using LaunchDarkly.Sdk.Server;
|
||||
using LaunchDarkly.Sdk.Server.Interfaces;
|
||||
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||
|
@ -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
|
||||
);
|
||||
|
||||
|
||||
|
@ -14,24 +14,27 @@ BEGIN
|
||||
EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate
|
||||
|
||||
-- Groups
|
||||
;WITH [AvailableGroupsCTE] AS(
|
||||
SELECT
|
||||
Id
|
||||
FROM
|
||||
[dbo].[Group]
|
||||
WHERE
|
||||
OrganizationId = @OrganizationId
|
||||
)
|
||||
MERGE
|
||||
[dbo].[CollectionGroup] AS [Target]
|
||||
USING
|
||||
@Groups AS [Source]
|
||||
ON
|
||||
[Target].[CollectionId] = @Id
|
||||
AND [Target].[GroupId] = [Source].[Id]
|
||||
WHEN NOT MATCHED BY TARGET
|
||||
AND [Source].[Id] IN (SELECT [Id] FROM [AvailableGroupsCTE]) THEN
|
||||
INSERT -- Add explicit column list
|
||||
-- Delete groups that are no longer in source
|
||||
DELETE cg
|
||||
FROM [dbo].[CollectionGroup] cg
|
||||
LEFT JOIN @Groups g ON cg.GroupId = g.Id
|
||||
WHERE cg.CollectionId = @Id
|
||||
AND g.Id IS NULL;
|
||||
|
||||
-- Update existing groups
|
||||
UPDATE cg
|
||||
SET cg.ReadOnly = g.ReadOnly,
|
||||
cg.HidePasswords = g.HidePasswords,
|
||||
cg.Manage = g.Manage
|
||||
FROM [dbo].[CollectionGroup] cg
|
||||
INNER JOIN @Groups g ON cg.GroupId = g.Id
|
||||
WHERE cg.CollectionId = @Id
|
||||
AND (cg.ReadOnly != g.ReadOnly
|
||||
OR cg.HidePasswords != g.HidePasswords
|
||||
OR cg.Manage != g.Manage);
|
||||
|
||||
-- Insert new groups
|
||||
INSERT INTO [dbo].[CollectionGroup]
|
||||
(
|
||||
[CollectionId],
|
||||
[GroupId],
|
||||
@ -39,46 +42,41 @@ BEGIN
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
SELECT
|
||||
@Id,
|
||||
[Source].[Id],
|
||||
[Source].[ReadOnly],
|
||||
[Source].[HidePasswords],
|
||||
[Source].[Manage]
|
||||
)
|
||||
WHEN MATCHED AND (
|
||||
[Target].[ReadOnly] != [Source].[ReadOnly]
|
||||
OR [Target].[HidePasswords] != [Source].[HidePasswords]
|
||||
OR [Target].[Manage] != [Source].[Manage]
|
||||
) THEN
|
||||
UPDATE SET [Target].[ReadOnly] = [Source].[ReadOnly],
|
||||
[Target].[HidePasswords] = [Source].[HidePasswords],
|
||||
[Target].[Manage] = [Source].[Manage]
|
||||
WHEN NOT MATCHED BY SOURCE
|
||||
AND [Target].[CollectionId] = @Id THEN
|
||||
DELETE
|
||||
;
|
||||
g.Id,
|
||||
g.ReadOnly,
|
||||
g.HidePasswords,
|
||||
g.Manage
|
||||
FROM @Groups g
|
||||
INNER JOIN [dbo].[Group] grp ON grp.Id = g.Id
|
||||
LEFT JOIN [dbo].[CollectionGroup] cg
|
||||
ON cg.CollectionId = @Id AND cg.GroupId = g.Id
|
||||
WHERE grp.OrganizationId = @OrganizationId
|
||||
AND cg.CollectionId IS NULL;
|
||||
|
||||
-- Users
|
||||
;WITH [AvailableGroupsCTE] AS(
|
||||
SELECT
|
||||
Id
|
||||
FROM
|
||||
[dbo].[OrganizationUser]
|
||||
WHERE
|
||||
OrganizationId = @OrganizationId
|
||||
)
|
||||
MERGE
|
||||
[dbo].[CollectionUser] AS [Target]
|
||||
USING
|
||||
@Users AS [Source]
|
||||
ON
|
||||
[Target].[CollectionId] = @Id
|
||||
AND [Target].[OrganizationUserId] = [Source].[Id]
|
||||
WHEN NOT MATCHED BY TARGET
|
||||
AND [Source].[Id] IN (SELECT [Id] FROM [AvailableGroupsCTE]) THEN
|
||||
INSERT
|
||||
-- Delete users that are no longer in source
|
||||
DELETE cu
|
||||
FROM [dbo].[CollectionUser] cu
|
||||
LEFT JOIN @Users u ON cu.OrganizationUserId = u.Id
|
||||
WHERE cu.CollectionId = @Id
|
||||
AND u.Id IS NULL;
|
||||
|
||||
-- Update existing users
|
||||
UPDATE cu
|
||||
SET cu.ReadOnly = u.ReadOnly,
|
||||
cu.HidePasswords = u.HidePasswords,
|
||||
cu.Manage = u.Manage
|
||||
FROM [dbo].[CollectionUser] cu
|
||||
INNER JOIN @Users u ON cu.OrganizationUserId = u.Id
|
||||
WHERE cu.CollectionId = @Id
|
||||
AND (cu.ReadOnly != u.ReadOnly
|
||||
OR cu.HidePasswords != u.HidePasswords
|
||||
OR cu.Manage != u.Manage);
|
||||
|
||||
-- Insert new users
|
||||
INSERT INTO [dbo].[CollectionUser]
|
||||
(
|
||||
[CollectionId],
|
||||
[OrganizationUserId],
|
||||
@ -86,26 +84,18 @@ BEGIN
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
SELECT
|
||||
@Id,
|
||||
[Source].[Id],
|
||||
[Source].[ReadOnly],
|
||||
[Source].[HidePasswords],
|
||||
[Source].[Manage]
|
||||
)
|
||||
WHEN MATCHED AND (
|
||||
[Target].[ReadOnly] != [Source].[ReadOnly]
|
||||
OR [Target].[HidePasswords] != [Source].[HidePasswords]
|
||||
OR [Target].[Manage] != [Source].[Manage]
|
||||
) THEN
|
||||
UPDATE SET [Target].[ReadOnly] = [Source].[ReadOnly],
|
||||
[Target].[HidePasswords] = [Source].[HidePasswords],
|
||||
[Target].[Manage] = [Source].[Manage]
|
||||
WHEN NOT MATCHED BY SOURCE
|
||||
AND [Target].[CollectionId] = @Id THEN
|
||||
DELETE
|
||||
;
|
||||
u.Id,
|
||||
u.ReadOnly,
|
||||
u.HidePasswords,
|
||||
u.Manage
|
||||
FROM @Users u
|
||||
INNER JOIN [dbo].[OrganizationUser] ou ON ou.Id = u.Id
|
||||
LEFT JOIN [dbo].[CollectionUser] cu
|
||||
ON cu.CollectionId = @Id AND cu.OrganizationUserId = u.Id
|
||||
WHERE ou.OrganizationId = @OrganizationId
|
||||
AND cu.CollectionId IS NULL;
|
||||
|
||||
EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId
|
||||
END
|
||||
|
@ -5,10 +5,19 @@ BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
SELECT
|
||||
COUNT(1)
|
||||
FROM
|
||||
[dbo].[OrganizationUserView]
|
||||
WHERE
|
||||
OrganizationId = @OrganizationId
|
||||
(
|
||||
-- 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
|
||||
|
@ -9,3 +9,9 @@
|
||||
CONSTRAINT [FK_CollectionGroup_Group] FOREIGN KEY ([GroupId]) REFERENCES [dbo].[Group] ([Id]) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
GO
|
||||
CREATE NONCLUSTERED INDEX IX_CollectionGroup_GroupId
|
||||
ON [dbo].[CollectionGroup] (GroupId)
|
||||
INCLUDE (ReadOnly, HidePasswords, Manage)
|
||||
|
||||
GO
|
||||
|
@ -9,3 +9,9 @@
|
||||
CONSTRAINT [FK_CollectionUser_OrganizationUser] FOREIGN KEY ([OrganizationUserId]) REFERENCES [dbo].[OrganizationUser] ([Id])
|
||||
);
|
||||
|
||||
GO
|
||||
CREATE NONCLUSTERED INDEX IX_CollectionUser_OrganizationUserId
|
||||
ON [dbo].[CollectionUser] (OrganizationUserId)
|
||||
INCLUDE (ReadOnly, HidePasswords, Manage)
|
||||
|
||||
GO
|
||||
|
@ -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
|
||||
|
||||
|
@ -0,0 +1,201 @@
|
||||
using Bit.Api.AdminConsole.Controllers;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Controllers;
|
||||
|
||||
[ControllerCustomize(typeof(OrganizationIntegrationController))]
|
||||
[SutProviderCustomize]
|
||||
public class OrganizationIntegrationControllerTests
|
||||
{
|
||||
private OrganizationIntegrationRequestModel _webhookRequestModel = new OrganizationIntegrationRequestModel()
|
||||
{
|
||||
Configuration = null,
|
||||
Type = IntegrationType.Webhook
|
||||
};
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_Webhook_AllParamsProvided_Succeeds(
|
||||
SutProvider<OrganizationIntegrationController> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.CreateAsync(Arg.Any<OrganizationIntegration>())
|
||||
.Returns(callInfo => callInfo.Arg<OrganizationIntegration>());
|
||||
var response = await sutProvider.Sut.CreateAsync(organizationId, _webhookRequestModel);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||
.CreateAsync(Arg.Any<OrganizationIntegration>());
|
||||
Assert.IsType<OrganizationIntegrationResponseModel>(response);
|
||||
Assert.Equal(IntegrationType.Webhook, response.Type);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider<OrganizationIntegrationController> sutProvider, Guid organizationId)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(false);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(organizationId, _webhookRequestModel));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteAsync_AllParamsProvided_Succeeds(
|
||||
SutProvider<OrganizationIntegrationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration)
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegration);
|
||||
|
||||
await sutProvider.Sut.DeleteAsync(organizationId, organizationIntegration.Id);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||
.GetByIdAsync(organizationIntegration.Id);
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||
.DeleteAsync(organizationIntegration);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
|
||||
SutProvider<OrganizationIntegrationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration)
|
||||
{
|
||||
organizationIntegration.OrganizationId = Guid.NewGuid();
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.ReturnsNull();
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, Guid.Empty));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteAsync_IntegrationDoesNotExist_ThrowsNotFound(
|
||||
SutProvider<OrganizationIntegrationController> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.ReturnsNull();
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, Guid.Empty));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(
|
||||
SutProvider<OrganizationIntegrationController> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(false);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, Guid.Empty));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_AllParamsProvided_Succeeds(
|
||||
SutProvider<OrganizationIntegrationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration)
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegration.Type = IntegrationType.Webhook;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegration);
|
||||
|
||||
var response = await sutProvider.Sut.UpdateAsync(organizationId, organizationIntegration.Id, _webhookRequestModel);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||
.GetByIdAsync(organizationIntegration.Id);
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||
.ReplaceAsync(organizationIntegration);
|
||||
Assert.IsType<OrganizationIntegrationResponseModel>(response);
|
||||
Assert.Equal(IntegrationType.Webhook, response.Type);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
|
||||
SutProvider<OrganizationIntegrationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration)
|
||||
{
|
||||
organizationIntegration.OrganizationId = Guid.NewGuid();
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.ReturnsNull();
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UpdateAsync(organizationId, Guid.Empty, _webhookRequestModel));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_IntegrationDoesNotExist_ThrowsNotFound(
|
||||
SutProvider<OrganizationIntegrationController> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.ReturnsNull();
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UpdateAsync(organizationId, Guid.Empty, _webhookRequestModel));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(
|
||||
SutProvider<OrganizationIntegrationController> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(false);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UpdateAsync(organizationId, Guid.Empty, _webhookRequestModel));
|
||||
}
|
||||
}
|
@ -0,0 +1,621 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Api.AdminConsole.Controllers;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Data.Integrations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ReturnsExtensions;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Controllers;
|
||||
|
||||
[ControllerCustomize(typeof(OrganizationIntegrationConfigurationController))]
|
||||
[SutProviderCustomize]
|
||||
public class OrganizationIntegrationsConfigurationControllerTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteAsync_AllParamsProvided_Succeeds(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration,
|
||||
OrganizationIntegrationConfiguration organizationIntegrationConfiguration)
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegration);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegrationConfiguration);
|
||||
|
||||
await sutProvider.Sut.DeleteAsync(organizationId, organizationIntegration.Id, organizationIntegrationConfiguration.Id);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||
.GetByIdAsync(organizationIntegration.Id);
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
|
||||
.GetByIdAsync(organizationIntegrationConfiguration.Id);
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
|
||||
.DeleteAsync(organizationIntegrationConfiguration);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteAsync_IntegrationConfigurationDoesNotExist_ThrowsNotFound(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration)
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegration);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.ReturnsNull();
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, Guid.Empty, Guid.Empty));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteAsync_IntegrationDoesNotExist_ThrowsNotFound(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.ReturnsNull();
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, Guid.Empty, Guid.Empty));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegration);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, organizationIntegration.Id, Guid.Empty));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteAsync_IntegrationConfigDoesNotBelongToIntegration_ThrowsNotFound(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration,
|
||||
OrganizationIntegrationConfiguration organizationIntegrationConfiguration)
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegrationConfiguration.OrganizationIntegrationId = Guid.Empty;
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegration);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegrationConfiguration);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, organizationIntegration.Id, Guid.Empty));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task DeleteAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(false);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.DeleteAsync(organizationId, Guid.Empty, Guid.Empty));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostAsync_AllParamsProvided_Slack_Succeeds(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration,
|
||||
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
|
||||
OrganizationIntegrationConfigurationRequestModel model)
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegration.Type = IntegrationType.Slack;
|
||||
var slackConfig = new SlackIntegrationConfiguration(channelId: "C123456");
|
||||
model.Configuration = JsonSerializer.Serialize(slackConfig);
|
||||
model.Template = "Template String";
|
||||
|
||||
var expected = new OrganizationIntegrationConfigurationResponseModel(organizationIntegrationConfiguration);
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegration);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
|
||||
.Returns(organizationIntegrationConfiguration);
|
||||
var requestAction = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
|
||||
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
|
||||
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(requestAction);
|
||||
Assert.Equal(expected.Id, requestAction.Id);
|
||||
Assert.Equal(expected.Configuration, requestAction.Configuration);
|
||||
Assert.Equal(expected.EventType, requestAction.EventType);
|
||||
Assert.Equal(expected.Template, requestAction.Template);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostAsync_AllParamsProvided_Webhook_Succeeds(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration,
|
||||
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
|
||||
OrganizationIntegrationConfigurationRequestModel model)
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegration.Type = IntegrationType.Webhook;
|
||||
var webhookConfig = new WebhookIntegrationConfiguration(url: "https://localhost");
|
||||
model.Configuration = JsonSerializer.Serialize(webhookConfig);
|
||||
model.Template = "Template String";
|
||||
|
||||
var expected = new OrganizationIntegrationConfigurationResponseModel(organizationIntegrationConfiguration);
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegration);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
|
||||
.Returns(organizationIntegrationConfiguration);
|
||||
var requestAction = await sutProvider.Sut.CreateAsync(organizationId, organizationIntegration.Id, model);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
|
||||
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>());
|
||||
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(requestAction);
|
||||
Assert.Equal(expected.Id, requestAction.Id);
|
||||
Assert.Equal(expected.Configuration, requestAction.Configuration);
|
||||
Assert.Equal(expected.EventType, requestAction.EventType);
|
||||
Assert.Equal(expected.Template, requestAction.Template);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostAsync_IntegrationTypeCloudBillingSync_ThrowsBadRequestException(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration,
|
||||
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
|
||||
OrganizationIntegrationConfigurationRequestModel model)
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegration.Type = IntegrationType.CloudBillingSync;
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegration);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
|
||||
.Returns(organizationIntegrationConfiguration);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(
|
||||
organizationId,
|
||||
organizationIntegration.Id,
|
||||
model));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostAsync_IntegrationTypeScim_ThrowsBadRequestException(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration,
|
||||
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
|
||||
OrganizationIntegrationConfigurationRequestModel model)
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegration.Type = IntegrationType.Scim;
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegration);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
|
||||
.Returns(organizationIntegrationConfiguration);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(
|
||||
organizationId,
|
||||
organizationIntegration.Id,
|
||||
model));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostAsync_IntegrationDoesNotExist_ThrowsNotFound(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.ReturnsNull();
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(
|
||||
organizationId,
|
||||
Guid.Empty,
|
||||
new OrganizationIntegrationConfigurationRequestModel()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegration);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(
|
||||
organizationId,
|
||||
organizationIntegration.Id,
|
||||
new OrganizationIntegrationConfigurationRequestModel()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostAsync_InvalidConfiguration_ThrowsBadRequestException(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration,
|
||||
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
|
||||
OrganizationIntegrationConfigurationRequestModel model)
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegration.Type = IntegrationType.Webhook;
|
||||
model.Configuration = null;
|
||||
model.Template = "Template String";
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegration);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
|
||||
.Returns(organizationIntegrationConfiguration);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(
|
||||
organizationId,
|
||||
organizationIntegration.Id,
|
||||
model));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostAsync_InvalidTemplate_ThrowsBadRequestException(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration,
|
||||
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
|
||||
OrganizationIntegrationConfigurationRequestModel model)
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegration.Type = IntegrationType.Webhook;
|
||||
var webhookConfig = new WebhookIntegrationConfiguration(url: "https://localhost");
|
||||
model.Configuration = JsonSerializer.Serialize(webhookConfig);
|
||||
model.Template = null;
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegration);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
.CreateAsync(Arg.Any<OrganizationIntegrationConfiguration>())
|
||||
.Returns(organizationIntegrationConfiguration);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(
|
||||
organizationId,
|
||||
organizationIntegration.Id,
|
||||
model));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task PostAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider<OrganizationIntegrationConfigurationController> sutProvider, Guid organizationId)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(false);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(organizationId, Guid.Empty, new OrganizationIntegrationConfigurationRequestModel()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_AllParamsProvided_Slack_Succeeds(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration,
|
||||
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
|
||||
OrganizationIntegrationConfigurationRequestModel model)
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
|
||||
organizationIntegration.Type = IntegrationType.Slack;
|
||||
var slackConfig = new SlackIntegrationConfiguration(channelId: "C123456");
|
||||
model.Configuration = JsonSerializer.Serialize(slackConfig);
|
||||
model.Template = "Template String";
|
||||
|
||||
var expected = new OrganizationIntegrationConfigurationResponseModel(model.ToOrganizationIntegrationConfiguration(organizationIntegrationConfiguration));
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegration);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegrationConfiguration);
|
||||
var requestAction = await sutProvider.Sut.UpdateAsync(
|
||||
organizationId,
|
||||
organizationIntegration.Id,
|
||||
organizationIntegrationConfiguration.Id,
|
||||
model);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
|
||||
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
|
||||
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(requestAction);
|
||||
Assert.Equal(expected.Id, requestAction.Id);
|
||||
Assert.Equal(expected.Configuration, requestAction.Configuration);
|
||||
Assert.Equal(expected.EventType, requestAction.EventType);
|
||||
Assert.Equal(expected.Template, requestAction.Template);
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_AllParamsProvided_Webhook_Succeeds(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration,
|
||||
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
|
||||
OrganizationIntegrationConfigurationRequestModel model)
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
|
||||
organizationIntegration.Type = IntegrationType.Webhook;
|
||||
var webhookConfig = new WebhookIntegrationConfiguration(url: "https://localhost");
|
||||
model.Configuration = JsonSerializer.Serialize(webhookConfig);
|
||||
model.Template = "Template String";
|
||||
|
||||
var expected = new OrganizationIntegrationConfigurationResponseModel(model.ToOrganizationIntegrationConfiguration(organizationIntegrationConfiguration));
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegration);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegrationConfiguration);
|
||||
var requestAction = await sutProvider.Sut.UpdateAsync(
|
||||
organizationId,
|
||||
organizationIntegration.Id,
|
||||
organizationIntegrationConfiguration.Id,
|
||||
model);
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>().Received(1)
|
||||
.ReplaceAsync(Arg.Any<OrganizationIntegrationConfiguration>());
|
||||
Assert.IsType<OrganizationIntegrationConfigurationResponseModel>(requestAction);
|
||||
Assert.Equal(expected.Id, requestAction.Id);
|
||||
Assert.Equal(expected.Configuration, requestAction.Configuration);
|
||||
Assert.Equal(expected.EventType, requestAction.EventType);
|
||||
Assert.Equal(expected.Template, requestAction.Template);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_IntegrationConfigurationDoesNotExist_ThrowsNotFound(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration,
|
||||
OrganizationIntegrationConfigurationRequestModel model)
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegration.Type = IntegrationType.Webhook;
|
||||
var webhookConfig = new WebhookIntegrationConfiguration(url: "https://localhost");
|
||||
model.Configuration = JsonSerializer.Serialize(webhookConfig);
|
||||
model.Template = "Template String";
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegration);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.ReturnsNull();
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UpdateAsync(
|
||||
organizationId,
|
||||
organizationIntegration.Id,
|
||||
Guid.Empty,
|
||||
model));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_IntegrationDoesNotExist_ThrowsNotFound(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.ReturnsNull();
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UpdateAsync(
|
||||
organizationId,
|
||||
Guid.Empty,
|
||||
Guid.Empty,
|
||||
new OrganizationIntegrationConfigurationRequestModel()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_IntegrationDoesNotBelongToOrganization_ThrowsNotFound(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegration);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UpdateAsync(
|
||||
organizationId,
|
||||
organizationIntegration.Id,
|
||||
Guid.Empty,
|
||||
new OrganizationIntegrationConfigurationRequestModel()));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_InvalidConfiguration_ThrowsBadRequestException(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration,
|
||||
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
|
||||
OrganizationIntegrationConfigurationRequestModel model)
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
|
||||
organizationIntegration.Type = IntegrationType.Slack;
|
||||
model.Configuration = null;
|
||||
model.Template = "Template String";
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegration);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegrationConfiguration);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.UpdateAsync(
|
||||
organizationId,
|
||||
organizationIntegration.Id,
|
||||
organizationIntegrationConfiguration.Id,
|
||||
model));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_InvalidTemplate_ThrowsBadRequestException(
|
||||
SutProvider<OrganizationIntegrationConfigurationController> sutProvider,
|
||||
Guid organizationId,
|
||||
OrganizationIntegration organizationIntegration,
|
||||
OrganizationIntegrationConfiguration organizationIntegrationConfiguration,
|
||||
OrganizationIntegrationConfigurationRequestModel model)
|
||||
{
|
||||
organizationIntegration.OrganizationId = organizationId;
|
||||
organizationIntegrationConfiguration.OrganizationIntegrationId = organizationIntegration.Id;
|
||||
organizationIntegration.Type = IntegrationType.Slack;
|
||||
var slackConfig = new SlackIntegrationConfiguration(channelId: "C123456");
|
||||
model.Configuration = JsonSerializer.Serialize(slackConfig);
|
||||
model.Template = null;
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegration);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationConfigurationRepository>()
|
||||
.GetByIdAsync(Arg.Any<Guid>())
|
||||
.Returns(organizationIntegrationConfiguration);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.UpdateAsync(
|
||||
organizationId,
|
||||
organizationIntegration.Id,
|
||||
organizationIntegrationConfiguration.Id,
|
||||
model));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider<OrganizationIntegrationConfigurationController> sutProvider, Guid organizationId)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(false);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.UpdateAsync(
|
||||
organizationId,
|
||||
Guid.Empty,
|
||||
Guid.Empty,
|
||||
new OrganizationIntegrationConfigurationRequestModel()));
|
||||
}
|
||||
}
|
@ -0,0 +1,130 @@
|
||||
using Bit.Api.AdminConsole.Controllers;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Controllers;
|
||||
|
||||
[ControllerCustomize(typeof(SlackIntegrationController))]
|
||||
[SutProviderCustomize]
|
||||
public class SlackIntegrationControllerTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_AllParamsProvided_Succeeds(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
|
||||
{
|
||||
var token = "xoxb-test-token";
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<ISlackService>()
|
||||
.ObtainTokenViaOAuth(Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(token);
|
||||
sutProvider.GetDependency<IOrganizationIntegrationRepository>()
|
||||
.CreateAsync(Arg.Any<OrganizationIntegration>())
|
||||
.Returns(callInfo => callInfo.Arg<OrganizationIntegration>());
|
||||
var requestAction = await sutProvider.Sut.CreateAsync(organizationId, "A_test_code");
|
||||
|
||||
await sutProvider.GetDependency<IOrganizationIntegrationRepository>().Received(1)
|
||||
.CreateAsync(Arg.Any<OrganizationIntegration>());
|
||||
Assert.IsType<CreatedResult>(requestAction);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_CodeIsEmpty_ThrowsBadRequest(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(organizationId, string.Empty));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_SlackServiceReturnsEmpty_ThrowsBadRequest(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<ISlackService>()
|
||||
.ObtainTokenViaOAuth(Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(string.Empty);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(async () => await sutProvider.Sut.CreateAsync(organizationId, "A_test_code"));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task CreateAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
|
||||
{
|
||||
var token = "xoxb-test-token";
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(false);
|
||||
sutProvider.GetDependency<ISlackService>()
|
||||
.ObtainTokenViaOAuth(Arg.Any<string>(), Arg.Any<string>())
|
||||
.Returns(token);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.CreateAsync(organizationId, "A_test_code"));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RedirectAsync_Success(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
|
||||
{
|
||||
var expectedUrl = $"https://localhost/{organizationId}";
|
||||
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ISlackService>().GetRedirectUrl(Arg.Any<string>()).Returns(expectedUrl);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.HttpContext.Request.Scheme
|
||||
.Returns("https");
|
||||
|
||||
var requestAction = await sutProvider.Sut.RedirectAsync(organizationId);
|
||||
|
||||
var redirectResult = Assert.IsType<RedirectResult>(requestAction);
|
||||
Assert.Equal(expectedUrl, redirectResult.Url);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RedirectAsync_SlackServiceReturnsEmpty_ThrowsNotFound(SutProvider<SlackIntegrationController> sutProvider, Guid organizationId)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ISlackService>().GetRedirectUrl(Arg.Any<string>()).Returns(string.Empty);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.HttpContext.Request.Scheme
|
||||
.Returns("https");
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task RedirectAsync_UserIsNotOrganizationAdmin_ThrowsNotFound(SutProvider<SlackIntegrationController> sutProvider,
|
||||
Guid organizationId)
|
||||
{
|
||||
sutProvider.Sut.Url = Substitute.For<IUrlHelper>();
|
||||
sutProvider.GetDependency<ISlackService>().GetRedirectUrl(Arg.Any<string>()).Returns(string.Empty);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.OrganizationOwner(organizationId)
|
||||
.Returns(false);
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.HttpContext.Request.Scheme
|
||||
.Returns("https");
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RedirectAsync(organizationId));
|
||||
}
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Integrations;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Models.Request.Organizations;
|
||||
|
||||
public class OrganizationIntegrationConfigurationRequestModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void IsValidForType_CloudBillingSyncIntegration_ReturnsFalse()
|
||||
{
|
||||
var model = new OrganizationIntegrationConfigurationRequestModel
|
||||
{
|
||||
Configuration = "{}",
|
||||
Template = "template"
|
||||
};
|
||||
|
||||
Assert.False(model.IsValidForType(IntegrationType.CloudBillingSync));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void IsValidForType_EmptyConfiguration_ReturnsFalse(string? config)
|
||||
{
|
||||
var model = new OrganizationIntegrationConfigurationRequestModel
|
||||
{
|
||||
Configuration = config,
|
||||
Template = "template"
|
||||
};
|
||||
|
||||
var result = model.IsValidForType(IntegrationType.Slack);
|
||||
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
[InlineData("")]
|
||||
[InlineData(" ")]
|
||||
public void IsValidForType_EmptyTemplate_ReturnsFalse(string? template)
|
||||
{
|
||||
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com"));
|
||||
var model = new OrganizationIntegrationConfigurationRequestModel
|
||||
{
|
||||
Configuration = config,
|
||||
Template = template
|
||||
};
|
||||
|
||||
Assert.False(model.IsValidForType(IntegrationType.Webhook));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValidForType_InvalidJsonConfiguration_ReturnsFalse()
|
||||
{
|
||||
var model = new OrganizationIntegrationConfigurationRequestModel
|
||||
{
|
||||
Configuration = "{not valid json}",
|
||||
Template = "template"
|
||||
};
|
||||
|
||||
Assert.False(model.IsValidForType(IntegrationType.Webhook));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValidForType_ScimIntegration_ReturnsFalse()
|
||||
{
|
||||
var model = new OrganizationIntegrationConfigurationRequestModel
|
||||
{
|
||||
Configuration = "{}",
|
||||
Template = "template"
|
||||
};
|
||||
|
||||
Assert.False(model.IsValidForType(IntegrationType.Scim));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValidForType_ValidSlackConfiguration_ReturnsTrue()
|
||||
{
|
||||
var config = JsonSerializer.Serialize(new SlackIntegrationConfiguration("C12345"));
|
||||
|
||||
var model = new OrganizationIntegrationConfigurationRequestModel
|
||||
{
|
||||
Configuration = config,
|
||||
Template = "template"
|
||||
};
|
||||
|
||||
Assert.True(model.IsValidForType(IntegrationType.Slack));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValidForType_ValidWebhookConfiguration_ReturnsTrue()
|
||||
{
|
||||
var config = JsonSerializer.Serialize(new WebhookIntegrationConfiguration("https://example.com"));
|
||||
var model = new OrganizationIntegrationConfigurationRequestModel
|
||||
{
|
||||
Configuration = config,
|
||||
Template = "template"
|
||||
};
|
||||
|
||||
Assert.True(model.IsValidForType(IntegrationType.Webhook));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsValidForType_UnknownIntegrationType_ReturnsFalse()
|
||||
{
|
||||
var model = new OrganizationIntegrationConfigurationRequestModel
|
||||
{
|
||||
Configuration = "{}",
|
||||
Template = "template"
|
||||
};
|
||||
|
||||
var unknownType = (IntegrationType)999;
|
||||
|
||||
Assert.False(model.IsValidForType(unknownType));
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Core.Enums;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.Test.AdminConsole.Models.Request.Organizations;
|
||||
|
||||
public class OrganizationIntegrationRequestModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void Validate_CloudBillingSync_ReturnsNotYetSupportedError()
|
||||
{
|
||||
var model = new OrganizationIntegrationRequestModel
|
||||
{
|
||||
Type = IntegrationType.CloudBillingSync,
|
||||
Configuration = null
|
||||
};
|
||||
|
||||
var results = model.Validate(new ValidationContext(model)).ToList();
|
||||
|
||||
Assert.Single(results);
|
||||
Assert.Contains(nameof(model.Type), results[0].MemberNames);
|
||||
Assert.Contains("not yet supported", results[0].ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Scim_ReturnsNotYetSupportedError()
|
||||
{
|
||||
var model = new OrganizationIntegrationRequestModel
|
||||
{
|
||||
Type = IntegrationType.Scim,
|
||||
Configuration = null
|
||||
};
|
||||
|
||||
var results = model.Validate(new ValidationContext(model)).ToList();
|
||||
|
||||
Assert.Single(results);
|
||||
Assert.Contains(nameof(model.Type), results[0].MemberNames);
|
||||
Assert.Contains("not yet supported", results[0].ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Slack_ReturnsCannotBeCreatedDirectlyError()
|
||||
{
|
||||
var model = new OrganizationIntegrationRequestModel
|
||||
{
|
||||
Type = IntegrationType.Slack,
|
||||
Configuration = null
|
||||
};
|
||||
|
||||
var results = model.Validate(new ValidationContext(model)).ToList();
|
||||
|
||||
Assert.Single(results);
|
||||
Assert.Contains(nameof(model.Type), results[0].MemberNames);
|
||||
Assert.Contains("cannot be created directly", results[0].ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Webhook_WithNullConfiguration_ReturnsNoErrors()
|
||||
{
|
||||
var model = new OrganizationIntegrationRequestModel
|
||||
{
|
||||
Type = IntegrationType.Webhook,
|
||||
Configuration = null
|
||||
};
|
||||
|
||||
var results = model.Validate(new ValidationContext(model)).ToList();
|
||||
|
||||
Assert.Empty(results);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_Webhook_WithConfiguration_ReturnsConfigurationError()
|
||||
{
|
||||
var model = new OrganizationIntegrationRequestModel
|
||||
{
|
||||
Type = IntegrationType.Webhook,
|
||||
Configuration = "something"
|
||||
};
|
||||
|
||||
var results = model.Validate(new ValidationContext(model)).ToList();
|
||||
|
||||
Assert.Single(results);
|
||||
Assert.Contains(nameof(model.Configuration), results[0].MemberNames);
|
||||
Assert.Contains("must not include configuration", results[0].ErrorMessage);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Validate_UnknownIntegrationType_ReturnsUnrecognizedError()
|
||||
{
|
||||
var model = new OrganizationIntegrationRequestModel
|
||||
{
|
||||
Type = (IntegrationType)999,
|
||||
Configuration = null
|
||||
};
|
||||
|
||||
var results = model.Validate(new ValidationContext(model)).ToList();
|
||||
|
||||
Assert.Single(results);
|
||||
Assert.Contains(nameof(model.Type), results[0].MemberNames);
|
||||
Assert.Contains("not recognized", results[0].ErrorMessage);
|
||||
}
|
||||
}
|
@ -63,7 +63,9 @@ public class DevicesControllerTest
|
||||
UserId = userId,
|
||||
Name = "chrome",
|
||||
Type = DeviceType.ChromeBrowser,
|
||||
Identifier = Guid.Parse("811E9254-F77C-48C8-AF0A-A181943F5708").ToString()
|
||||
Identifier = Guid.Parse("811E9254-F77C-48C8-AF0A-A181943F5708").ToString(),
|
||||
EncryptedPublicKey = "PublicKey",
|
||||
EncryptedUserKey = "UserKey",
|
||||
},
|
||||
Guid.Parse("E09D6943-D574-49E5-AC85-C3F12B4E019E"),
|
||||
authDateTimeResponse)
|
||||
@ -78,6 +80,13 @@ public class DevicesControllerTest
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.IsType<ListResponseModel<DeviceAuthRequestResponseModel>>(result);
|
||||
var resultDevice = result.Data.First();
|
||||
Assert.Equal("chrome", resultDevice.Name);
|
||||
Assert.Equal(DeviceType.ChromeBrowser, resultDevice.Type);
|
||||
Assert.Equal(Guid.Parse("B3136B10-7818-444F-B05B-4D7A9B8C48BF"), resultDevice.Id);
|
||||
Assert.Equal(Guid.Parse("811E9254-F77C-48C8-AF0A-A181943F5708").ToString(), resultDevice.Identifier);
|
||||
Assert.Equal("PublicKey", resultDevice.EncryptedPublicKey);
|
||||
Assert.Equal("UserKey", resultDevice.EncryptedUserKey);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
@ -1,7 +1,9 @@
|
||||
using Bit.Api.Auth.Controllers;
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Models.Api.Response.Accounts;
|
||||
@ -80,6 +82,57 @@ public class WebAuthnControllerTests
|
||||
Assert.Contains("Passkeys cannot be created for your account. SSO login is required", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AttestationOptions_RequireSsoPolicyNotApplicable_Succeeds(
|
||||
SecretVerificationRequestModel requestModel, User user, SutProvider<WebAuthnController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
||||
sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, default).ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<IPolicyService>().AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso).ReturnsForAnyArgs(false);
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()
|
||||
.Protect(Arg.Any<WebAuthnCredentialCreateOptionsTokenable>()).Returns("token");
|
||||
|
||||
var result = await sutProvider.Sut.AttestationOptions(requestModel);
|
||||
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AttestationOptions_WithPolicyRequirementsEnabled_CanUsePasskeyLoginFalse_ThrowsBadRequestException(
|
||||
SecretVerificationRequestModel requestModel, User user, SutProvider<WebAuthnController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
||||
sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, default).ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||
.ReturnsForAnyArgs(new RequireSsoPolicyRequirement { CanUsePasskeyLogin = false });
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.AttestationOptions(requestModel));
|
||||
Assert.Contains("Passkeys cannot be created for your account. SSO login is required", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task AttestationOptions_WithPolicyRequirementsEnabled_CanUsePasskeyLoginTrue_Succeeds(
|
||||
SecretVerificationRequestModel requestModel, User user, SutProvider<WebAuthnController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(user);
|
||||
sutProvider.GetDependency<IUserService>().VerifySecretAsync(user, default).ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||
.ReturnsForAnyArgs(new RequireSsoPolicyRequirement { CanUsePasskeyLogin = true });
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()
|
||||
.Protect(Arg.Any<WebAuthnCredentialCreateOptionsTokenable>()).Returns("token");
|
||||
|
||||
var result = await sutProvider.Sut.AttestationOptions(requestModel);
|
||||
|
||||
Assert.NotNull(result);
|
||||
}
|
||||
|
||||
#region Assertion Options
|
||||
[Theory, BitAutoData]
|
||||
public async Task AssertionOptions_UserNotFound_ThrowsUnauthorizedAccessException(SecretVerificationRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
||||
@ -211,6 +264,102 @@ public class WebAuthnControllerTests
|
||||
Assert.Contains("Passkeys cannot be created for your account. SSO login is required", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Post_RequireSsoPolicyNotApplicable_Succeeds(
|
||||
WebAuthnLoginCredentialCreateRequestModel requestModel,
|
||||
CredentialCreateOptions createOptions,
|
||||
User user,
|
||||
SutProvider<WebAuthnController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByPrincipalAsync(default)
|
||||
.ReturnsForAnyArgs(user);
|
||||
sutProvider.GetDependency<ICreateWebAuthnLoginCredentialCommand>()
|
||||
.CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()
|
||||
.Unprotect(requestModel.Token)
|
||||
.Returns(token);
|
||||
sutProvider.GetDependency<IPolicyService>().AnyPoliciesApplicableToUserAsync(user.Id, PolicyType.RequireSso).ReturnsForAnyArgs(false);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.Post(requestModel);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IUserService>()
|
||||
.Received(1)
|
||||
.GetUserByPrincipalAsync(default);
|
||||
await sutProvider.GetDependency<ICreateWebAuthnLoginCredentialCommand>()
|
||||
.Received(1)
|
||||
.CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Post_WithPolicyRequirementsEnabled_CanUsePasskeyLoginFalse_ThrowsBadRequestException(
|
||||
WebAuthnLoginCredentialCreateRequestModel requestModel,
|
||||
CredentialCreateOptions createOptions,
|
||||
User user,
|
||||
SutProvider<WebAuthnController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByPrincipalAsync(default)
|
||||
.ReturnsForAnyArgs(user);
|
||||
sutProvider.GetDependency<ICreateWebAuthnLoginCredentialCommand>()
|
||||
.CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>(), false)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()
|
||||
.Unprotect(requestModel.Token)
|
||||
.Returns(token);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||
.ReturnsForAnyArgs(new RequireSsoPolicyRequirement { CanUsePasskeyLogin = false });
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => sutProvider.Sut.Post(requestModel));
|
||||
Assert.Contains("Passkeys cannot be created for your account. SSO login is required", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Post_WithPolicyRequirementsEnabled_CanUsePasskeyLoginTrue_Succeeds(
|
||||
WebAuthnLoginCredentialCreateRequestModel requestModel,
|
||||
CredentialCreateOptions createOptions,
|
||||
User user,
|
||||
SutProvider<WebAuthnController> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var token = new WebAuthnCredentialCreateOptionsTokenable(user, createOptions);
|
||||
sutProvider.GetDependency<IUserService>()
|
||||
.GetUserByPrincipalAsync(default)
|
||||
.ReturnsForAnyArgs(user);
|
||||
sutProvider.GetDependency<ICreateWebAuthnLoginCredentialCommand>()
|
||||
.CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey)
|
||||
.Returns(true);
|
||||
sutProvider.GetDependency<IDataProtectorTokenFactory<WebAuthnCredentialCreateOptionsTokenable>>()
|
||||
.Unprotect(requestModel.Token)
|
||||
.Returns(token);
|
||||
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.PolicyRequirements).ReturnsForAnyArgs(true);
|
||||
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||
.GetAsync<RequireSsoPolicyRequirement>(user.Id)
|
||||
.ReturnsForAnyArgs(new RequireSsoPolicyRequirement { CanUsePasskeyLogin = true });
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.Post(requestModel);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<IUserService>()
|
||||
.Received(1)
|
||||
.GetUserByPrincipalAsync(default);
|
||||
await sutProvider.GetDependency<ICreateWebAuthnLoginCredentialCommand>()
|
||||
.Received(1)
|
||||
.CreateWebAuthnLoginCredentialAsync(user, requestModel.Name, createOptions, Arg.Any<AuthenticatorAttestationRawResponse>(), requestModel.SupportsPrf, requestModel.EncryptedUserKey, requestModel.EncryptedPublicKey, requestModel.EncryptedPrivateKey);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task Delete_UserNotFound_ThrowsUnauthorizedAccessException(Guid credentialId, SecretVerificationRequestModel requestModel, SutProvider<WebAuthnController> sutProvider)
|
||||
{
|
||||
|
@ -420,7 +420,7 @@ public class InviteOrganizationUserCommandTests
|
||||
Assert.IsType<Success<ScimInviteOrganizationUsersResponse>>(result);
|
||||
|
||||
await sutProvider.GetDependency<IPaymentService>()
|
||||
.AdjustSeatsAsync(organization, inviteOrganization.Plan, passwordManagerUpdate.SeatsRequiredToAdd);
|
||||
.AdjustSeatsAsync(organization, inviteOrganization.Plan, passwordManagerUpdate.UpdatedSeatTotal!.Value);
|
||||
|
||||
await orgRepository.Received(1).ReplaceAsync(Arg.Is<Organization>(x => x.Seats == passwordManagerUpdate.UpdatedSeatTotal));
|
||||
|
||||
|
@ -0,0 +1,104 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class RequireSsoPolicyRequirementFactoryTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public void CanUsePasskeyLogin_WithNoPolicies_ReturnsTrue(
|
||||
SutProvider<RequireSsoPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
var actual = sutProvider.Sut.Create([]);
|
||||
|
||||
Assert.True(actual.CanUsePasskeyLogin);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(OrganizationUserStatusType.Accepted)]
|
||||
[BitAutoData(OrganizationUserStatusType.Confirmed)]
|
||||
public void CanUsePasskeyLogin_WithoutExemptStatus_ReturnsFalse(
|
||||
OrganizationUserStatusType userStatus,
|
||||
SutProvider<RequireSsoPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
var actual = sutProvider.Sut.Create(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
PolicyType = PolicyType.RequireSso,
|
||||
OrganizationUserStatus = userStatus
|
||||
}
|
||||
]);
|
||||
|
||||
Assert.False(actual.CanUsePasskeyLogin);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(OrganizationUserStatusType.Revoked)]
|
||||
[BitAutoData(OrganizationUserStatusType.Invited)]
|
||||
public void CanUsePasskeyLogin_WithExemptStatus_ReturnsTrue(
|
||||
OrganizationUserStatusType userStatus,
|
||||
SutProvider<RequireSsoPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
var actual = sutProvider.Sut.Create(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
PolicyType = PolicyType.RequireSso,
|
||||
OrganizationUserStatus = userStatus
|
||||
}
|
||||
]);
|
||||
|
||||
Assert.True(actual.CanUsePasskeyLogin);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void SsoRequired_WithNoPolicies_ReturnsFalse(
|
||||
SutProvider<RequireSsoPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
var actual = sutProvider.Sut.Create([]);
|
||||
|
||||
Assert.False(actual.SsoRequired);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData(OrganizationUserStatusType.Revoked)]
|
||||
[BitAutoData(OrganizationUserStatusType.Invited)]
|
||||
[BitAutoData(OrganizationUserStatusType.Accepted)]
|
||||
public void SsoRequired_WithoutExemptStatus_ReturnsFalse(
|
||||
OrganizationUserStatusType userStatus,
|
||||
SutProvider<RequireSsoPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
var actual = sutProvider.Sut.Create(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
PolicyType = PolicyType.RequireSso,
|
||||
OrganizationUserStatus = userStatus
|
||||
}
|
||||
]);
|
||||
|
||||
Assert.False(actual.SsoRequired);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void SsoRequired_WithExemptStatus_ReturnsTrue(
|
||||
SutProvider<RequireSsoPolicyRequirementFactory> sutProvider)
|
||||
{
|
||||
var actual = sutProvider.Sut.Create(
|
||||
[
|
||||
new PolicyDetails
|
||||
{
|
||||
PolicyType = PolicyType.RequireSso,
|
||||
OrganizationUserStatus = OrganizationUserStatusType.Confirmed
|
||||
}
|
||||
]);
|
||||
|
||||
Assert.True(actual.SsoRequired);
|
||||
}
|
||||
}
|
181
test/Core.Test/AdminConsole/Services/SlackEventHandlerTests.cs
Normal file
181
test/Core.Test/AdminConsole/Services/SlackEventHandlerTests.cs
Normal file
@ -0,0 +1,181 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class SlackEventHandlerTests
|
||||
{
|
||||
private readonly IOrganizationIntegrationConfigurationRepository _repository = Substitute.For<IOrganizationIntegrationConfigurationRepository>();
|
||||
private readonly ISlackService _slackService = Substitute.For<ISlackService>();
|
||||
private readonly string _channelId = "C12345";
|
||||
private readonly string _channelId2 = "C67890";
|
||||
private readonly string _token = "xoxb-test-token";
|
||||
private readonly string _token2 = "xoxb-another-test-token";
|
||||
|
||||
private SutProvider<SlackEventHandler> GetSutProvider(
|
||||
List<OrganizationIntegrationConfigurationDetails> integrationConfigurations)
|
||||
{
|
||||
_repository.GetConfigurationDetailsAsync(Arg.Any<Guid>(),
|
||||
IntegrationType.Slack, Arg.Any<EventType>())
|
||||
.Returns(integrationConfigurations);
|
||||
|
||||
return new SutProvider<SlackEventHandler>()
|
||||
.SetDependency(_repository)
|
||||
.SetDependency(_slackService)
|
||||
.Create();
|
||||
}
|
||||
|
||||
private List<OrganizationIntegrationConfigurationDetails> NoConfigurations()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
private List<OrganizationIntegrationConfigurationDetails> OneConfiguration()
|
||||
{
|
||||
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||
config.Configuration = JsonSerializer.Serialize(new { token = _token });
|
||||
config.IntegrationConfiguration = JsonSerializer.Serialize(new { channelId = _channelId });
|
||||
config.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#";
|
||||
|
||||
return [config];
|
||||
}
|
||||
|
||||
private List<OrganizationIntegrationConfigurationDetails> TwoConfigurations()
|
||||
{
|
||||
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||
config.Configuration = JsonSerializer.Serialize(new { token = _token });
|
||||
config.IntegrationConfiguration = JsonSerializer.Serialize(new { channelId = _channelId });
|
||||
config.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#";
|
||||
var config2 = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||
config2.Configuration = JsonSerializer.Serialize(new { token = _token2 });
|
||||
config2.IntegrationConfiguration = JsonSerializer.Serialize(new { channelId = _channelId2 });
|
||||
config2.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#";
|
||||
|
||||
return [config, config2];
|
||||
}
|
||||
|
||||
private List<OrganizationIntegrationConfigurationDetails> WrongConfiguration()
|
||||
{
|
||||
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||
config.Configuration = JsonSerializer.Serialize(new { });
|
||||
config.IntegrationConfiguration = JsonSerializer.Serialize(new { });
|
||||
config.Template = "Date: #Date#, Type: #Type#, UserId: #UserId#";
|
||||
|
||||
return [config];
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_NoConfigurations_DoesNothing(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(NoConfigurations());
|
||||
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
sutProvider.GetDependency<ISlackService>().DidNotReceiveWithAnyArgs();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_OneConfiguration_SendsEventViaSlackService(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration());
|
||||
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_token)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(
|
||||
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))
|
||||
);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_TwoConfigurations_SendsMultipleEvents(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(TwoConfigurations());
|
||||
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_token)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(
|
||||
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))
|
||||
);
|
||||
sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_token2)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(
|
||||
$"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}")),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId2))
|
||||
);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_WrongConfiguration_DoesNothing(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(WrongConfiguration());
|
||||
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
sutProvider.GetDependency<ISlackService>().DidNotReceiveWithAnyArgs();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleManyEventsAsync_OneConfiguration_SendsEventsViaSlackService(List<EventMessage> eventMessages)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration());
|
||||
|
||||
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
||||
|
||||
var received = sutProvider.GetDependency<ISlackService>().ReceivedCalls();
|
||||
using var calls = received.GetEnumerator();
|
||||
|
||||
Assert.Equal(eventMessages.Count, received.Count());
|
||||
|
||||
foreach (var eventMessage in eventMessages)
|
||||
{
|
||||
Assert.True(calls.MoveNext());
|
||||
var arguments = calls.Current.GetArguments();
|
||||
Assert.Equal(_token, arguments[0] as string);
|
||||
Assert.Equal($"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}",
|
||||
arguments[1] as string);
|
||||
Assert.Equal(_channelId, arguments[2] as string);
|
||||
}
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleManyEventsAsync_TwoConfigurations_SendsMultipleEvents(List<EventMessage> eventMessages)
|
||||
{
|
||||
var sutProvider = GetSutProvider(TwoConfigurations());
|
||||
|
||||
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
||||
|
||||
var received = sutProvider.GetDependency<ISlackService>().ReceivedCalls();
|
||||
using var calls = received.GetEnumerator();
|
||||
|
||||
Assert.Equal(eventMessages.Count * 2, received.Count());
|
||||
|
||||
foreach (var eventMessage in eventMessages)
|
||||
{
|
||||
Assert.True(calls.MoveNext());
|
||||
var arguments = calls.Current.GetArguments();
|
||||
Assert.Equal(_token, arguments[0] as string);
|
||||
Assert.Equal($"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}",
|
||||
arguments[1] as string);
|
||||
Assert.Equal(_channelId, arguments[2] as string);
|
||||
|
||||
Assert.True(calls.MoveNext());
|
||||
var arguments2 = calls.Current.GetArguments();
|
||||
Assert.Equal(_token2, arguments2[0] as string);
|
||||
Assert.Equal($"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}",
|
||||
arguments2[1] as string);
|
||||
Assert.Equal(_channelId2, arguments2[2] as string);
|
||||
}
|
||||
}
|
||||
}
|
344
test/Core.Test/AdminConsole/Services/SlackServiceTests.cs
Normal file
344
test/Core.Test/AdminConsole/Services/SlackServiceTests.cs
Normal file
@ -0,0 +1,344 @@
|
||||
using System.Net;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.MockedHttpClient;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class SlackServiceTests
|
||||
{
|
||||
private readonly MockedHttpMessageHandler _handler;
|
||||
private readonly HttpClient _httpClient;
|
||||
private const string _token = "xoxb-test-token";
|
||||
|
||||
public SlackServiceTests()
|
||||
{
|
||||
_handler = new MockedHttpMessageHandler();
|
||||
_httpClient = _handler.ToHttpClient();
|
||||
}
|
||||
|
||||
private SutProvider<SlackService> GetSutProvider()
|
||||
{
|
||||
var clientFactory = Substitute.For<IHttpClientFactory>();
|
||||
clientFactory.CreateClient(SlackService.HttpClientName).Returns(_httpClient);
|
||||
|
||||
var globalSettings = Substitute.For<GlobalSettings>();
|
||||
globalSettings.Slack.ApiBaseUrl.Returns("https://slack.com/api");
|
||||
|
||||
return new SutProvider<SlackService>()
|
||||
.SetDependency(clientFactory)
|
||||
.SetDependency(globalSettings)
|
||||
.Create();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetChannelIdsAsync_ReturnsCorrectChannelIds()
|
||||
{
|
||||
var response = JsonSerializer.Serialize(
|
||||
new
|
||||
{
|
||||
ok = true,
|
||||
channels =
|
||||
new[] {
|
||||
new { id = "C12345", name = "general" },
|
||||
new { id = "C67890", name = "random" }
|
||||
},
|
||||
response_metadata = new { next_cursor = "" }
|
||||
}
|
||||
);
|
||||
_handler.When(HttpMethod.Get)
|
||||
.RespondWith(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent(response));
|
||||
|
||||
var sutProvider = GetSutProvider();
|
||||
var channelNames = new List<string> { "general", "random" };
|
||||
var result = await sutProvider.Sut.GetChannelIdsAsync(_token, channelNames);
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains("C12345", result);
|
||||
Assert.Contains("C67890", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetChannelIdsAsync_WithPagination_ReturnsCorrectChannelIds()
|
||||
{
|
||||
var firstPageResponse = JsonSerializer.Serialize(
|
||||
new
|
||||
{
|
||||
ok = true,
|
||||
channels = new[] { new { id = "C12345", name = "general" } },
|
||||
response_metadata = new { next_cursor = "next_cursor_value" }
|
||||
}
|
||||
);
|
||||
var secondPageResponse = JsonSerializer.Serialize(
|
||||
new
|
||||
{
|
||||
ok = true,
|
||||
channels = new[] { new { id = "C67890", name = "random" } },
|
||||
response_metadata = new { next_cursor = "" }
|
||||
}
|
||||
);
|
||||
|
||||
_handler.When("https://slack.com/api/conversations.list?types=public_channel%2cprivate_channel&limit=1000")
|
||||
.RespondWith(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent(firstPageResponse));
|
||||
_handler.When("https://slack.com/api/conversations.list?types=public_channel%2cprivate_channel&limit=1000&cursor=next_cursor_value")
|
||||
.RespondWith(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent(secondPageResponse));
|
||||
|
||||
var sutProvider = GetSutProvider();
|
||||
var channelNames = new List<string> { "general", "random" };
|
||||
|
||||
var result = await sutProvider.Sut.GetChannelIdsAsync(_token, channelNames);
|
||||
|
||||
Assert.Equal(2, result.Count);
|
||||
Assert.Contains("C12345", result);
|
||||
Assert.Contains("C67890", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetChannelIdsAsync_ApiError_ReturnsEmptyResult()
|
||||
{
|
||||
var errorResponse = JsonSerializer.Serialize(
|
||||
new { ok = false, error = "rate_limited" }
|
||||
);
|
||||
|
||||
_handler.When(HttpMethod.Get)
|
||||
.RespondWith(HttpStatusCode.TooManyRequests)
|
||||
.WithContent(new StringContent(errorResponse));
|
||||
|
||||
var sutProvider = GetSutProvider();
|
||||
var channelNames = new List<string> { "general", "random" };
|
||||
|
||||
var result = await sutProvider.Sut.GetChannelIdsAsync(_token, channelNames);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetChannelIdsAsync_NoChannelsFound_ReturnsEmptyResult()
|
||||
{
|
||||
var emptyResponse = JsonSerializer.Serialize(
|
||||
new
|
||||
{
|
||||
ok = true,
|
||||
channels = Array.Empty<string>(),
|
||||
response_metadata = new { next_cursor = "" }
|
||||
});
|
||||
|
||||
_handler.When(HttpMethod.Get)
|
||||
.RespondWith(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent(emptyResponse));
|
||||
|
||||
var sutProvider = GetSutProvider();
|
||||
var channelNames = new List<string> { "general", "random" };
|
||||
var result = await sutProvider.Sut.GetChannelIdsAsync(_token, channelNames);
|
||||
|
||||
Assert.Empty(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetChannelIdAsync_ReturnsCorrectChannelId()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var response = new
|
||||
{
|
||||
ok = true,
|
||||
channels = new[]
|
||||
{
|
||||
new { id = "C12345", name = "general" },
|
||||
new { id = "C67890", name = "random" }
|
||||
},
|
||||
response_metadata = new { next_cursor = "" }
|
||||
};
|
||||
|
||||
_handler.When(HttpMethod.Get)
|
||||
.RespondWith(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent(JsonSerializer.Serialize(response)));
|
||||
|
||||
var result = await sutProvider.Sut.GetChannelIdAsync(_token, "general");
|
||||
|
||||
Assert.Equal("C12345", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetDmChannelByEmailAsync_ReturnsCorrectDmChannelId()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var email = "user@example.com";
|
||||
var userId = "U12345";
|
||||
var dmChannelId = "D67890";
|
||||
|
||||
var userResponse = new
|
||||
{
|
||||
ok = true,
|
||||
user = new { id = userId }
|
||||
};
|
||||
|
||||
var dmResponse = new
|
||||
{
|
||||
ok = true,
|
||||
channel = new { id = dmChannelId }
|
||||
};
|
||||
|
||||
_handler.When($"https://slack.com/api/users.lookupByEmail?email={email}")
|
||||
.RespondWith(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent(JsonSerializer.Serialize(userResponse)));
|
||||
|
||||
_handler.When("https://slack.com/api/conversations.open")
|
||||
.RespondWith(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent(JsonSerializer.Serialize(dmResponse)));
|
||||
|
||||
var result = await sutProvider.Sut.GetDmChannelByEmailAsync(_token, email);
|
||||
|
||||
Assert.Equal(dmChannelId, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetDmChannelByEmailAsync_ApiErrorDmResponse_ReturnsEmptyString()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var email = "user@example.com";
|
||||
var userId = "U12345";
|
||||
|
||||
var userResponse = new
|
||||
{
|
||||
ok = true,
|
||||
user = new { id = userId }
|
||||
};
|
||||
|
||||
var dmResponse = new
|
||||
{
|
||||
ok = false,
|
||||
error = "An error occured"
|
||||
};
|
||||
|
||||
_handler.When($"https://slack.com/api/users.lookupByEmail?email={email}")
|
||||
.RespondWith(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent(JsonSerializer.Serialize(userResponse)));
|
||||
|
||||
_handler.When("https://slack.com/api/conversations.open")
|
||||
.RespondWith(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent(JsonSerializer.Serialize(dmResponse)));
|
||||
|
||||
var result = await sutProvider.Sut.GetDmChannelByEmailAsync(_token, email);
|
||||
|
||||
Assert.Equal(string.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetDmChannelByEmailAsync_ApiErrorUserResponse_ReturnsEmptyString()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var email = "user@example.com";
|
||||
|
||||
var userResponse = new
|
||||
{
|
||||
ok = false,
|
||||
error = "An error occured"
|
||||
};
|
||||
|
||||
_handler.When($"https://slack.com/api/users.lookupByEmail?email={email}")
|
||||
.RespondWith(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent(JsonSerializer.Serialize(userResponse)));
|
||||
|
||||
var result = await sutProvider.Sut.GetDmChannelByEmailAsync(_token, email);
|
||||
|
||||
Assert.Equal(string.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetRedirectUrl_ReturnsCorrectUrl()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var ClientId = sutProvider.GetDependency<GlobalSettings>().Slack.ClientId;
|
||||
var Scopes = sutProvider.GetDependency<GlobalSettings>().Slack.Scopes;
|
||||
var redirectUrl = "https://example.com/callback";
|
||||
var expectedUrl = $"https://slack.com/oauth/v2/authorize?client_id={ClientId}&scope={Scopes}&redirect_uri={redirectUrl}";
|
||||
var result = sutProvider.Sut.GetRedirectUrl(redirectUrl);
|
||||
Assert.Equal(expectedUrl, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ObtainTokenViaOAuth_ReturnsAccessToken_WhenSuccessful()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var jsonResponse = JsonSerializer.Serialize(new
|
||||
{
|
||||
ok = true,
|
||||
access_token = "test-access-token"
|
||||
});
|
||||
|
||||
_handler.When("https://slack.com/api/oauth.v2.access")
|
||||
.RespondWith(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent(jsonResponse));
|
||||
|
||||
var result = await sutProvider.Sut.ObtainTokenViaOAuth("test-code", "https://example.com/callback");
|
||||
|
||||
Assert.Equal("test-access-token", result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ObtainTokenViaOAuth_ReturnsEmptyString_WhenErrorResponse()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var jsonResponse = JsonSerializer.Serialize(new
|
||||
{
|
||||
ok = false,
|
||||
error = "invalid_code"
|
||||
});
|
||||
|
||||
_handler.When("https://slack.com/api/oauth.v2.access")
|
||||
.RespondWith(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent(jsonResponse));
|
||||
|
||||
var result = await sutProvider.Sut.ObtainTokenViaOAuth("test-code", "https://example.com/callback");
|
||||
|
||||
Assert.Equal(string.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ObtainTokenViaOAuth_ReturnsEmptyString_WhenHttpCallFails()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
_handler.When("https://slack.com/api/oauth.v2.access")
|
||||
.RespondWith(HttpStatusCode.InternalServerError)
|
||||
.WithContent(new StringContent(string.Empty));
|
||||
|
||||
var result = await sutProvider.Sut.ObtainTokenViaOAuth("test-code", "https://example.com/callback");
|
||||
|
||||
Assert.Equal(string.Empty, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SendSlackMessageByChannelId_Sends_Correct_Message()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var channelId = "C12345";
|
||||
var message = "Hello, Slack!";
|
||||
|
||||
_handler.When(HttpMethod.Post)
|
||||
.RespondWith(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent(string.Empty));
|
||||
|
||||
await sutProvider.Sut.SendSlackMessageByChannelIdAsync(_token, message, channelId);
|
||||
|
||||
Assert.Single(_handler.CapturedRequests);
|
||||
var request = _handler.CapturedRequests[0];
|
||||
Assert.NotNull(request);
|
||||
Assert.Equal(HttpMethod.Post, request.Method);
|
||||
Assert.NotNull(request.Headers.Authorization);
|
||||
Assert.Equal($"Bearer {_token}", request.Headers.Authorization.ToString());
|
||||
Assert.NotNull(request.Content);
|
||||
var returned = (await request.Content.ReadAsStringAsync());
|
||||
var json = JsonDocument.Parse(returned);
|
||||
Assert.Equal(message, json.RootElement.GetProperty("text").GetString() ?? string.Empty);
|
||||
Assert.Equal(channelId, json.RootElement.GetProperty("channel").GetString() ?? string.Empty);
|
||||
}
|
||||
}
|
@ -1,6 +1,10 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Models.Data.Organizations;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
@ -8,7 +12,6 @@ using Bit.Test.Common.Helpers;
|
||||
using Bit.Test.Common.MockedHttpClient;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
using GlobalSettings = Bit.Core.Settings.GlobalSettings;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
@ -16,9 +19,18 @@ namespace Bit.Core.Test.Services;
|
||||
public class WebhookEventHandlerTests
|
||||
{
|
||||
private readonly MockedHttpMessageHandler _handler;
|
||||
private HttpClient _httpClient;
|
||||
private readonly HttpClient _httpClient;
|
||||
|
||||
private const string _template =
|
||||
"""
|
||||
{
|
||||
"Date": "#Date#",
|
||||
"Type": "#Type#",
|
||||
"UserId": "#UserId#"
|
||||
}
|
||||
""";
|
||||
private const string _webhookUrl = "http://localhost/test/event";
|
||||
private const string _webhookUrl2 = "http://localhost/another/event";
|
||||
|
||||
public WebhookEventHandlerTests()
|
||||
{
|
||||
@ -29,57 +41,195 @@ public class WebhookEventHandlerTests
|
||||
_httpClient = _handler.ToHttpClient();
|
||||
}
|
||||
|
||||
public SutProvider<WebhookEventHandler> GetSutProvider()
|
||||
private SutProvider<WebhookEventHandler> GetSutProvider(
|
||||
List<OrganizationIntegrationConfigurationDetails> configurations)
|
||||
{
|
||||
var clientFactory = Substitute.For<IHttpClientFactory>();
|
||||
clientFactory.CreateClient(WebhookEventHandler.HttpClientName).Returns(_httpClient);
|
||||
|
||||
var globalSettings = new GlobalSettings();
|
||||
globalSettings.EventLogging.WebhookUrl = _webhookUrl;
|
||||
var repository = Substitute.For<IOrganizationIntegrationConfigurationRepository>();
|
||||
repository.GetConfigurationDetailsAsync(Arg.Any<Guid>(),
|
||||
IntegrationType.Webhook, Arg.Any<EventType>()).Returns(configurations);
|
||||
|
||||
return new SutProvider<WebhookEventHandler>()
|
||||
.SetDependency(globalSettings)
|
||||
.SetDependency(repository)
|
||||
.SetDependency(clientFactory)
|
||||
.Create();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_PostsEventToUrl(EventMessage eventMessage)
|
||||
private static List<OrganizationIntegrationConfigurationDetails> NoConfigurations()
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
return [];
|
||||
}
|
||||
|
||||
private static List<OrganizationIntegrationConfigurationDetails> OneConfiguration()
|
||||
{
|
||||
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||
config.Configuration = null;
|
||||
config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _webhookUrl });
|
||||
config.Template = _template;
|
||||
|
||||
return [config];
|
||||
}
|
||||
|
||||
private static List<OrganizationIntegrationConfigurationDetails> TwoConfigurations()
|
||||
{
|
||||
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||
config.Configuration = null;
|
||||
config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _webhookUrl });
|
||||
config.Template = _template;
|
||||
var config2 = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||
config2.Configuration = null;
|
||||
config2.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _webhookUrl2 });
|
||||
config2.Template = _template;
|
||||
|
||||
return [config, config2];
|
||||
}
|
||||
|
||||
private static List<OrganizationIntegrationConfigurationDetails> WrongConfiguration()
|
||||
{
|
||||
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||
config.Configuration = null;
|
||||
config.IntegrationConfiguration = JsonSerializer.Serialize(new { error = string.Empty });
|
||||
config.Template = _template;
|
||||
|
||||
return [config];
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_NoConfigurations_DoesNothing(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(NoConfigurations());
|
||||
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual<string>(WebhookEventHandler.HttpClientName))
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName))
|
||||
);
|
||||
|
||||
Assert.Single(_handler.CapturedRequests);
|
||||
var request = _handler.CapturedRequests[0];
|
||||
Assert.NotNull(request);
|
||||
var returned = await request.Content.ReadFromJsonAsync<EventMessage>();
|
||||
|
||||
Assert.Equal(HttpMethod.Post, request.Method);
|
||||
Assert.Equal(_webhookUrl, request.RequestUri.ToString());
|
||||
AssertHelper.AssertPropertyEqual(eventMessage, returned, new[] { "IdempotencyId" });
|
||||
Assert.Empty(_handler.CapturedRequests);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventManyAsync_PostsEventsToUrl(IEnumerable<EventMessage> eventMessages)
|
||||
public async Task HandleEventAsync_OneConfiguration_PostsEventToUrl(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
var sutProvider = GetSutProvider(OneConfiguration());
|
||||
|
||||
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual<string>(WebhookEventHandler.HttpClientName))
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName))
|
||||
);
|
||||
|
||||
Assert.Single(_handler.CapturedRequests);
|
||||
var request = _handler.CapturedRequests[0];
|
||||
Assert.NotNull(request);
|
||||
var returned = request.Content.ReadFromJsonAsAsyncEnumerable<EventMessage>();
|
||||
var returned = await request.Content.ReadFromJsonAsync<MockEvent>();
|
||||
var expected = MockEvent.From(eventMessage);
|
||||
|
||||
Assert.Equal(HttpMethod.Post, request.Method);
|
||||
Assert.Equal(_webhookUrl, request.RequestUri.ToString());
|
||||
AssertHelper.AssertPropertyEqual(eventMessages, returned, new[] { "IdempotencyId" });
|
||||
AssertHelper.AssertPropertyEqual(expected, returned);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_WrongConfigurations_DoesNothing(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(WrongConfiguration());
|
||||
|
||||
await sutProvider.Sut.HandleEventAsync(eventMessage);
|
||||
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName))
|
||||
);
|
||||
|
||||
Assert.Empty(_handler.CapturedRequests);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleManyEventsAsync_NoConfigurations_DoesNothing(List<EventMessage> eventMessages)
|
||||
{
|
||||
var sutProvider = GetSutProvider(NoConfigurations());
|
||||
|
||||
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
||||
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName))
|
||||
);
|
||||
|
||||
Assert.Empty(_handler.CapturedRequests);
|
||||
}
|
||||
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleManyEventsAsync_OneConfiguration_PostsEventsToUrl(List<EventMessage> eventMessages)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration());
|
||||
|
||||
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
||||
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName))
|
||||
);
|
||||
|
||||
Assert.Equal(eventMessages.Count, _handler.CapturedRequests.Count);
|
||||
var index = 0;
|
||||
foreach (var request in _handler.CapturedRequests)
|
||||
{
|
||||
Assert.NotNull(request);
|
||||
var returned = await request.Content.ReadFromJsonAsync<MockEvent>();
|
||||
var expected = MockEvent.From(eventMessages[index]);
|
||||
|
||||
Assert.Equal(HttpMethod.Post, request.Method);
|
||||
Assert.Equal(_webhookUrl, request.RequestUri.ToString());
|
||||
AssertHelper.AssertPropertyEqual(expected, returned);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleManyEventsAsync_TwoConfigurations_PostsEventsToMultipleUrls(List<EventMessage> eventMessages)
|
||||
{
|
||||
var sutProvider = GetSutProvider(TwoConfigurations());
|
||||
|
||||
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
||||
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookEventHandler.HttpClientName))
|
||||
);
|
||||
|
||||
using var capturedRequests = _handler.CapturedRequests.GetEnumerator();
|
||||
Assert.Equal(eventMessages.Count * 2, _handler.CapturedRequests.Count);
|
||||
|
||||
foreach (var eventMessage in eventMessages)
|
||||
{
|
||||
var expected = MockEvent.From(eventMessage);
|
||||
|
||||
Assert.True(capturedRequests.MoveNext());
|
||||
var request = capturedRequests.Current;
|
||||
Assert.NotNull(request);
|
||||
Assert.Equal(HttpMethod.Post, request.Method);
|
||||
Assert.Equal(_webhookUrl, request.RequestUri.ToString());
|
||||
var returned = await request.Content.ReadFromJsonAsync<MockEvent>();
|
||||
AssertHelper.AssertPropertyEqual(expected, returned);
|
||||
|
||||
Assert.True(capturedRequests.MoveNext());
|
||||
request = capturedRequests.Current;
|
||||
Assert.NotNull(request);
|
||||
Assert.Equal(HttpMethod.Post, request.Method);
|
||||
Assert.Equal(_webhookUrl2, request.RequestUri.ToString());
|
||||
returned = await request.Content.ReadFromJsonAsync<MockEvent>();
|
||||
AssertHelper.AssertPropertyEqual(expected, returned);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class MockEvent(string date, string type, string userId)
|
||||
{
|
||||
public string Date { get; set; } = date;
|
||||
public string Type { get; set; } = type;
|
||||
public string UserId { get; set; } = userId;
|
||||
|
||||
public static MockEvent From(EventMessage eventMessage)
|
||||
{
|
||||
return new MockEvent(
|
||||
eventMessage.Date.ToString(),
|
||||
eventMessage.Type.ToString(),
|
||||
eventMessage.UserId.ToString()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,92 @@
|
||||
using Bit.Core.AdminConsole.Utilities;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.AdminConsole.Utilities;
|
||||
|
||||
public class IntegrationTemplateProcessorTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public void ReplaceTokens_ReplacesSingleToken(EventMessage eventMessage)
|
||||
{
|
||||
var template = "Event #Type# occurred.";
|
||||
var expected = $"Event {eventMessage.Type} occurred.";
|
||||
var result = IntegrationTemplateProcessor.ReplaceTokens(template, eventMessage);
|
||||
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ReplaceTokens_ReplacesMultipleTokens(EventMessage eventMessage)
|
||||
{
|
||||
var template = "Event #Type#, User (id: #UserId#).";
|
||||
var expected = $"Event {eventMessage.Type}, User (id: {eventMessage.UserId}).";
|
||||
var result = IntegrationTemplateProcessor.ReplaceTokens(template, eventMessage);
|
||||
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ReplaceTokens_LeavesUnknownTokensUnchanged(EventMessage eventMessage)
|
||||
{
|
||||
var template = "Event #Type#, User (id: #UserId#), Details: #UnknownKey#.";
|
||||
var expected = $"Event {eventMessage.Type}, User (id: {eventMessage.UserId}), Details: #UnknownKey#.";
|
||||
var result = IntegrationTemplateProcessor.ReplaceTokens(template, eventMessage);
|
||||
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ReplaceTokens_WithNullProperty_LeavesTokenUnchanged(EventMessage eventMessage)
|
||||
{
|
||||
eventMessage.UserId = null;
|
||||
|
||||
var template = "Event #Type#, User (id: #UserId#).";
|
||||
var expected = $"Event {eventMessage.Type}, User (id: #UserId#).";
|
||||
var result = IntegrationTemplateProcessor.ReplaceTokens(template, eventMessage);
|
||||
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ReplaceTokens_TokensWithNonmatchingCase_LeavesTokensUnchanged(EventMessage eventMessage)
|
||||
{
|
||||
var template = "Event #type#, User (id: #UserId#).";
|
||||
var expected = $"Event #type#, User (id: {eventMessage.UserId}).";
|
||||
var result = IntegrationTemplateProcessor.ReplaceTokens(template, eventMessage);
|
||||
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ReplaceTokens_NoTokensPresent_ReturnsOriginalString(EventMessage eventMessage)
|
||||
{
|
||||
var template = "System is running normally.";
|
||||
var expected = "System is running normally.";
|
||||
var result = IntegrationTemplateProcessor.ReplaceTokens(template, eventMessage);
|
||||
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public void ReplaceTokens_TemplateIsEmpty_ReturnsOriginalString(EventMessage eventMessage)
|
||||
{
|
||||
var emptyTemplate = "";
|
||||
var expectedEmpty = "";
|
||||
|
||||
Assert.Equal(expectedEmpty, IntegrationTemplateProcessor.ReplaceTokens(emptyTemplate, eventMessage));
|
||||
Assert.Null(IntegrationTemplateProcessor.ReplaceTokens(null, eventMessage));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplaceTokens_DataObjectIsNull_ReturnsOriginalString()
|
||||
{
|
||||
var template = "Event #Type#, User (id: #UserId#).";
|
||||
var expected = "Event #Type#, User (id: #UserId#).";
|
||||
|
||||
var result = IntegrationTemplateProcessor.ReplaceTokens(template, null);
|
||||
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
}
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ using Bit.Core.Test.AutoFixture.UserFixtures;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Duende.IdentityModel;
|
||||
using IdentityModel;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Xunit;
|
||||
|
||||
|
@ -17,9 +17,9 @@ using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.IntegrationTestCommon.Factories;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using IdentityModel;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
@ -17,9 +17,9 @@ using Bit.Core.Utilities;
|
||||
using Bit.IntegrationTestCommon.Factories;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using Duende.IdentityModel;
|
||||
using Duende.IdentityServer.Models;
|
||||
using Duende.IdentityServer.Stores;
|
||||
using IdentityModel;
|
||||
using LinqToDB;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using NSubstitute;
|
||||
|
@ -1,6 +1,7 @@
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
@ -41,6 +42,7 @@ public class BaseRequestValidatorTests
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ISsoConfigRepository _ssoConfigRepository;
|
||||
private readonly IUserDecryptionOptionsBuilder _userDecryptionOptionsBuilder;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
|
||||
private readonly BaseRequestValidatorTestWrapper _sut;
|
||||
|
||||
@ -61,6 +63,7 @@ public class BaseRequestValidatorTests
|
||||
_featureService = Substitute.For<IFeatureService>();
|
||||
_ssoConfigRepository = Substitute.For<ISsoConfigRepository>();
|
||||
_userDecryptionOptionsBuilder = Substitute.For<IUserDecryptionOptionsBuilder>();
|
||||
_policyRequirementQuery = Substitute.For<IPolicyRequirementQuery>();
|
||||
|
||||
_sut = new BaseRequestValidatorTestWrapper(
|
||||
_userManager,
|
||||
@ -77,7 +80,8 @@ public class BaseRequestValidatorTests
|
||||
_policyService,
|
||||
_featureService,
|
||||
_ssoConfigRepository,
|
||||
_userDecryptionOptionsBuilder);
|
||||
_userDecryptionOptionsBuilder,
|
||||
_policyRequirementQuery);
|
||||
}
|
||||
|
||||
/* Logic path
|
||||
@ -276,6 +280,75 @@ public class BaseRequestValidatorTests
|
||||
Assert.Equal("SSO authentication is required.", errorResponse.Message);
|
||||
}
|
||||
|
||||
// Test grantTypes with RequireSsoPolicyRequirement when feature flag is enabled
|
||||
[Theory]
|
||||
[BitAutoData("password")]
|
||||
[BitAutoData("webauthn")]
|
||||
[BitAutoData("refresh_token")]
|
||||
public async Task ValidateAsync_GrantTypes_WithPolicyRequirementsEnabled_OrgSsoRequiredTrue_ShouldSetSsoResult(
|
||||
string grantType,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
// Arrange
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
||||
_sut.isValid = true;
|
||||
|
||||
context.ValidatedTokenRequest.GrantType = grantType;
|
||||
// Configure requirement to require SSO
|
||||
var requirement = new RequireSsoPolicyRequirement { SsoRequired = true };
|
||||
_policyRequirementQuery.GetAsync<RequireSsoPolicyRequirement>(Arg.Any<Guid>()).Returns(requirement);
|
||||
|
||||
// Act
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
// Assert
|
||||
await _policyService.DidNotReceive().AnyPoliciesApplicableToUserAsync(
|
||||
Arg.Any<Guid>(), PolicyType.RequireSso, OrganizationUserStatusType.Confirmed);
|
||||
Assert.True(context.GrantResult.IsError);
|
||||
var errorResponse = (ErrorResponseModel)context.GrantResult.CustomResponse["ErrorModel"];
|
||||
Assert.Equal("SSO authentication is required.", errorResponse.Message);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData("password")]
|
||||
[BitAutoData("webauthn")]
|
||||
[BitAutoData("refresh_token")]
|
||||
public async Task ValidateAsync_GrantTypes_WithPolicyRequirementsEnabled_OrgSsoRequiredFalse_ShouldSucceed(
|
||||
string grantType,
|
||||
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
|
||||
CustomValidatorRequestContext requestContext,
|
||||
GrantValidationResult grantResult)
|
||||
{
|
||||
// Arrange
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements).Returns(true);
|
||||
var context = CreateContext(tokenRequest, requestContext, grantResult);
|
||||
context.CustomValidatorRequestContext.CaptchaResponse.IsBot = false;
|
||||
_sut.isValid = true;
|
||||
|
||||
context.ValidatedTokenRequest.GrantType = grantType;
|
||||
context.ValidatedTokenRequest.ClientId = "web";
|
||||
|
||||
// Configure requirement to not require SSO
|
||||
var requirement = new RequireSsoPolicyRequirement { SsoRequired = false };
|
||||
_policyRequirementQuery.GetAsync<RequireSsoPolicyRequirement>(Arg.Any<Guid>()).Returns(requirement);
|
||||
|
||||
_twoFactorAuthenticationValidator.RequiresTwoFactorAsync(requestContext.User, tokenRequest)
|
||||
.Returns(Task.FromResult(new Tuple<bool, Organization>(false, null)));
|
||||
_deviceValidator.ValidateRequestDeviceAsync(tokenRequest, requestContext)
|
||||
.Returns(Task.FromResult(true));
|
||||
|
||||
await _sut.ValidateAsync(context);
|
||||
|
||||
Assert.False(context.GrantResult.IsError);
|
||||
await _eventService.Received(1).LogUserEventAsync(
|
||||
context.CustomValidatorRequestContext.User.Id, EventType.User_LoggedIn);
|
||||
await _userRepository.Received(1).ReplaceAsync(Arg.Any<User>());
|
||||
}
|
||||
|
||||
// Test grantTypes where SSO would be required but the user is not in an
|
||||
// organization that requires it
|
||||
[Theory]
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Context;
|
||||
@ -61,7 +62,8 @@ IBaseRequestValidatorTestWrapper
|
||||
IPolicyService policyService,
|
||||
IFeatureService featureService,
|
||||
ISsoConfigRepository ssoConfigRepository,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) :
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||
IPolicyRequirementQuery policyRequirementQuery) :
|
||||
base(
|
||||
userManager,
|
||||
userService,
|
||||
@ -77,7 +79,8 @@ IBaseRequestValidatorTestWrapper
|
||||
policyService,
|
||||
featureService,
|
||||
ssoConfigRepository,
|
||||
userDecryptionOptionsBuilder)
|
||||
userDecryptionOptionsBuilder,
|
||||
policyRequirementQuery)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -599,5 +599,11 @@ public class CollectionRepositoryTests
|
||||
Assert.True(actualOrgUser3.Manage);
|
||||
Assert.False(actualOrgUser3.HidePasswords);
|
||||
Assert.True(actualOrgUser3.ReadOnly);
|
||||
|
||||
// Clean up data
|
||||
await userRepository.DeleteAsync(user);
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
await groupRepository.DeleteManyAsync([group1.Id, group2.Id, group3.Id]);
|
||||
await organizationUserRepository.DeleteManyAsync([orgUser1.Id, orgUser2.Id, orgUser3.Id]);
|
||||
}
|
||||
}
|
||||
|
@ -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,118 @@
|
||||
CREATE OR ALTER PROCEDURE [dbo].[Collection_UpdateWithGroupsAndUsers]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@Name VARCHAR(MAX),
|
||||
@ExternalId NVARCHAR(300),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7),
|
||||
@Groups AS [dbo].[CollectionAccessSelectionType] READONLY,
|
||||
@Users AS [dbo].[CollectionAccessSelectionType] READONLY
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
EXEC [dbo].[Collection_Update] @Id, @OrganizationId, @Name, @ExternalId, @CreationDate, @RevisionDate
|
||||
|
||||
-- Groups
|
||||
-- Delete groups that are no longer in source
|
||||
DELETE cg
|
||||
FROM [dbo].[CollectionGroup] cg
|
||||
LEFT JOIN @Groups g ON cg.GroupId = g.Id
|
||||
WHERE cg.CollectionId = @Id
|
||||
AND g.Id IS NULL;
|
||||
|
||||
-- Update existing groups
|
||||
UPDATE cg
|
||||
SET cg.ReadOnly = g.ReadOnly,
|
||||
cg.HidePasswords = g.HidePasswords,
|
||||
cg.Manage = g.Manage
|
||||
FROM [dbo].[CollectionGroup] cg
|
||||
INNER JOIN @Groups g ON cg.GroupId = g.Id
|
||||
WHERE cg.CollectionId = @Id
|
||||
AND (cg.ReadOnly != g.ReadOnly
|
||||
OR cg.HidePasswords != g.HidePasswords
|
||||
OR cg.Manage != g.Manage);
|
||||
|
||||
-- Insert new groups
|
||||
INSERT INTO [dbo].[CollectionGroup]
|
||||
(
|
||||
[CollectionId],
|
||||
[GroupId],
|
||||
[ReadOnly],
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
)
|
||||
SELECT
|
||||
@Id,
|
||||
g.Id,
|
||||
g.ReadOnly,
|
||||
g.HidePasswords,
|
||||
g.Manage
|
||||
FROM @Groups g
|
||||
INNER JOIN [dbo].[Group] grp ON grp.Id = g.Id
|
||||
LEFT JOIN [dbo].[CollectionGroup] cg
|
||||
ON cg.CollectionId = @Id AND cg.GroupId = g.Id
|
||||
WHERE grp.OrganizationId = @OrganizationId
|
||||
AND cg.CollectionId IS NULL;
|
||||
|
||||
-- Users
|
||||
-- Delete users that are no longer in source
|
||||
DELETE cu
|
||||
FROM [dbo].[CollectionUser] cu
|
||||
LEFT JOIN @Users u ON cu.OrganizationUserId = u.Id
|
||||
WHERE cu.CollectionId = @Id
|
||||
AND u.Id IS NULL;
|
||||
|
||||
-- Update existing users
|
||||
UPDATE cu
|
||||
SET cu.ReadOnly = u.ReadOnly,
|
||||
cu.HidePasswords = u.HidePasswords,
|
||||
cu.Manage = u.Manage
|
||||
FROM [dbo].[CollectionUser] cu
|
||||
INNER JOIN @Users u ON cu.OrganizationUserId = u.Id
|
||||
WHERE cu.CollectionId = @Id
|
||||
AND (cu.ReadOnly != u.ReadOnly
|
||||
OR cu.HidePasswords != u.HidePasswords
|
||||
OR cu.Manage != u.Manage);
|
||||
|
||||
-- Insert new users
|
||||
INSERT INTO [dbo].[CollectionUser]
|
||||
(
|
||||
[CollectionId],
|
||||
[OrganizationUserId],
|
||||
[ReadOnly],
|
||||
[HidePasswords],
|
||||
[Manage]
|
||||
)
|
||||
SELECT
|
||||
@Id,
|
||||
u.Id,
|
||||
u.ReadOnly,
|
||||
u.HidePasswords,
|
||||
u.Manage
|
||||
FROM @Users u
|
||||
INNER JOIN [dbo].[OrganizationUser] ou ON ou.Id = u.Id
|
||||
LEFT JOIN [dbo].[CollectionUser] cu
|
||||
ON cu.CollectionId = @Id AND cu.OrganizationUserId = u.Id
|
||||
WHERE ou.OrganizationId = @OrganizationId
|
||||
AND cu.CollectionId IS NULL;
|
||||
|
||||
EXEC [dbo].[User_BumpAccountRevisionDateByCollectionId] @Id, @OrganizationId
|
||||
END
|
||||
GO
|
||||
|
||||
IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_CollectionGroup_GroupId')
|
||||
BEGIN
|
||||
CREATE NONCLUSTERED INDEX IX_CollectionGroup_GroupId
|
||||
ON [dbo].[CollectionGroup] (GroupId)
|
||||
INCLUDE (ReadOnly, HidePasswords, Manage)
|
||||
END
|
||||
GO
|
||||
|
||||
IF NOT EXISTS(SELECT name FROM sys.indexes WHERE name = 'IX_CollectionUser_OrganizationUserId')
|
||||
BEGIN
|
||||
CREATE NONCLUSTERED INDEX IX_CollectionUser_OrganizationUserId
|
||||
ON [dbo].[CollectionUser] (OrganizationUserId)
|
||||
INCLUDE (ReadOnly, HidePasswords, Manage)
|
||||
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
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user