diff --git a/.github/renovate.json5 b/.github/renovate.json5
index ac34903c1b..5c1b259539 100644
--- a/.github/renovate.json5
+++ b/.github/renovate.json5
@@ -15,8 +15,7 @@
matchManagers: ["github-actions"],
matchFileNames: [
".github/workflows/publish.yml",
- ".github/workflows/release.yml",
- ".github/workflows/repository-management.yml"
+ ".github/workflows/release.yml"
],
commitMessagePrefix: "[deps] BRE:",
reviewers: ["team:dept-bre"],
@@ -134,8 +133,8 @@
reviewers: ["team:dept-dbops"],
},
{
- matchPackageNames: ["CommandDotNet", "YamlDotNet"],
- description: "DevOps owned dependencies",
+ matchPackageNames: ["YamlDotNet"],
+ description: "BRE owned dependencies",
commitMessagePrefix: "[deps] BRE:",
reviewers: ["team:dept-bre"],
},
diff --git a/bitwarden_license/src/Sso/package-lock.json b/bitwarden_license/src/Sso/package-lock.json
index 98ea72c69e..636d6317a1 100644
--- a/bitwarden_license/src/Sso/package-lock.json
+++ b/bitwarden_license/src/Sso/package-lock.json
@@ -9,7 +9,7 @@
"version": "0.0.0",
"license": "-",
"dependencies": {
- "bootstrap": "5.3.3",
+ "bootstrap": "5.3.6",
"font-awesome": "4.7.0",
"jquery": "3.7.1"
},
@@ -17,9 +17,9 @@
"css-loader": "7.1.2",
"expose-loader": "5.0.1",
"mini-css-extract-plugin": "2.9.2",
- "sass": "1.85.0",
- "sass-loader": "16.0.4",
- "webpack": "5.97.1",
+ "sass": "1.88.0",
+ "sass-loader": "16.0.5",
+ "webpack": "5.99.8",
"webpack-cli": "5.1.4"
}
},
@@ -455,13 +455,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "22.13.14",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz",
- "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==",
+ "version": "22.15.21",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
+ "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "undici-types": "~6.20.0"
+ "undici-types": "~6.21.0"
}
},
"node_modules/@webassemblyjs/ast": {
@@ -748,9 +748,9 @@
}
},
"node_modules/bootstrap": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz",
- "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==",
+ "version": "5.3.6",
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz",
+ "integrity": "sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==",
"funding": [
{
"type": "github",
@@ -781,9 +781,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.24.4",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
- "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
+ "version": "4.24.5",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
+ "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
"dev": true,
"funding": [
{
@@ -801,10 +801,10 @@
],
"license": "MIT",
"dependencies": {
- "caniuse-lite": "^1.0.30001688",
- "electron-to-chromium": "^1.5.73",
+ "caniuse-lite": "^1.0.30001716",
+ "electron-to-chromium": "^1.5.149",
"node-releases": "^2.0.19",
- "update-browserslist-db": "^1.1.1"
+ "update-browserslist-db": "^1.1.3"
},
"bin": {
"browserslist": "cli.js"
@@ -821,9 +821,9 @@
"license": "MIT"
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001707",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz",
- "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==",
+ "version": "1.0.30001718",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz",
+ "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==",
"dev": true,
"funding": [
{
@@ -975,9 +975,9 @@
}
},
"node_modules/electron-to-chromium": {
- "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==",
+ "version": "1.5.155",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz",
+ "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==",
"dev": true,
"license": "ISC"
},
@@ -1009,9 +1009,9 @@
}
},
"node_modules/es-module-lexer": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz",
- "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==",
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
"dev": true,
"license": "MIT"
},
@@ -1106,13 +1106,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/fast-json-stable-stringify": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
- "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/fast-uri": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
@@ -1248,9 +1241,9 @@
}
},
"node_modules/immutable": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz",
- "integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==",
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
+ "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==",
"dev": true,
"license": "MIT"
},
@@ -1754,16 +1747,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/punycode": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
- "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -1877,9 +1860,9 @@
"license": "MIT"
},
"node_modules/sass": {
- "version": "1.85.0",
- "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz",
- "integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==",
+ "version": "1.88.0",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz",
+ "integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1898,9 +1881,9 @@
}
},
"node_modules/sass-loader": {
- "version": "16.0.4",
- "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz",
- "integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==",
+ "version": "16.0.5",
+ "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz",
+ "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1939,9 +1922,9 @@
}
},
"node_modules/schema-utils": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz",
- "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
+ "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1959,9 +1942,9 @@
}
},
"node_modules/semver": {
- "version": "7.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
- "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -2078,9 +2061,9 @@
}
},
"node_modules/tapable": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
- "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
+ "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2088,14 +2071,14 @@
}
},
"node_modules/terser": {
- "version": "5.39.0",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
- "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==",
+ "version": "5.39.2",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz",
+ "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
- "acorn": "^8.8.2",
+ "acorn": "^8.14.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
@@ -2156,9 +2139,9 @@
}
},
"node_modules/undici-types": {
- "version": "6.20.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
- "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
@@ -2193,16 +2176,6 @@
"browserslist": ">= 4.21.0"
}
},
- "node_modules/uri-js": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
- "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "punycode": "^2.1.0"
- }
- },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -2211,9 +2184,9 @@
"license": "MIT"
},
"node_modules/watchpack": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
- "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==",
+ "version": "2.4.4",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
+ "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2225,14 +2198,15 @@
}
},
"node_modules/webpack": {
- "version": "5.97.1",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz",
- "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==",
+ "version": "5.99.8",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz",
+ "integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6",
+ "@types/json-schema": "^7.0.15",
"@webassemblyjs/ast": "^1.14.1",
"@webassemblyjs/wasm-edit": "^1.14.1",
"@webassemblyjs/wasm-parser": "^1.14.1",
@@ -2249,9 +2223,9 @@
"loader-runner": "^4.2.0",
"mime-types": "^2.1.27",
"neo-async": "^2.6.2",
- "schema-utils": "^3.2.0",
+ "schema-utils": "^4.3.2",
"tapable": "^2.1.1",
- "terser-webpack-plugin": "^5.3.10",
+ "terser-webpack-plugin": "^5.3.11",
"watchpack": "^2.4.1",
"webpack-sources": "^3.2.3"
},
@@ -2352,59 +2326,6 @@
"node": ">=10.13.0"
}
},
- "node_modules/webpack/node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/webpack/node_modules/ajv-keywords": {
- "version": "3.5.2",
- "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
- "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "ajv": "^6.9.1"
- }
- },
- "node_modules/webpack/node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/webpack/node_modules/schema-utils": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
- "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/json-schema": "^7.0.8",
- "ajv": "^6.12.5",
- "ajv-keywords": "^3.5.2"
- },
- "engines": {
- "node": ">= 10.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- }
- },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/bitwarden_license/src/Sso/package.json b/bitwarden_license/src/Sso/package.json
index 289612e79a..137f86680c 100644
--- a/bitwarden_license/src/Sso/package.json
+++ b/bitwarden_license/src/Sso/package.json
@@ -8,7 +8,7 @@
"build": "webpack"
},
"dependencies": {
- "bootstrap": "5.3.3",
+ "bootstrap": "5.3.6",
"font-awesome": "4.7.0",
"jquery": "3.7.1"
},
@@ -16,9 +16,9 @@
"css-loader": "7.1.2",
"expose-loader": "5.0.1",
"mini-css-extract-plugin": "2.9.2",
- "sass": "1.85.0",
- "sass-loader": "16.0.4",
- "webpack": "5.97.1",
+ "sass": "1.88.0",
+ "sass-loader": "16.0.5",
+ "webpack": "5.99.8",
"webpack-cli": "5.1.4"
}
}
diff --git a/src/Admin/package-lock.json b/src/Admin/package-lock.json
index 3d339bd80c..e73ccfcef5 100644
--- a/src/Admin/package-lock.json
+++ b/src/Admin/package-lock.json
@@ -9,7 +9,7 @@
"version": "0.0.0",
"license": "GPL-3.0",
"dependencies": {
- "bootstrap": "5.3.3",
+ "bootstrap": "5.3.6",
"font-awesome": "4.7.0",
"jquery": "3.7.1",
"toastr": "2.1.4"
@@ -18,9 +18,9 @@
"css-loader": "7.1.2",
"expose-loader": "5.0.1",
"mini-css-extract-plugin": "2.9.2",
- "sass": "1.85.0",
- "sass-loader": "16.0.4",
- "webpack": "5.97.1",
+ "sass": "1.88.0",
+ "sass-loader": "16.0.5",
+ "webpack": "5.99.8",
"webpack-cli": "5.1.4"
}
},
@@ -456,13 +456,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "22.13.14",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz",
- "integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==",
+ "version": "22.15.21",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
+ "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
"dev": true,
"license": "MIT",
"dependencies": {
- "undici-types": "~6.20.0"
+ "undici-types": "~6.21.0"
}
},
"node_modules/@webassemblyjs/ast": {
@@ -749,9 +749,9 @@
}
},
"node_modules/bootstrap": {
- "version": "5.3.3",
- "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz",
- "integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==",
+ "version": "5.3.6",
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz",
+ "integrity": "sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==",
"funding": [
{
"type": "github",
@@ -782,9 +782,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.24.4",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz",
- "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==",
+ "version": "4.24.5",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
+ "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
"dev": true,
"funding": [
{
@@ -802,10 +802,10 @@
],
"license": "MIT",
"dependencies": {
- "caniuse-lite": "^1.0.30001688",
- "electron-to-chromium": "^1.5.73",
+ "caniuse-lite": "^1.0.30001716",
+ "electron-to-chromium": "^1.5.149",
"node-releases": "^2.0.19",
- "update-browserslist-db": "^1.1.1"
+ "update-browserslist-db": "^1.1.3"
},
"bin": {
"browserslist": "cli.js"
@@ -822,9 +822,9 @@
"license": "MIT"
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001707",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz",
- "integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==",
+ "version": "1.0.30001718",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz",
+ "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==",
"dev": true,
"funding": [
{
@@ -976,9 +976,9 @@
}
},
"node_modules/electron-to-chromium": {
- "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==",
+ "version": "1.5.155",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz",
+ "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==",
"dev": true,
"license": "ISC"
},
@@ -1010,9 +1010,9 @@
}
},
"node_modules/es-module-lexer": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz",
- "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==",
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
"dev": true,
"license": "MIT"
},
@@ -1107,13 +1107,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/fast-json-stable-stringify": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
- "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
- "dev": true,
- "license": "MIT"
- },
"node_modules/fast-uri": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
@@ -1249,9 +1242,9 @@
}
},
"node_modules/immutable": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz",
- "integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==",
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
+ "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==",
"dev": true,
"license": "MIT"
},
@@ -1755,16 +1748,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/punycode": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
- "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6"
- }
- },
"node_modules/randombytes": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@@ -1878,9 +1861,9 @@
"license": "MIT"
},
"node_modules/sass": {
- "version": "1.85.0",
- "resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz",
- "integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==",
+ "version": "1.88.0",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz",
+ "integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1899,9 +1882,9 @@
}
},
"node_modules/sass-loader": {
- "version": "16.0.4",
- "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz",
- "integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==",
+ "version": "16.0.5",
+ "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz",
+ "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1940,9 +1923,9 @@
}
},
"node_modules/schema-utils": {
- "version": "4.3.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz",
- "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==",
+ "version": "4.3.2",
+ "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
+ "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1960,9 +1943,9 @@
}
},
"node_modules/semver": {
- "version": "7.7.1",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
- "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
+ "version": "7.7.2",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+ "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
@@ -2079,9 +2062,9 @@
}
},
"node_modules/tapable": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
- "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==",
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
+ "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2089,14 +2072,14 @@
}
},
"node_modules/terser": {
- "version": "5.39.0",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
- "integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==",
+ "version": "5.39.2",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz",
+ "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
- "acorn": "^8.8.2",
+ "acorn": "^8.14.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
@@ -2165,9 +2148,9 @@
}
},
"node_modules/undici-types": {
- "version": "6.20.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
- "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true,
"license": "MIT"
},
@@ -2202,16 +2185,6 @@
"browserslist": ">= 4.21.0"
}
},
- "node_modules/uri-js": {
- "version": "4.4.1",
- "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
- "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
- "dev": true,
- "license": "BSD-2-Clause",
- "dependencies": {
- "punycode": "^2.1.0"
- }
- },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -2220,9 +2193,9 @@
"license": "MIT"
},
"node_modules/watchpack": {
- "version": "2.4.2",
- "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
- "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==",
+ "version": "2.4.4",
+ "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
+ "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2234,14 +2207,15 @@
}
},
"node_modules/webpack": {
- "version": "5.97.1",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz",
- "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==",
+ "version": "5.99.8",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz",
+ "integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6",
+ "@types/json-schema": "^7.0.15",
"@webassemblyjs/ast": "^1.14.1",
"@webassemblyjs/wasm-edit": "^1.14.1",
"@webassemblyjs/wasm-parser": "^1.14.1",
@@ -2258,9 +2232,9 @@
"loader-runner": "^4.2.0",
"mime-types": "^2.1.27",
"neo-async": "^2.6.2",
- "schema-utils": "^3.2.0",
+ "schema-utils": "^4.3.2",
"tapable": "^2.1.1",
- "terser-webpack-plugin": "^5.3.10",
+ "terser-webpack-plugin": "^5.3.11",
"watchpack": "^2.4.1",
"webpack-sources": "^3.2.3"
},
@@ -2361,59 +2335,6 @@
"node": ">=10.13.0"
}
},
- "node_modules/webpack/node_modules/ajv": {
- "version": "6.12.6",
- "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
- "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "fast-deep-equal": "^3.1.1",
- "fast-json-stable-stringify": "^2.0.0",
- "json-schema-traverse": "^0.4.1",
- "uri-js": "^4.2.2"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/epoberezkin"
- }
- },
- "node_modules/webpack/node_modules/ajv-keywords": {
- "version": "3.5.2",
- "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
- "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
- "dev": true,
- "license": "MIT",
- "peerDependencies": {
- "ajv": "^6.9.1"
- }
- },
- "node_modules/webpack/node_modules/json-schema-traverse": {
- "version": "0.4.1",
- "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
- "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
- "dev": true,
- "license": "MIT"
- },
- "node_modules/webpack/node_modules/schema-utils": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
- "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/json-schema": "^7.0.8",
- "ajv": "^6.12.5",
- "ajv-keywords": "^3.5.2"
- },
- "engines": {
- "node": ">= 10.13.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/webpack"
- }
- },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/src/Admin/package.json b/src/Admin/package.json
index eed8eaf7aa..e88cd42eca 100644
--- a/src/Admin/package.json
+++ b/src/Admin/package.json
@@ -8,7 +8,7 @@
"build": "webpack"
},
"dependencies": {
- "bootstrap": "5.3.3",
+ "bootstrap": "5.3.6",
"font-awesome": "4.7.0",
"jquery": "3.7.1",
"toastr": "2.1.4"
@@ -17,9 +17,9 @@
"css-loader": "7.1.2",
"expose-loader": "5.0.1",
"mini-css-extract-plugin": "2.9.2",
- "sass": "1.85.0",
- "sass-loader": "16.0.4",
- "webpack": "5.97.1",
+ "sass": "1.88.0",
+ "sass-loader": "16.0.5",
+ "webpack": "5.99.8",
"webpack-cli": "5.1.4"
}
}
diff --git a/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs b/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs
index a8bef10dc6..c0ab5c059b 100644
--- a/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs
+++ b/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs
@@ -2,10 +2,10 @@
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
+using Bit.Core.AdminConsole.Models.Data.Integrations;
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 Bit.Core.Utilities;
diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs
index 6566760e17..ccab2b36ae 100644
--- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs
+++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs
@@ -1,8 +1,8 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
+using Bit.Core.AdminConsole.Models.Data.Integrations;
using Bit.Core.Enums;
-using Bit.Core.Models.Data.Integrations;
#nullable enable
diff --git a/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs b/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs
index 2fb9a67199..90b265715d 100644
--- a/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs
+++ b/src/Api/Auth/Models/Response/EmergencyAccessResponseModel.cs
@@ -90,6 +90,13 @@ public class EmergencyAccessGrantorDetailsResponseModel : EmergencyAccessRespons
public class EmergencyAccessTakeoverResponseModel : ResponseModel
{
+ ///
+ /// Creates a new instance of the class.
+ ///
+ /// Consumed for the Encrypted Key value
+ /// consumed for the KDF configuration
+ /// name of the object
+ /// emergencyAccess cannot be null
public EmergencyAccessTakeoverResponseModel(EmergencyAccess emergencyAccess, User grantor, string obj = "emergencyAccessTakeover") : base(obj)
{
if (emergencyAccess == null)
diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs
index 9e57545098..094ca0a435 100644
--- a/src/Api/Billing/Controllers/OrganizationBillingController.cs
+++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs
@@ -302,8 +302,12 @@ public class OrganizationBillingController(
Debug.Assert(org is not null, "This organization has already been found via this same ID, this should be fine.");
var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken);
var taxInformation = TaxInformation.From(organizationSignup.TaxInfo);
- await organizationBillingService.UpdatePaymentMethod(org, paymentSource, taxInformation);
await organizationBillingService.Finalize(sale);
+ var updatedOrg = await organizationRepository.GetByIdAsync(organizationId);
+ if (updatedOrg != null)
+ {
+ await organizationBillingService.UpdatePaymentMethod(updatedOrg, paymentSource, taxInformation);
+ }
return TypedResults.Ok();
}
diff --git a/src/Api/Startup.cs b/src/Api/Startup.cs
index 68949b052b..e24f96a7a9 100644
--- a/src/Api/Startup.cs
+++ b/src/Api/Startup.cs
@@ -28,10 +28,8 @@ using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
using Bit.Api.Auth.Models.Request.WebAuthn;
using Bit.Api.Billing;
-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,18 +222,8 @@ public class Startup
services.AddHostedService();
}
- // Slack
- if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) &&
- CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) &&
- CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes))
- {
- services.AddHttpClient(SlackService.HttpClientName);
- services.AddSingleton();
- }
- else
- {
- services.AddSingleton();
- }
+ // Add SlackService for OAuth API requests - if configured
+ services.AddSlackService(globalSettings);
}
public void Configure(
diff --git a/src/Api/Vault/Controllers/CiphersController.cs b/src/Api/Vault/Controllers/CiphersController.cs
index 4f105128ea..251362589e 100644
--- a/src/Api/Vault/Controllers/CiphersController.cs
+++ b/src/Api/Vault/Controllers/CiphersController.cs
@@ -151,6 +151,16 @@ public class CiphersController : Controller
public async Task Post([FromBody] CipherRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
+
+ // Validate the model was encrypted for the posting user
+ if (model.EncryptedFor != null)
+ {
+ if (model.EncryptedFor != user.Id)
+ {
+ throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
+ }
+ }
+
var cipher = model.ToCipherDetails(user.Id);
if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value))
{
@@ -170,6 +180,16 @@ public class CiphersController : Controller
public async Task PostCreate([FromBody] CipherCreateRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
+
+ // Validate the model was encrypted for the posting user
+ if (model.Cipher.EncryptedFor != null)
+ {
+ if (model.Cipher.EncryptedFor != user.Id)
+ {
+ throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
+ }
+ }
+
var cipher = model.Cipher.ToCipherDetails(user.Id);
if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value))
{
@@ -192,6 +212,16 @@ public class CiphersController : Controller
}
var userId = _userService.GetProperUserId(User).Value;
+
+ // Validate the model was encrypted for the posting user
+ if (model.Cipher.EncryptedFor != null)
+ {
+ if (model.Cipher.EncryptedFor != userId)
+ {
+ throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
+ }
+ }
+
await _cipherService.SaveAsync(cipher, userId, model.Cipher.LastKnownRevisionDate, model.CollectionIds, true, false);
var response = new CipherMiniResponseModel(cipher, _globalSettings, false);
@@ -209,6 +239,15 @@ public class CiphersController : Controller
throw new NotFoundException();
}
+ // Validate the model was encrypted for the posting user
+ if (model.EncryptedFor != null)
+ {
+ if (model.EncryptedFor != user.Id)
+ {
+ throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
+ }
+ }
+
ValidateClientVersionForFido2CredentialSupport(cipher);
var collectionIds = (await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id)).Select(c => c.CollectionId).ToList();
@@ -237,6 +276,15 @@ public class CiphersController : Controller
var userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(id);
+ // Validate the model was encrypted for the posting user
+ if (model.EncryptedFor != null)
+ {
+ if (model.EncryptedFor != userId)
+ {
+ throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
+ }
+ }
+
ValidateClientVersionForFido2CredentialSupport(cipher);
if (cipher == null || !cipher.OrganizationId.HasValue ||
@@ -658,6 +706,15 @@ public class CiphersController : Controller
throw new NotFoundException();
}
+ // Validate the model was encrypted for the posting user
+ if (model.Cipher.EncryptedFor != null)
+ {
+ if (model.Cipher.EncryptedFor != user.Id)
+ {
+ throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
+ }
+ }
+
ValidateClientVersionForFido2CredentialSupport(cipher);
var original = cipher.Clone();
@@ -1019,6 +1076,18 @@ public class CiphersController : Controller
var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: false);
var ciphersDict = ciphers.ToDictionary(c => c.Id);
+ // Validate the model was encrypted for the posting user
+ foreach (var cipher in model.Ciphers)
+ {
+ if (cipher.EncryptedFor != null)
+ {
+ if (cipher.EncryptedFor != userId)
+ {
+ throw new BadRequestException("Cipher was not encrypted for the current user. Please try again.");
+ }
+ }
+ }
+
var shareCiphers = new List<(Cipher, DateTime?)>();
foreach (var cipher in model.Ciphers)
{
diff --git a/src/Api/Vault/Models/Request/CipherRequestModel.cs b/src/Api/Vault/Models/Request/CipherRequestModel.cs
index 89eda415b1..5c288ab66d 100644
--- a/src/Api/Vault/Models/Request/CipherRequestModel.cs
+++ b/src/Api/Vault/Models/Request/CipherRequestModel.cs
@@ -11,6 +11,10 @@ namespace Bit.Api.Vault.Models.Request;
public class CipherRequestModel
{
+ ///
+ /// The Id of the user that encrypted the cipher. It should always represent a UserId.
+ ///
+ public Guid? EncryptedFor { get; set; }
public CipherType Type { get; set; }
[StringLength(36)]
diff --git a/src/Core/AdminConsole/Enums/IntegrationType.cs b/src/Core/AdminConsole/Enums/IntegrationType.cs
index 0f5123554e..5edd54df23 100644
--- a/src/Core/AdminConsole/Enums/IntegrationType.cs
+++ b/src/Core/AdminConsole/Enums/IntegrationType.cs
@@ -7,3 +7,19 @@ public enum IntegrationType : int
Slack = 3,
Webhook = 4,
}
+
+public static class IntegrationTypeExtensions
+{
+ public static string ToRoutingKey(this IntegrationType type)
+ {
+ switch (type)
+ {
+ case IntegrationType.Slack:
+ return "slack";
+ case IntegrationType.Webhook:
+ return "webhook";
+ default:
+ throw new ArgumentOutOfRangeException(nameof(type), $"Unsupported integration type: {type}");
+ }
+ }
+}
diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs b/src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs
new file mode 100644
index 0000000000..bd1f280cad
--- /dev/null
+++ b/src/Core/AdminConsole/Models/Data/Integrations/IIntegrationMessage.cs
@@ -0,0 +1,12 @@
+using Bit.Core.Enums;
+
+namespace Bit.Core.AdminConsole.Models.Data.Integrations;
+
+public interface IIntegrationMessage
+{
+ IntegrationType IntegrationType { get; }
+ int RetryCount { get; set; }
+ DateTime? DelayUntilDate { get; set; }
+ void ApplyRetry(DateTime? handlerDelayUntilDate);
+ string ToJson();
+}
diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs
new file mode 100644
index 0000000000..d2f0bde693
--- /dev/null
+++ b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationHandlerResult.cs
@@ -0,0 +1,16 @@
+namespace Bit.Core.AdminConsole.Models.Data.Integrations;
+
+public class IntegrationHandlerResult
+{
+ public IntegrationHandlerResult(bool success, IIntegrationMessage message)
+ {
+ Success = success;
+ Message = message;
+ }
+
+ public bool Success { get; set; } = false;
+ public bool Retryable { get; set; } = false;
+ public IIntegrationMessage Message { get; set; }
+ public DateTime? DelayUntilDate { get; set; }
+ public string FailureReason { get; set; } = string.Empty;
+}
diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs
new file mode 100644
index 0000000000..1f288914d0
--- /dev/null
+++ b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationMessage.cs
@@ -0,0 +1,34 @@
+using System.Text.Json;
+using Bit.Core.Enums;
+
+namespace Bit.Core.AdminConsole.Models.Data.Integrations;
+
+public class IntegrationMessage : IIntegrationMessage
+{
+ public IntegrationType IntegrationType { get; set; }
+ public T Configuration { get; set; }
+ public string RenderedTemplate { get; set; }
+ public int RetryCount { get; set; } = 0;
+ public DateTime? DelayUntilDate { get; set; }
+
+ public void ApplyRetry(DateTime? handlerDelayUntilDate)
+ {
+ RetryCount++;
+
+ var baseTime = handlerDelayUntilDate ?? DateTime.UtcNow;
+ var backoffSeconds = Math.Pow(2, RetryCount);
+ var jitterSeconds = Random.Shared.Next(0, 3);
+
+ DelayUntilDate = baseTime.AddSeconds(backoffSeconds + jitterSeconds);
+ }
+
+ public string ToJson()
+ {
+ return JsonSerializer.Serialize(this);
+ }
+
+ public static IntegrationMessage FromJson(string json)
+ {
+ return JsonSerializer.Deserialize>(json);
+ }
+}
diff --git a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationTemplateContext.cs b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationTemplateContext.cs
index 18aa3b7681..338c2b963d 100644
--- a/src/Core/AdminConsole/Models/Data/Integrations/IntegrationTemplateContext.cs
+++ b/src/Core/AdminConsole/Models/Data/Integrations/IntegrationTemplateContext.cs
@@ -1,10 +1,11 @@
-using Bit.Core.AdminConsole.Entities;
+#nullable enable
+
+using Bit.Core.AdminConsole.Entities;
using Bit.Core.Entities;
using Bit.Core.Enums;
+using Bit.Core.Models.Data;
-#nullable enable
-
-namespace Bit.Core.Models.Data.Integrations;
+namespace Bit.Core.AdminConsole.Models.Data.Integrations;
public class IntegrationTemplateContext(EventMessage eventMessage)
{
diff --git a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs
index e6fc1440ea..4fcce542ce 100644
--- a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs
+++ b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegration.cs
@@ -1,3 +1,3 @@
-namespace Bit.Core.Models.Data.Integrations;
+namespace Bit.Core.AdminConsole.Models.Data.Integrations;
public record SlackIntegration(string token);
diff --git a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs
index ad25d35e7e..2930004cbf 100644
--- a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs
+++ b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfiguration.cs
@@ -1,3 +1,3 @@
-namespace Bit.Core.Models.Data.Integrations;
+namespace Bit.Core.AdminConsole.Models.Data.Integrations;
public record SlackIntegrationConfiguration(string channelId);
diff --git a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs
index 49ca9df4e0..b81e50d403 100644
--- a/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs
+++ b/src/Core/AdminConsole/Models/Data/Integrations/SlackIntegrationConfigurationDetails.cs
@@ -1,3 +1,3 @@
-namespace Bit.Core.Models.Data.Integrations;
+namespace Bit.Core.AdminConsole.Models.Data.Integrations;
public record SlackIntegrationConfigurationDetails(string channelId, string token);
diff --git a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs
index 9a7591f24b..e8217d3ad3 100644
--- a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs
+++ b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfiguration.cs
@@ -1,3 +1,3 @@
-namespace Bit.Core.Models.Data.Integrations;
+namespace Bit.Core.AdminConsole.Models.Data.Integrations;
public record WebhookIntegrationConfiguration(string url);
diff --git a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs
new file mode 100644
index 0000000000..e3e92c900f
--- /dev/null
+++ b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetails.cs
@@ -0,0 +1,3 @@
+namespace Bit.Core.AdminConsole.Models.Data.Integrations;
+
+public record WebhookIntegrationConfigurationDetails(string url);
diff --git a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetils.cs b/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetils.cs
deleted file mode 100644
index f165828de0..0000000000
--- a/src/Core/AdminConsole/Models/Data/Integrations/WebhookIntegrationConfigurationDetils.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-namespace Bit.Core.Models.Data.Integrations;
-
-public record WebhookIntegrationConfigurationDetils(string url);
diff --git a/src/Core/AdminConsole/Services/IIntegrationHandler.cs b/src/Core/AdminConsole/Services/IIntegrationHandler.cs
new file mode 100644
index 0000000000..bf6e6791cf
--- /dev/null
+++ b/src/Core/AdminConsole/Services/IIntegrationHandler.cs
@@ -0,0 +1,24 @@
+using Bit.Core.AdminConsole.Models.Data.Integrations;
+
+namespace Bit.Core.Services;
+
+public interface IIntegrationHandler
+{
+ Task HandleAsync(string json);
+}
+
+public interface IIntegrationHandler : IIntegrationHandler
+{
+ Task HandleAsync(IntegrationMessage message);
+}
+
+public abstract class IntegrationHandlerBase : IIntegrationHandler
+{
+ public async Task HandleAsync(string json)
+ {
+ var message = IntegrationMessage.FromJson(json);
+ return await HandleAsync(message);
+ }
+
+ public abstract Task HandleAsync(IntegrationMessage message);
+}
diff --git a/src/Core/AdminConsole/Services/IIntegrationPublisher.cs b/src/Core/AdminConsole/Services/IIntegrationPublisher.cs
new file mode 100644
index 0000000000..986ea776e1
--- /dev/null
+++ b/src/Core/AdminConsole/Services/IIntegrationPublisher.cs
@@ -0,0 +1,8 @@
+using Bit.Core.AdminConsole.Models.Data.Integrations;
+
+namespace Bit.Core.Services;
+
+public interface IIntegrationPublisher
+{
+ Task PublishAsync(IIntegrationMessage message);
+}
diff --git a/src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs
new file mode 100644
index 0000000000..9a80ed67b2
--- /dev/null
+++ b/src/Core/AdminConsole/Services/Implementations/EventIntegrationHandler.cs
@@ -0,0 +1,83 @@
+using System.Text.Json;
+using Bit.Core.AdminConsole.Models.Data.Integrations;
+using Bit.Core.AdminConsole.Utilities;
+using Bit.Core.Enums;
+using Bit.Core.Models.Data;
+using Bit.Core.Repositories;
+
+namespace Bit.Core.Services;
+
+#nullable enable
+
+public class EventIntegrationHandler(
+ IntegrationType integrationType,
+ IIntegrationPublisher integrationPublisher,
+ IOrganizationIntegrationConfigurationRepository configurationRepository,
+ IUserRepository userRepository,
+ IOrganizationRepository organizationRepository)
+ : IEventMessageHandler
+{
+ public async Task HandleEventAsync(EventMessage eventMessage)
+ {
+ if (eventMessage.OrganizationId is not Guid organizationId)
+ {
+ return;
+ }
+
+ var configurations = await configurationRepository.GetConfigurationDetailsAsync(
+ organizationId,
+ integrationType,
+ eventMessage.Type);
+
+ foreach (var configuration in configurations)
+ {
+ var template = configuration.Template ?? string.Empty;
+ var context = await BuildContextAsync(eventMessage, template);
+ var renderedTemplate = IntegrationTemplateProcessor.ReplaceTokens(template, context);
+
+ var config = configuration.MergedConfiguration.Deserialize()
+ ?? throw new InvalidOperationException($"Failed to deserialize to {typeof(T).Name}");
+
+ var message = new IntegrationMessage
+ {
+ IntegrationType = integrationType,
+ Configuration = config,
+ RenderedTemplate = renderedTemplate,
+ RetryCount = 0,
+ DelayUntilDate = null
+ };
+
+ await integrationPublisher.PublishAsync(message);
+ }
+ }
+
+ public async Task HandleManyEventsAsync(IEnumerable eventMessages)
+ {
+ foreach (var eventMessage in eventMessages)
+ {
+ await HandleEventAsync(eventMessage);
+ }
+ }
+
+ private async Task BuildContextAsync(EventMessage eventMessage, string template)
+ {
+ var context = new IntegrationTemplateContext(eventMessage);
+
+ if (IntegrationTemplateProcessor.TemplateRequiresUser(template) && eventMessage.UserId.HasValue)
+ {
+ context.User = await userRepository.GetByIdAsync(eventMessage.UserId.Value);
+ }
+
+ if (IntegrationTemplateProcessor.TemplateRequiresActingUser(template) && eventMessage.ActingUserId.HasValue)
+ {
+ context.ActingUser = await userRepository.GetByIdAsync(eventMessage.ActingUserId.Value);
+ }
+
+ if (IntegrationTemplateProcessor.TemplateRequiresOrganization(template) && eventMessage.OrganizationId.HasValue)
+ {
+ context.Organization = await organizationRepository.GetByIdAsync(eventMessage.OrganizationId.Value);
+ }
+
+ return context;
+ }
+}
diff --git a/src/Core/AdminConsole/Services/Implementations/IntegrationEventHandlerBase.cs b/src/Core/AdminConsole/Services/Implementations/IntegrationEventHandlerBase.cs
index d8e521de97..4df2d25b1b 100644
--- a/src/Core/AdminConsole/Services/Implementations/IntegrationEventHandlerBase.cs
+++ b/src/Core/AdminConsole/Services/Implementations/IntegrationEventHandlerBase.cs
@@ -1,8 +1,8 @@
using System.Text.Json.Nodes;
+using Bit.Core.AdminConsole.Models.Data.Integrations;
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;
diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs
index 1ee3fa5ea7..74833f38a0 100644
--- a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs
+++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventListenerService.cs
@@ -29,7 +29,7 @@ public class RabbitMqEventListenerService : EventLoggingListenerService
UserName = globalSettings.EventLogging.RabbitMq.Username,
Password = globalSettings.EventLogging.RabbitMq.Password
};
- _exchangeName = globalSettings.EventLogging.RabbitMq.ExchangeName;
+ _exchangeName = globalSettings.EventLogging.RabbitMq.EventExchangeName;
_logger = logger;
_queueName = queueName;
}
diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs
index 86abddec58..05fcf71a92 100644
--- a/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs
+++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqEventWriteService.cs
@@ -18,7 +18,7 @@ public class RabbitMqEventWriteService : IEventWriteService, IAsyncDisposable
UserName = globalSettings.EventLogging.RabbitMq.Username,
Password = globalSettings.EventLogging.RabbitMq.Password
};
- _exchangeName = globalSettings.EventLogging.RabbitMq.ExchangeName;
+ _exchangeName = globalSettings.EventLogging.RabbitMq.EventExchangeName;
_lazyConnection = new Lazy>(CreateConnectionAsync);
}
diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs
new file mode 100644
index 0000000000..1d6910db95
--- /dev/null
+++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationListenerService.cs
@@ -0,0 +1,191 @@
+using System.Text;
+using Bit.Core.Settings;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using RabbitMQ.Client;
+using RabbitMQ.Client.Events;
+
+namespace Bit.Core.Services;
+
+public class RabbitMqIntegrationListenerService : BackgroundService
+{
+ private const string _deadLetterRoutingKey = "dead-letter";
+ private IChannel _channel;
+ private IConnection _connection;
+ private readonly string _exchangeName;
+ private readonly string _queueName;
+ private readonly string _retryQueueName;
+ private readonly string _deadLetterQueueName;
+ private readonly string _routingKey;
+ private readonly string _retryRoutingKey;
+ private readonly int _maxRetries;
+ private readonly IIntegrationHandler _handler;
+ private readonly ConnectionFactory _factory;
+ private readonly ILogger _logger;
+ private readonly int _retryTiming;
+
+ public RabbitMqIntegrationListenerService(IIntegrationHandler handler,
+ string routingKey,
+ string queueName,
+ string retryQueueName,
+ string deadLetterQueueName,
+ GlobalSettings globalSettings,
+ ILogger logger)
+ {
+ _handler = handler;
+ _routingKey = routingKey;
+ _retryRoutingKey = $"{_routingKey}-retry";
+ _queueName = queueName;
+ _retryQueueName = retryQueueName;
+ _deadLetterQueueName = deadLetterQueueName;
+ _logger = logger;
+ _exchangeName = globalSettings.EventLogging.RabbitMq.IntegrationExchangeName;
+ _maxRetries = globalSettings.EventLogging.RabbitMq.MaxRetries;
+ _retryTiming = globalSettings.EventLogging.RabbitMq.RetryTiming;
+
+ _factory = new ConnectionFactory
+ {
+ HostName = globalSettings.EventLogging.RabbitMq.HostName,
+ UserName = globalSettings.EventLogging.RabbitMq.Username,
+ Password = globalSettings.EventLogging.RabbitMq.Password
+ };
+ }
+
+ public override async Task StartAsync(CancellationToken cancellationToken)
+ {
+ _connection = await _factory.CreateConnectionAsync(cancellationToken);
+ _channel = await _connection.CreateChannelAsync(cancellationToken: cancellationToken);
+
+ await _channel.ExchangeDeclareAsync(exchange: _exchangeName,
+ type: ExchangeType.Direct,
+ durable: true,
+ cancellationToken: cancellationToken);
+
+ // Declare main queue
+ await _channel.QueueDeclareAsync(queue: _queueName,
+ durable: true,
+ exclusive: false,
+ autoDelete: false,
+ arguments: null,
+ cancellationToken: cancellationToken);
+ await _channel.QueueBindAsync(queue: _queueName,
+ exchange: _exchangeName,
+ routingKey: _routingKey,
+ cancellationToken: cancellationToken);
+
+ // Declare retry queue (Configurable TTL, dead-letters back to main queue)
+ await _channel.QueueDeclareAsync(queue: _retryQueueName,
+ durable: true,
+ exclusive: false,
+ autoDelete: false,
+ arguments: new Dictionary
+ {
+ { "x-dead-letter-exchange", _exchangeName },
+ { "x-dead-letter-routing-key", _routingKey },
+ { "x-message-ttl", _retryTiming }
+ },
+ cancellationToken: cancellationToken);
+ await _channel.QueueBindAsync(queue: _retryQueueName,
+ exchange: _exchangeName,
+ routingKey: _retryRoutingKey,
+ cancellationToken: cancellationToken);
+
+ // Declare dead letter queue
+ await _channel.QueueDeclareAsync(queue: _deadLetterQueueName,
+ durable: true,
+ exclusive: false,
+ autoDelete: false,
+ arguments: null,
+ cancellationToken: cancellationToken);
+ await _channel.QueueBindAsync(queue: _deadLetterQueueName,
+ exchange: _exchangeName,
+ routingKey: _deadLetterRoutingKey,
+ cancellationToken: cancellationToken);
+
+ await base.StartAsync(cancellationToken);
+ }
+
+ protected override async Task ExecuteAsync(CancellationToken cancellationToken)
+ {
+ var consumer = new AsyncEventingBasicConsumer(_channel);
+ consumer.ReceivedAsync += async (_, ea) =>
+ {
+ var json = Encoding.UTF8.GetString(ea.Body.Span);
+
+ try
+ {
+ var result = await _handler.HandleAsync(json);
+ var message = result.Message;
+
+ if (result.Success)
+ {
+ // Successful integration send. Acknowledge message delivery and return
+ await _channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken);
+ return;
+ }
+
+ if (result.Retryable)
+ {
+ // Integration failed, but is retryable - apply delay and check max retries
+ message.ApplyRetry(result.DelayUntilDate);
+
+ if (message.RetryCount < _maxRetries)
+ {
+ // Publish message to the retry queue. It will be re-published for retry after a delay
+ await _channel.BasicPublishAsync(
+ exchange: _exchangeName,
+ routingKey: _retryRoutingKey,
+ body: Encoding.UTF8.GetBytes(message.ToJson()),
+ cancellationToken: cancellationToken);
+ }
+ else
+ {
+ // Exceeded the max number of retries; fail and send to dead letter queue
+ await PublishToDeadLetterAsync(message.ToJson());
+ _logger.LogWarning("Max retry attempts reached. Sent to DLQ.");
+ }
+ }
+ else
+ {
+ // Fatal error (i.e. not retryable) occurred. Send message to dead letter queue without any retries
+ await PublishToDeadLetterAsync(message.ToJson());
+ _logger.LogWarning("Non-retryable failure. Sent to DLQ.");
+ }
+
+ // Message has been sent to retry or dead letter queues.
+ // Acknowledge receipt so Rabbit knows it's been processed
+ await _channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken);
+ }
+ catch (Exception ex)
+ {
+ // Unknown error occurred. Acknowledge so Rabbit doesn't keep attempting. Log the error
+ _logger.LogError(ex, "Unhandled error processing integration message.");
+ await _channel.BasicAckAsync(ea.DeliveryTag, false, cancellationToken);
+ }
+ };
+
+ await _channel.BasicConsumeAsync(queue: _queueName, autoAck: false, consumer: consumer, cancellationToken: cancellationToken);
+ }
+
+ private async Task PublishToDeadLetterAsync(string json)
+ {
+ await _channel.BasicPublishAsync(
+ exchange: _exchangeName,
+ routingKey: _deadLetterRoutingKey,
+ body: Encoding.UTF8.GetBytes(json));
+ }
+
+ public override async Task StopAsync(CancellationToken cancellationToken)
+ {
+ await _channel.CloseAsync(cancellationToken);
+ await _connection.CloseAsync(cancellationToken);
+ await base.StopAsync(cancellationToken);
+ }
+
+ public override void Dispose()
+ {
+ _channel.Dispose();
+ _connection.Dispose();
+ base.Dispose();
+ }
+}
diff --git a/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationPublisher.cs b/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationPublisher.cs
new file mode 100644
index 0000000000..12801e3216
--- /dev/null
+++ b/src/Core/AdminConsole/Services/Implementations/RabbitMqIntegrationPublisher.cs
@@ -0,0 +1,54 @@
+using System.Text;
+using Bit.Core.AdminConsole.Models.Data.Integrations;
+using Bit.Core.Enums;
+using Bit.Core.Settings;
+using RabbitMQ.Client;
+
+namespace Bit.Core.Services;
+
+public class RabbitMqIntegrationPublisher : IIntegrationPublisher, IAsyncDisposable
+{
+ private readonly ConnectionFactory _factory;
+ private readonly Lazy> _lazyConnection;
+ private readonly string _exchangeName;
+
+ public RabbitMqIntegrationPublisher(GlobalSettings globalSettings)
+ {
+ _factory = new ConnectionFactory
+ {
+ HostName = globalSettings.EventLogging.RabbitMq.HostName,
+ UserName = globalSettings.EventLogging.RabbitMq.Username,
+ Password = globalSettings.EventLogging.RabbitMq.Password
+ };
+ _exchangeName = globalSettings.EventLogging.RabbitMq.IntegrationExchangeName;
+
+ _lazyConnection = new Lazy>(CreateConnectionAsync);
+ }
+
+ public async Task PublishAsync(IIntegrationMessage message)
+ {
+ var routingKey = message.IntegrationType.ToRoutingKey();
+ var connection = await _lazyConnection.Value;
+ await using var channel = await connection.CreateChannelAsync();
+
+ await channel.ExchangeDeclareAsync(exchange: _exchangeName, type: ExchangeType.Direct, durable: true);
+
+ var body = Encoding.UTF8.GetBytes(message.ToJson());
+
+ await channel.BasicPublishAsync(exchange: _exchangeName, routingKey: routingKey, body: body);
+ }
+
+ public async ValueTask DisposeAsync()
+ {
+ if (_lazyConnection.IsValueCreated)
+ {
+ var connection = await _lazyConnection.Value;
+ await connection.DisposeAsync();
+ }
+ }
+
+ private async Task CreateConnectionAsync()
+ {
+ return await _factory.CreateConnectionAsync();
+ }
+}
diff --git a/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs b/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs
index 3ddecc67f4..a767776c36 100644
--- a/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs
+++ b/src/Core/AdminConsole/Services/Implementations/SlackEventHandler.cs
@@ -1,7 +1,7 @@
using System.Text.Json;
using System.Text.Json.Nodes;
+using Bit.Core.AdminConsole.Models.Data.Integrations;
using Bit.Core.Enums;
-using Bit.Core.Models.Data.Integrations;
using Bit.Core.Repositories;
#nullable enable
diff --git a/src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs
new file mode 100644
index 0000000000..134e93e838
--- /dev/null
+++ b/src/Core/AdminConsole/Services/Implementations/SlackIntegrationHandler.cs
@@ -0,0 +1,19 @@
+using Bit.Core.AdminConsole.Models.Data.Integrations;
+
+namespace Bit.Core.Services;
+
+public class SlackIntegrationHandler(
+ ISlackService slackService)
+ : IntegrationHandlerBase
+{
+ public override async Task HandleAsync(IntegrationMessage message)
+ {
+ await slackService.SendSlackMessageByChannelIdAsync(
+ message.Configuration.token,
+ message.RenderedTemplate,
+ message.Configuration.channelId
+ );
+
+ return new IntegrationHandlerResult(success: true, message: message);
+ }
+}
diff --git a/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs b/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs
index ec6924bb3e..97453497bc 100644
--- a/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs
+++ b/src/Core/AdminConsole/Services/Implementations/WebhookEventHandler.cs
@@ -1,8 +1,8 @@
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
+using Bit.Core.AdminConsole.Models.Data.Integrations;
using Bit.Core.Enums;
-using Bit.Core.Models.Data.Integrations;
using Bit.Core.Repositories;
#nullable enable
@@ -25,7 +25,7 @@ public class WebhookEventHandler(
protected override async Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration,
string renderedTemplate)
{
- var config = mergedConfiguration.Deserialize();
+ var config = mergedConfiguration.Deserialize();
if (config is null || string.IsNullOrEmpty(config.url))
{
return;
diff --git a/src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs b/src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs
new file mode 100644
index 0000000000..5f9898afe8
--- /dev/null
+++ b/src/Core/AdminConsole/Services/Implementations/WebhookIntegrationHandler.cs
@@ -0,0 +1,61 @@
+using System.Globalization;
+using System.Net;
+using System.Text;
+using Bit.Core.AdminConsole.Models.Data.Integrations;
+
+#nullable enable
+
+namespace Bit.Core.Services;
+
+public class WebhookIntegrationHandler(IHttpClientFactory httpClientFactory)
+ : IntegrationHandlerBase
+{
+ private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName);
+
+ public const string HttpClientName = "WebhookIntegrationHandlerHttpClient";
+
+ public override async Task HandleAsync(IntegrationMessage message)
+ {
+ var content = new StringContent(message.RenderedTemplate, Encoding.UTF8, "application/json");
+ var response = await _httpClient.PostAsync(message.Configuration.url, content);
+ var result = new IntegrationHandlerResult(success: response.IsSuccessStatusCode, message);
+
+ switch (response.StatusCode)
+ {
+ case HttpStatusCode.TooManyRequests:
+ case HttpStatusCode.RequestTimeout:
+ case HttpStatusCode.InternalServerError:
+ case HttpStatusCode.BadGateway:
+ case HttpStatusCode.ServiceUnavailable:
+ case HttpStatusCode.GatewayTimeout:
+ result.Retryable = true;
+ result.FailureReason = response.ReasonPhrase;
+
+ if (response.Headers.TryGetValues("Retry-After", out var values))
+ {
+ var value = values.FirstOrDefault();
+ if (int.TryParse(value, out var seconds))
+ {
+ // Retry-after was specified in seconds. Adjust DelayUntilDate by the requested number of seconds.
+ result.DelayUntilDate = DateTime.UtcNow.AddSeconds(seconds);
+ }
+ else if (DateTimeOffset.TryParseExact(value,
+ "r", // "r" is the round-trip format: RFC1123
+ CultureInfo.InvariantCulture,
+ DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
+ out var retryDate))
+ {
+ // Retry-after was specified as a date. Adjust DelayUntilDate to the specified date.
+ result.DelayUntilDate = retryDate.UtcDateTime;
+ }
+ }
+ break;
+ default:
+ result.Retryable = false;
+ result.FailureReason = response.ReasonPhrase;
+ break;
+ }
+
+ return result;
+ }
+}
diff --git a/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs b/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs
index 4fb5c15e63..aab4e448e5 100644
--- a/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs
+++ b/src/Core/AdminConsole/Utilities/IntegrationTemplateProcessor.cs
@@ -1,4 +1,6 @@
-using System.Text.RegularExpressions;
+#nullable enable
+
+using System.Text.RegularExpressions;
namespace Bit.Core.AdminConsole.Utilities;
@@ -9,7 +11,7 @@ public static partial class IntegrationTemplateProcessor
public static string ReplaceTokens(string template, object values)
{
- if (string.IsNullOrEmpty(template) || values == null)
+ if (string.IsNullOrEmpty(template))
{
return template;
}
diff --git a/src/Core/Auth/Entities/SsoUser.cs b/src/Core/Auth/Entities/SsoUser.cs
index 3199f00221..2e457afbc6 100644
--- a/src/Core/Auth/Entities/SsoUser.cs
+++ b/src/Core/Auth/Entities/SsoUser.cs
@@ -8,7 +8,7 @@ public class SsoUser : ITableObject
public long Id { get; set; }
public Guid UserId { get; set; }
public Guid? OrganizationId { get; set; }
- [MaxLength(50)]
+ [MaxLength(300)]
public string ExternalId { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
diff --git a/src/Core/Auth/Enums/EmergencyAccessType.cs b/src/Core/Auth/Enums/EmergencyAccessType.cs
index a3497cc287..6e4e6e7f56 100644
--- a/src/Core/Auth/Enums/EmergencyAccessType.cs
+++ b/src/Core/Auth/Enums/EmergencyAccessType.cs
@@ -2,6 +2,12 @@
public enum EmergencyAccessType : byte
{
+ ///
+ /// Allows emergency contact to view the Grantor's data.
+ ///
View = 0,
+ ///
+ /// Allows emergency contact to take over the Grantor's account by overwriting the Grantor's password.
+ ///
Takeover = 1,
}
diff --git a/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs b/src/Core/Auth/Services/EmergencyAccess/EmergencyAccessService.cs
similarity index 82%
rename from src/Core/Auth/Services/Implementations/EmergencyAccessService.cs
rename to src/Core/Auth/Services/EmergencyAccess/EmergencyAccessService.cs
index 2418830ea7..6a8fe9dd17 100644
--- a/src/Core/Auth/Services/Implementations/EmergencyAccessService.cs
+++ b/src/Core/Auth/Services/EmergencyAccess/EmergencyAccessService.cs
@@ -58,38 +58,38 @@ public class EmergencyAccessService : IEmergencyAccessService
_removeOrganizationUserCommand = removeOrganizationUserCommand;
}
- public async Task InviteAsync(User invitingUser, string email, EmergencyAccessType type, int waitTime)
+ public async Task InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime)
{
- if (!await _userService.CanAccessPremium(invitingUser))
+ if (!await _userService.CanAccessPremium(grantorUser))
{
throw new BadRequestException("Not a premium user.");
}
- if (type == EmergencyAccessType.Takeover && invitingUser.UsesKeyConnector)
+ if (accessType == EmergencyAccessType.Takeover && grantorUser.UsesKeyConnector)
{
throw new BadRequestException("You cannot use Emergency Access Takeover because you are using Key Connector.");
}
var emergencyAccess = new EmergencyAccess
{
- GrantorId = invitingUser.Id,
- Email = email.ToLowerInvariant(),
+ GrantorId = grantorUser.Id,
+ Email = emergencyContactEmail.ToLowerInvariant(),
Status = EmergencyAccessStatusType.Invited,
- Type = type,
+ Type = accessType,
WaitTimeDays = waitTime,
CreationDate = DateTime.UtcNow,
RevisionDate = DateTime.UtcNow,
};
await _emergencyAccessRepository.CreateAsync(emergencyAccess);
- await SendInviteAsync(emergencyAccess, NameOrEmail(invitingUser));
+ await SendInviteAsync(emergencyAccess, NameOrEmail(grantorUser));
return emergencyAccess;
}
- public async Task GetAsync(Guid emergencyAccessId, Guid userId)
+ public async Task GetAsync(Guid emergencyAccessId, Guid grantorId)
{
- var emergencyAccess = await _emergencyAccessRepository.GetDetailsByIdGrantorIdAsync(emergencyAccessId, userId);
+ var emergencyAccess = await _emergencyAccessRepository.GetDetailsByIdGrantorIdAsync(emergencyAccessId, grantorId);
if (emergencyAccess == null)
{
throw new BadRequestException("Emergency Access not valid.");
@@ -98,19 +98,19 @@ public class EmergencyAccessService : IEmergencyAccessService
return emergencyAccess;
}
- public async Task ResendInviteAsync(User invitingUser, Guid emergencyAccessId)
+ public async Task ResendInviteAsync(User grantorUser, Guid emergencyAccessId)
{
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
- if (emergencyAccess == null || emergencyAccess.GrantorId != invitingUser.Id ||
+ if (emergencyAccess == null || emergencyAccess.GrantorId != grantorUser.Id ||
emergencyAccess.Status != EmergencyAccessStatusType.Invited)
{
throw new BadRequestException("Emergency Access not valid.");
}
- await SendInviteAsync(emergencyAccess, NameOrEmail(invitingUser));
+ await SendInviteAsync(emergencyAccess, NameOrEmail(grantorUser));
}
- public async Task AcceptUserAsync(Guid emergencyAccessId, User user, string token, IUserService userService)
+ public async Task AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService)
{
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
if (emergencyAccess == null)
@@ -123,7 +123,7 @@ public class EmergencyAccessService : IEmergencyAccessService
throw new BadRequestException("Invalid token.");
}
- if (!data.IsValid(emergencyAccessId, user.Email))
+ if (!data.IsValid(emergencyAccessId, granteeUser.Email))
{
throw new BadRequestException("Invalid token.");
}
@@ -140,7 +140,7 @@ public class EmergencyAccessService : IEmergencyAccessService
// TODO PM-21687
// Might not be reachable since the Tokenable.IsValid() does an email comparison
if (string.IsNullOrWhiteSpace(emergencyAccess.Email) ||
- !emergencyAccess.Email.Equals(user.Email, StringComparison.InvariantCultureIgnoreCase))
+ !emergencyAccess.Email.Equals(granteeUser.Email, StringComparison.InvariantCultureIgnoreCase))
{
throw new BadRequestException("User email does not match invite.");
}
@@ -148,7 +148,7 @@ public class EmergencyAccessService : IEmergencyAccessService
var granteeEmail = emergencyAccess.Email;
emergencyAccess.Status = EmergencyAccessStatusType.Accepted;
- emergencyAccess.GranteeId = user.Id;
+ emergencyAccess.GranteeId = granteeUser.Id;
emergencyAccess.Email = null;
var grantor = await userService.GetUserByIdAsync(emergencyAccess.GrantorId);
@@ -172,16 +172,16 @@ public class EmergencyAccessService : IEmergencyAccessService
await _emergencyAccessRepository.DeleteAsync(emergencyAccess);
}
- public async Task ConfirmUserAsync(Guid emergencyAccessId, string key, Guid confirmingUserId)
+ public async Task ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId)
{
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
if (emergencyAccess == null || emergencyAccess.Status != EmergencyAccessStatusType.Accepted ||
- emergencyAccess.GrantorId != confirmingUserId)
+ emergencyAccess.GrantorId != grantorId)
{
throw new BadRequestException("Emergency Access not valid.");
}
- var grantor = await _userRepository.GetByIdAsync(confirmingUserId);
+ var grantor = await _userRepository.GetByIdAsync(grantorId);
if (emergencyAccess.Type == EmergencyAccessType.Takeover && grantor.UsesKeyConnector)
{
throw new BadRequestException("You cannot use Emergency Access Takeover because you are using Key Connector.");
@@ -198,14 +198,14 @@ public class EmergencyAccessService : IEmergencyAccessService
return emergencyAccess;
}
- public async Task SaveAsync(EmergencyAccess emergencyAccess, User savingUser)
+ public async Task SaveAsync(EmergencyAccess emergencyAccess, User grantorUser)
{
- if (!await _userService.CanAccessPremium(savingUser))
+ if (!await _userService.CanAccessPremium(grantorUser))
{
throw new BadRequestException("Not a premium user.");
}
- if (emergencyAccess.GrantorId != savingUser.Id)
+ if (emergencyAccess.GrantorId != grantorUser.Id)
{
throw new BadRequestException("Emergency Access not valid.");
}
@@ -222,10 +222,11 @@ public class EmergencyAccessService : IEmergencyAccessService
await _emergencyAccessRepository.ReplaceAsync(emergencyAccess);
}
- public async Task InitiateAsync(Guid id, User initiatingUser)
+ // TODO PM-21687: rename this to something like InitiateRecoveryAsync, and something similar for Approve and Reject
+ public async Task InitiateAsync(Guid emergencyAccessId, User granteeUser)
{
- var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
- if (emergencyAccess == null || emergencyAccess.GranteeId != initiatingUser.Id ||
+ var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
+ if (emergencyAccess == null || emergencyAccess.GranteeId != granteeUser.Id ||
emergencyAccess.Status != EmergencyAccessStatusType.Confirmed)
{
throw new BadRequestException("Emergency Access not valid.");
@@ -245,14 +246,14 @@ public class EmergencyAccessService : IEmergencyAccessService
emergencyAccess.LastNotificationDate = now;
await _emergencyAccessRepository.ReplaceAsync(emergencyAccess);
- await _mailService.SendEmergencyAccessRecoveryInitiated(emergencyAccess, NameOrEmail(initiatingUser), grantor.Email);
+ await _mailService.SendEmergencyAccessRecoveryInitiated(emergencyAccess, NameOrEmail(granteeUser), grantor.Email);
}
- public async Task ApproveAsync(Guid id, User approvingUser)
+ public async Task ApproveAsync(Guid emergencyAccessId, User grantorUser)
{
- var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
+ var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
- if (emergencyAccess == null || emergencyAccess.GrantorId != approvingUser.Id ||
+ if (emergencyAccess == null || emergencyAccess.GrantorId != grantorUser.Id ||
emergencyAccess.Status != EmergencyAccessStatusType.RecoveryInitiated)
{
throw new BadRequestException("Emergency Access not valid.");
@@ -262,14 +263,14 @@ public class EmergencyAccessService : IEmergencyAccessService
await _emergencyAccessRepository.ReplaceAsync(emergencyAccess);
var grantee = await _userRepository.GetByIdAsync(emergencyAccess.GranteeId.Value);
- await _mailService.SendEmergencyAccessRecoveryApproved(emergencyAccess, NameOrEmail(approvingUser), grantee.Email);
+ await _mailService.SendEmergencyAccessRecoveryApproved(emergencyAccess, NameOrEmail(grantorUser), grantee.Email);
}
- public async Task RejectAsync(Guid id, User rejectingUser)
+ public async Task RejectAsync(Guid emergencyAccessId, User grantorUser)
{
- var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
+ var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
- if (emergencyAccess == null || emergencyAccess.GrantorId != rejectingUser.Id ||
+ if (emergencyAccess == null || emergencyAccess.GrantorId != grantorUser.Id ||
(emergencyAccess.Status != EmergencyAccessStatusType.RecoveryInitiated &&
emergencyAccess.Status != EmergencyAccessStatusType.RecoveryApproved))
{
@@ -280,17 +281,17 @@ public class EmergencyAccessService : IEmergencyAccessService
await _emergencyAccessRepository.ReplaceAsync(emergencyAccess);
var grantee = await _userRepository.GetByIdAsync(emergencyAccess.GranteeId.Value);
- await _mailService.SendEmergencyAccessRecoveryRejected(emergencyAccess, NameOrEmail(rejectingUser), grantee.Email);
+ await _mailService.SendEmergencyAccessRecoveryRejected(emergencyAccess, NameOrEmail(grantorUser), grantee.Email);
}
- public async Task> GetPoliciesAsync(Guid id, User requestingUser)
+ public async Task> GetPoliciesAsync(Guid emergencyAccessId, User granteeUser)
{
// TODO PM-21687
// Should we look up policies here or just verify the EmergencyAccess is correct
// and handle policy logic else where? Should this be a query/Command?
- var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
+ var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
- if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.Takeover))
+ if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.Takeover))
{
throw new BadRequestException("Emergency Access not valid.");
}
@@ -306,11 +307,12 @@ public class EmergencyAccessService : IEmergencyAccessService
return policies;
}
- public async Task<(EmergencyAccess, User)> TakeoverAsync(Guid id, User requestingUser)
+ // TODO PM-21687: rename this to something like InitiateRecoveryTakeoverAsync
+ public async Task<(EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser)
{
- var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
+ var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
- if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.Takeover))
+ if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.Takeover))
{
throw new BadRequestException("Emergency Access not valid.");
}
@@ -326,11 +328,12 @@ public class EmergencyAccessService : IEmergencyAccessService
return (emergencyAccess, grantor);
}
- public async Task PasswordAsync(Guid id, User requestingUser, string newMasterPasswordHash, string key)
+ // TODO PM-21687: rename this to something like FinishRecoveryTakeoverAsync
+ public async Task PasswordAsync(Guid emergencyAccessId, User granteeUser, string newMasterPasswordHash, string key)
{
- var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
+ var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
- if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.Takeover))
+ if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.Takeover))
{
throw new BadRequestException("Emergency Access not valid.");
}
@@ -392,11 +395,11 @@ public class EmergencyAccessService : IEmergencyAccessService
}
}
- public async Task ViewAsync(Guid id, User requestingUser)
+ public async Task ViewAsync(Guid emergencyAccessId, User granteeUser)
{
- var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
+ var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
- if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.View))
+ if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.View))
{
throw new BadRequestException("Emergency Access not valid.");
}
@@ -410,11 +413,11 @@ public class EmergencyAccessService : IEmergencyAccessService
};
}
- public async Task GetAttachmentDownloadAsync(Guid id, Guid cipherId, string attachmentId, User requestingUser)
+ public async Task GetAttachmentDownloadAsync(Guid emergencyAccessId, Guid cipherId, string attachmentId, User granteeUser)
{
- var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
+ var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(emergencyAccessId);
- if (!IsValidRequest(emergencyAccess, requestingUser, EmergencyAccessType.View))
+ if (!IsValidRequest(emergencyAccess, granteeUser, EmergencyAccessType.View))
{
throw new BadRequestException("Emergency Access not valid.");
}
@@ -429,18 +432,19 @@ public class EmergencyAccessService : IEmergencyAccessService
await _mailService.SendEmergencyAccessInviteEmailAsync(emergencyAccess, invitingUsersName, token);
}
+ // TODO PM-21687: move this to the user entity -> User.GetNameOrEmail()?
private static string NameOrEmail(User user)
{
return string.IsNullOrWhiteSpace(user.Name) ? user.Email : user.Name;
}
-
/*
* Checks if EmergencyAccess Object is null
* Checks the requesting user is the same as the granteeUser (So we are checking for proper grantee action)
* Status _must_ equal RecoveryApproved (This means the grantor has invited, the grantee has accepted, and the grantor has approved so the shared key exists but hasn't been exercised yet)
* request type must equal the type of access requested (View or Takeover)
*/
+ //TODO PM-21687: this IsValidRequest() checks the validity based on the granteeUser. There should be a complementary method for the grantorUser
private static bool IsValidRequest(
EmergencyAccess availableAccess,
User requestingUser,
diff --git a/src/Core/Auth/Services/EmergencyAccess/IEmergencyAccessService.cs b/src/Core/Auth/Services/EmergencyAccess/IEmergencyAccessService.cs
new file mode 100644
index 0000000000..de695bbd7d
--- /dev/null
+++ b/src/Core/Auth/Services/EmergencyAccess/IEmergencyAccessService.cs
@@ -0,0 +1,147 @@
+using Bit.Core.AdminConsole.Entities;
+using Bit.Core.Auth.Entities;
+using Bit.Core.Auth.Enums;
+using Bit.Core.Auth.Models.Data;
+using Bit.Core.Entities;
+using Bit.Core.Enums;
+using Bit.Core.Services;
+using Bit.Core.Vault.Models.Data;
+
+namespace Bit.Core.Auth.Services;
+
+public interface IEmergencyAccessService
+{
+ ///
+ /// Invites a user via email to become an emergency contact for the Grantor user. The Grantor must have a premium subscription.
+ /// the grantor user must not be a member of the organization that uses KeyConnector.
+ ///
+ /// The user initiating the emergency contact request
+ /// Emergency contact
+ /// Type of emergency access allowed to the emergency contact
+ /// The amount of time to pass before the invite is auto confirmed
+ /// a new Emergency Access object
+ Task InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime);
+ ///
+ /// Sends an invite to the emergency contact associated with the emergency access id.
+ ///
+ /// The grantor. This must be the owner of the Emergency Access object
+ /// The Id of the emergency access being requested.
+ /// void
+ Task ResendInviteAsync(User grantorUser, Guid emergencyAccessId);
+ ///
+ /// A grantee user accepts the emergency contact request. This updates the emergency access status to be
+ /// "Accepted", this is the middle step before the grantor user confirms the request.
+ ///
+ /// Id of the emergency access object being acted on.
+ /// User being invited to be an emergency contact
+ /// the tokenable that was sent via email
+ /// service dependency
+ /// void
+ Task AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService);
+ ///
+ /// The creator of the emergency access request can delete the request.
+ ///
+ /// Id of the emergency access being acted on
+ /// Id of the owner trying to delete the emergency access request
+ /// void
+ Task DeleteAsync(Guid emergencyAccessId, Guid grantorId);
+ ///
+ /// The grantor user confirms the acceptance of the emergency contact request. This stores the encrypted key allowing the grantee
+ /// access based on the emergency access type.
+ ///
+ /// Id of the emergency access being acted on.
+ /// The grantor user key encrypted by the grantee public key; grantee.PubicKey(grantor.User.Key)
+ /// Id of grantor user
+ /// emergency access object associated with the Id passed in
+ Task ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId);
+ ///
+ /// Fetches an emergency access object. The grantor user must own the object being fetched.
+ ///
+ /// Id of emergency access object
+ /// Id of the owner of the emergency access object.
+ /// Details of the emergency access object
+ Task GetAsync(Guid emergencyAccessId, Guid grantorId);
+ ///
+ /// Updates the emergency access object.
+ ///
+ /// emergency access entity being updated
+ /// grantor user
+ /// void
+ Task SaveAsync(EmergencyAccess emergencyAccess, User grantorUser);
+ ///
+ /// Initiates the recovery process. For either Takeover or view. Will send an email to the Grantor User notifying of the initiation.
+ ///
+ /// EmergencyAccess Id
+ /// grantee user
+ /// void
+ Task InitiateAsync(Guid emergencyAccessId, User granteeUser);
+ ///
+ /// Approves a recovery request. Sets the EmergencyAccess.Status to RecoveryApproved.
+ ///
+ /// emergency access id
+ /// grantor user
+ /// void
+ Task ApproveAsync(Guid emergencyAccessId, User grantorUser);
+ ///
+ /// Rejects a recovery request. Sets the EmergencyAccess.Status to Confirmed. This does not remove the emergency access entity. The
+ /// Grantee user can still initiate another recovery request.
+ ///
+ /// emergency access id
+ /// grantor user
+ /// void
+ Task RejectAsync(Guid emergencyAccessId, User grantorUser);
+ ///
+ /// This request is made by the Grantee user to fetch the policies for the Grantor User.
+ /// The Grantor User has to be the owner of the organization.
+ /// If the Grantor user has OrganizationUserType.Owner then the policies for the _Grantor_ user
+ /// are returned. This is used to ensure the password is of the proper complexity for the organization.
+ ///
+ /// EmergencyAccess.Id being acted on
+ /// User making the request, this is the Grantee
+ /// null if the GrantorUser is not an organization owner; A list of policies otherwise.
+ Task> GetPoliciesAsync(Guid emergencyAccessId, User granteeUser);
+ ///
+ /// Fetches the emergency access entity and grantor user. The grantor user is returned so the correct KDF configuration is
+ /// used for the new password.
+ ///
+ /// Id of entity being accessed
+ /// grantee user of the emergency access entity
+ /// emergency access entity and the grantorUser
+ Task<(EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser);
+ ///
+ /// Updates the grantor's password hash and updates the key for the EmergencyAccess entity.
+ ///
+ /// Emergency Access Id being acted on
+ /// user making the request
+ /// new password hash set by grantee user
+ /// new encrypted user key
+ /// void
+ Task PasswordAsync(Guid emergencyAccessId, User granteeUser, string newMasterPasswordHash, string key);
+ ///
+ /// sends a reminder email that there is a pending request for recovery.
+ ///
+ /// void
+ Task SendNotificationsAsync();
+ ///
+ /// This handles the auto approval of recovery requests started in the method.
+ /// An email will be sent to the Grantee and the Grantor notifying each the recovery has been approved.
+ ///
+ /// void
+ Task HandleTimedOutRequestsAsync();
+ ///
+ /// Fetched ciphers from the grantors account for the grantee to view.
+ ///
+ /// Emergency access entity being acted on
+ /// user requesting cipher items
+ /// ciphers associated with the emergency access request
+ Task ViewAsync(Guid emergencyAccessId, User granteeUser);
+ ///
+ /// Returns attachment if the grantee user has access to the cipher through the emergency access entity.
+ ///
+ /// EmergencyAccess entity being acted on
+ /// cipher entity containing the attachment
+ /// Attachment entity
+ /// user making the request
+ /// attachment response
+ Task GetAttachmentDownloadAsync(Guid emergencyAccessId, Guid cipherId, string attachmentId, User granteeUser);
+}
diff --git a/src/Core/Auth/Services/EmergencyAccess/readme.md b/src/Core/Auth/Services/EmergencyAccess/readme.md
new file mode 100644
index 0000000000..e2bdec3916
--- /dev/null
+++ b/src/Core/Auth/Services/EmergencyAccess/readme.md
@@ -0,0 +1,95 @@
+# Emergency Access System
+This system allows users to share their `User.Key` with other users using public key exchange. An emergency contact (a grantee user) can view or takeover (reset the password) of the grantor user.
+
+When an account is taken over all two factor methods are turned off and device verification is disabled.
+
+This system is affected by the Key Rotation feature. The `EmergencyAccess.KeyEncrypted` is the `Grantor.UserKey` encrypted by the `Grantee.PublicKey`. So if the `User.Key` is rotated then all `EmergencyAccess` entities will need to be updated.
+
+## Special Cases
+Users who use `KeyConnector` are not able to allow `Takeover` of their accounts. However, they can allow `View`.
+
+When a grantee user _takes over_ a grantor user's account, the grantor user will be removed from all organizations where the grantor user is not the `OrganizationUserType.Owner`. A grantor user will not be removed from organizations if the `EmergencyAccessType` is `View`. The grantee user will only be able to `View` the grantor user's ciphers, and not any of the organization ciphers, if any exist.
+
+## Step 1. Invitation
+
+A grantor user invites another user to be their emergency contact, the grantee. This will create a new `EmergencyAccess` entity in the database with the `EmergencyAccessStatusType` set to `Invited`.
+The `EmergencyAccess.KeyEncrypted` field is empty, and the `GranteeId` is `null` since the user being invited via email may not have an account yet.
+
+### code
+```csharp
+// creates entity.
+Task InviteAsync(User grantorUser, string emergencyContactEmail, EmergencyAccessType accessType, int waitTime);
+// resend email to the EmergencyAccess.Email.
+Task ResendInviteAsync(User grantorUser, Guid emergencyAccessId);
+```
+
+## Step 2. Acceptance
+
+The grantee user receives an email they have been invited to be an emergency contact for a grantor user.
+
+At this point the grantee user can accept the request. This will set the `EmergencyAccess.GranteeId` to the `User.Id` of the grantee user. The `EmergencyAccess.Status` is set to `Accepted`.
+
+If the grantee user does not have an account then they can create an account and accept the invitation.
+
+### Code
+```csharp
+// accepts the request to be an emergency contact.
+Task AcceptUserAsync(Guid emergencyAccessId, User granteeUser, string token, IUserService userService);
+```
+
+## Step 3. Confirmation
+
+Once the grantee user has accepted, the `EmergencyAccess.GranteeId` allows the grantor user the ability to query for the `GranteeUser.PublicKey`. With the `Grantee.PublicKey`, the grantor on the client is able to safely encrypt their `User.Key` and save the encrypted string to the database.
+
+The `EmergencyAccess.Status` is set to `Confirmed`, and the `EmergencyAccess.KeyEncrypted` is set.
+
+### Code
+```csharp
+Task ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId);
+```
+
+## Step 4. Recovery Approval
+
+The grantee user can now exercise the ability to view or takeover the account. This is done by initiating the recovery. Initiating recovery has a time delay specified by `EmergencyAccess.WaitTime`. `WaitTime` is set in the initial invite. The grantor user can approve the request before the `WaitTime`, but they _cannot_ reject the request _after_ the `WaitTime` has completed. If the recovery request is not rejected then once the `WaitTime` has passed the grantee user will be able to access the emergency access entity.
+
+### Code
+```csharp
+// Initiates the recovery process; Will set EmergencyAccess.Status to RecoveryInitiated.
+Task InitiateAsync(Guid id, User granteeUser);
+// Approved the recovery request; Will set EmergencyAccess.Status to RecoveryApproved.
+Task ApproveAsync(Guid id, User approvingUser);
+// Rejects the recovery request; Will set EmergencyAccess.Status to Confirmed.
+Task RejectAsync(Guid id, User rejectingUser);
+// Automatically set the EmergencyAccess.Status to RecoveryApproved after WaitTime has passed.
+Task HandleTimedOutRequestsAsync();
+```
+
+## Step 5. Recovering the account
+
+Once the `EmergencyAccess.Status` is `RecoveryApproved` the grantee user is able to exercise their ability to view or takeover the grantor account. Viewing allows the grantee user to view the vault data of the grantor user. Takeover allows the grantee to change the password of the grantor user.
+
+### Takeover
+`TakeoverAsync(Guid, User)` returns the grantor user object along with the `EmergencyAccess` entity. The grantor user object is required since to update the password the client needs access to the grantor kdf configuration. Once the password has been set in the `PasswordAsync(Guid, User, string, string)` the account has been successfully recovered.
+
+Taking over the account will change the password of the grantor user, empty the two factor array on the grantor user, and disable device verification.
+
+```csharp
+// Takeover returns the grantor user and the emergency access entity.
+Task<(EmergencyAccess, User)> TakeoverAsync(Guid emergencyAccessId, User granteeUser);
+// Password sets the password for the grantor user.
+Task PasswordAsync(Guid emergencyAccessId, User granteeUser, string newMasterPasswordHash, string key);
+// Returns Ciphers the Grantee is allowed to view based on the EmergencyAccess status.
+Task ViewAsync(Guid emergencyAccessId, User granteeUser);
+// Returns downloadable cipher attachments based on the EmergencyAccess status.
+Task GetAttachmentDownloadAsync(Guid emergencyAccessId, Guid cipherId, string attachmentId, User granteeUser);
+```
+
+## Optional steps
+
+The grantor user is able to delete an emergency contact at anytime, at any point in the recovery process.
+
+### Code
+```csharp
+// deletes the associated EmergencyAccess Entity
+Task DeleteAsync(Guid emergencyAccessId, Guid grantorId);
+```
diff --git a/src/Core/Auth/Services/IEmergencyAccessService.cs b/src/Core/Auth/Services/IEmergencyAccessService.cs
deleted file mode 100644
index 6dd17151e6..0000000000
--- a/src/Core/Auth/Services/IEmergencyAccessService.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-using Bit.Core.AdminConsole.Entities;
-using Bit.Core.Auth.Entities;
-using Bit.Core.Auth.Enums;
-using Bit.Core.Auth.Models.Data;
-using Bit.Core.Entities;
-using Bit.Core.Enums;
-using Bit.Core.Services;
-using Bit.Core.Vault.Models.Data;
-
-namespace Bit.Core.Auth.Services;
-
-public interface IEmergencyAccessService
-{
- Task InviteAsync(User invitingUser, string email, EmergencyAccessType type, int waitTime);
- Task ResendInviteAsync(User invitingUser, Guid emergencyAccessId);
- Task AcceptUserAsync(Guid emergencyAccessId, User user, string token, IUserService userService);
- Task DeleteAsync(Guid emergencyAccessId, Guid grantorId);
- Task ConfirmUserAsync(Guid emergencyAccessId, string key, Guid grantorId);
- Task GetAsync(Guid emergencyAccessId, Guid userId);
- Task SaveAsync(EmergencyAccess emergencyAccess, User savingUser);
- Task InitiateAsync(Guid id, User initiatingUser);
- Task ApproveAsync(Guid id, User approvingUser);
- Task RejectAsync(Guid id, User rejectingUser);
- ///
- /// This request is made by the Grantee user to fetch the policies for the Grantor User.
- /// The Grantor User has to be the owner of the organization.
- /// If the Grantor user has OrganizationUserType.Owner then the policies for the _Grantor_ user
- /// are returned.
- ///
- /// EmergencyAccess.Id being acted on
- /// User making the request, this is the Grantee
- /// null if the GrantorUser is not an organization owner; A list of policies otherwise.
- Task> GetPoliciesAsync(Guid id, User requestingUser);
- Task<(EmergencyAccess, User)> TakeoverAsync(Guid id, User initiatingUser);
- Task PasswordAsync(Guid id, User user, string newMasterPasswordHash, string key);
- Task SendNotificationsAsync();
- Task HandleTimedOutRequestsAsync();
- Task ViewAsync(Guid id, User user);
- Task GetAttachmentDownloadAsync(Guid id, Guid cipherId, string attachmentId, User user);
-}
diff --git a/src/Core/Constants.cs b/src/Core/Constants.cs
index 1c31ffaab4..7a2b3c9ac7 100644
--- a/src/Core/Constants.cs
+++ b/src/Core/Constants.cs
@@ -202,6 +202,7 @@ public static class FeatureFlagKeys
public const string EndUserNotifications = "pm-10609-end-user-notifications";
public const string SeparateCustomRolePermissions = "pm-19917-separate-custom-role-permissions";
public const string PhishingDetection = "phishing-detection";
+ public const string RemoveCardItemTypePolicy = "pm-16442-remove-card-item-type-policy";
public static List GetAllKeys()
{
diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj
index 633c3452d9..88ecaf8cef 100644
--- a/src/Core/Core.csproj
+++ b/src/Core/Core.csproj
@@ -34,9 +34,9 @@
-
+
-
+
diff --git a/src/Core/Entities/User.cs b/src/Core/Entities/User.cs
index b3a6a9592e..08981ca2d3 100644
--- a/src/Core/Entities/User.cs
+++ b/src/Core/Entities/User.cs
@@ -36,6 +36,11 @@ public class User : ITableObject, IStorableSubscriber, IRevisable, ITwoFac
public string? TwoFactorRecoveryCode { get; set; }
public string? EquivalentDomains { get; set; }
public string? ExcludedGlobalEquivalentDomains { get; set; }
+ ///
+ /// The Account Revision Date is used to check if new sync needs to occur. It should be updated
+ /// whenever a change is made that affects a client's sync data; for example, updating their vault or
+ /// organization membership.
+ ///
public DateTime AccountRevisionDate { get; set; } = DateTime.UtcNow;
public string? Key { get; set; }
public string? PublicKey { get; set; }
diff --git a/src/Core/Settings/GlobalSettings.cs b/src/Core/Settings/GlobalSettings.cs
index 9e1241620c..4050d1c988 100644
--- a/src/Core/Settings/GlobalSettings.cs
+++ b/src/Core/Settings/GlobalSettings.cs
@@ -312,11 +312,19 @@ public class GlobalSettings : IGlobalSettings
private string _hostName;
private string _username;
private string _password;
- private string _exchangeName;
+ private string _eventExchangeName;
+ private string _integrationExchangeName;
+ public int MaxRetries { get; set; } = 3;
+ public int RetryTiming { get; set; } = 30000; // 30s
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 virtual string IntegrationDeadLetterQueueName { get; set; } = "integration-dead-letter-queue";
+ public virtual string SlackEventsQueueName { get; set; } = "events-slack-queue";
+ public virtual string SlackIntegrationQueueName { get; set; } = "integration-slack-queue";
+ public virtual string SlackIntegrationRetryQueueName { get; set; } = "integration-slack-retry-queue";
+ public virtual string WebhookEventsQueueName { get; set; } = "events-webhook-queue";
+ public virtual string WebhookIntegrationQueueName { get; set; } = "integration-webhook-queue";
+ public virtual string WebhookIntegrationRetryQueueName { get; set; } = "integration-webhook-retry-queue";
public string HostName
{
@@ -333,10 +341,15 @@ public class GlobalSettings : IGlobalSettings
get => _password;
set => _password = value.Trim('"');
}
- public string ExchangeName
+ public string EventExchangeName
{
- get => _exchangeName;
- set => _exchangeName = value.Trim('"');
+ get => _eventExchangeName;
+ set => _eventExchangeName = value.Trim('"');
+ }
+ public string IntegrationExchangeName
+ {
+ get => _integrationExchangeName;
+ set => _integrationExchangeName = value.Trim('"');
}
}
}
diff --git a/src/Core/Tools/Entities/Send.cs b/src/Core/Tools/Entities/Send.cs
index 93fa7a37c0..34b68fd6c5 100644
--- a/src/Core/Tools/Entities/Send.cs
+++ b/src/Core/Tools/Entities/Send.cs
@@ -60,9 +60,21 @@ public class Send : ITableObject
///
/// Password provided by the user. Protected with pbkdf2.
///
+ ///
+ /// This field is mutually exclusive with
+ ///
[MaxLength(300)]
public string? Password { get; set; }
+ ///
+ /// Comma-separated list of emails for OTP authentication.
+ ///
+ ///
+ /// This field is mutually exclusive with
+ ///
+ [MaxLength(1024)]
+ public string? Emails { get; set; }
+
///
/// The send becomes unavailable to API callers when
/// >= .
diff --git a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs
index 3c58dca183..fd7a82172c 100644
--- a/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs
+++ b/src/Core/Tools/ImportFeatures/ImportCiphersCommand.cs
@@ -115,7 +115,7 @@ public class ImportCiphersCommand : IImportCiphersCommand
}
// Create it all
- await _cipherRepository.CreateAsync(ciphers, newFolders);
+ await _cipherRepository.CreateAsync(importingUserId, ciphers, newFolders);
// push
await _pushService.PushSyncVaultAsync(importingUserId);
diff --git a/src/Core/Tools/Models/Data/SendAuthenticationTypes.cs b/src/Core/Tools/Models/Data/SendAuthenticationTypes.cs
new file mode 100644
index 0000000000..9ce477ed0c
--- /dev/null
+++ b/src/Core/Tools/Models/Data/SendAuthenticationTypes.cs
@@ -0,0 +1,50 @@
+#nullable enable
+
+namespace Bit.Core.Tools.Models.Data;
+
+///
+/// A discriminated union for send authentication.
+///
+///
+/// const method : SendAuthenticationMethod;
+/// // other variable definitions omitted
+///
+/// var token = method switch
+/// {
+/// NotAuthenticated => issueTokenFor(sendId),
+/// ResourcePassword(var expected) => tryIssueTokenFor(sendId, expected, actual),
+/// EmailOtp(_) => tryIssueTokenFor(sendId, email, actualOtp),
+/// _ => throw new Exception()
+/// };
+///
+public abstract record SendAuthenticationMethod;
+
+///
+/// Never issue a send claim.
+///
+///
+/// This claim is issued when a send does not exist or when a send
+/// has exceeded its max access attempts.
+///
+public record NeverAuthenticate : SendAuthenticationMethod;
+
+///
+/// Create a send claim automatically.
+///
+public record NotAuthenticated : SendAuthenticationMethod;
+
+///
+/// Create a send claim by requesting a password confirmation hash.
+///
+///
+/// A base64 encoded hash that permits access to the send.
+///
+public record ResourcePassword(string Hash) : SendAuthenticationMethod;
+
+///
+/// Create a send claim by requesting a one time password (OTP) confirmation code.
+///
+///
+/// The list of email addresses permitted access to the send.
+///
+public record EmailOtp(string[] Emails) : SendAuthenticationMethod;
diff --git a/src/Core/Tools/SendFeatures/Queries/Interfaces/ISendAuthenticationQuery.cs b/src/Core/Tools/SendFeatures/Queries/Interfaces/ISendAuthenticationQuery.cs
new file mode 100644
index 0000000000..f7e5f7022c
--- /dev/null
+++ b/src/Core/Tools/SendFeatures/Queries/Interfaces/ISendAuthenticationQuery.cs
@@ -0,0 +1,20 @@
+using Bit.Core.Tools.Models.Data;
+
+#nullable enable
+
+namespace Bit.Core.Tools.SendFeatures.Queries.Interfaces;
+
+///
+/// Integration with authentication layer for generating send access claims.
+///
+public interface ISendAuthenticationQuery
+{
+ ///
+ /// Retrieves the authentication method of a Send.
+ ///
+ /// Identifies the send to inspect.
+ ///
+ /// The authentication method that should be performed for the send.
+ ///
+ Task GetAuthenticationMethod(Guid sendId);
+}
diff --git a/src/Core/Tools/SendFeatures/Queries/SendAuthenticationQuery.cs b/src/Core/Tools/SendFeatures/Queries/SendAuthenticationQuery.cs
new file mode 100644
index 0000000000..fed7c9e8d4
--- /dev/null
+++ b/src/Core/Tools/SendFeatures/Queries/SendAuthenticationQuery.cs
@@ -0,0 +1,53 @@
+using Bit.Core.Tools.Models.Data;
+using Bit.Core.Tools.Repositories;
+using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
+
+#nullable enable
+
+namespace Bit.Core.Tools.SendFeatures.Queries;
+
+///
+public class SendAuthenticationQuery : ISendAuthenticationQuery
+{
+ private static readonly NotAuthenticated NOT_AUTHENTICATED = new NotAuthenticated();
+ private static readonly NeverAuthenticate NEVER_AUTHENTICATE = new NeverAuthenticate();
+
+ private readonly ISendRepository _sendRepository;
+
+ ///
+ /// Instantiates the command
+ ///
+ ///
+ /// Retrieves send records
+ ///
+ ///
+ /// Thrown when is .
+ ///
+ public SendAuthenticationQuery(ISendRepository sendRepository)
+ {
+ _sendRepository = sendRepository ?? throw new ArgumentNullException(nameof(sendRepository));
+ }
+
+ ///
+ public async Task GetAuthenticationMethod(Guid sendId)
+ {
+ var send = await _sendRepository.GetByIdAsync(sendId);
+
+ SendAuthenticationMethod method = send switch
+ {
+ null => NEVER_AUTHENTICATE,
+ var s when s.AccessCount >= s.MaxAccessCount => NEVER_AUTHENTICATE,
+ var s when s.Emails is not null => emailOtp(s.Emails),
+ var s when s.Password is not null => new ResourcePassword(s.Password),
+ _ => NOT_AUTHENTICATED
+ };
+
+ return method;
+ }
+
+ private EmailOtp emailOtp(string emails)
+ {
+ var list = emails.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
+ return new EmailOtp(list);
+ }
+}
diff --git a/src/Core/Tools/SendFeatures/SendServiceCollectionExtension.cs b/src/Core/Tools/SendFeatures/SendServiceCollectionExtension.cs
index 02327adaac..3dca1cb482 100644
--- a/src/Core/Tools/SendFeatures/SendServiceCollectionExtension.cs
+++ b/src/Core/Tools/SendFeatures/SendServiceCollectionExtension.cs
@@ -1,5 +1,7 @@
using Bit.Core.Tools.SendFeatures.Commands;
using Bit.Core.Tools.SendFeatures.Commands.Interfaces;
+using Bit.Core.Tools.SendFeatures.Queries;
+using Bit.Core.Tools.SendFeatures.Queries.Interfaces;
using Bit.Core.Tools.Services;
using Microsoft.Extensions.DependencyInjection;
@@ -14,5 +16,6 @@ public static class SendServiceCollectionExtension
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
}
}
diff --git a/src/Core/Vault/Repositories/ICipherRepository.cs b/src/Core/Vault/Repositories/ICipherRepository.cs
index f6767fada2..46742c6aa3 100644
--- a/src/Core/Vault/Repositories/ICipherRepository.cs
+++ b/src/Core/Vault/Repositories/ICipherRepository.cs
@@ -32,7 +32,10 @@ public interface ICipherRepository : IRepository
Task DeleteByUserIdAsync(Guid userId);
Task DeleteByOrganizationIdAsync(Guid organizationId);
Task UpdateCiphersAsync(Guid userId, IEnumerable ciphers);
- Task CreateAsync(IEnumerable ciphers, IEnumerable folders);
+ ///
+ /// Create ciphers and folders for the specified UserId. Must not be used to create organization owned items.
+ ///
+ Task CreateAsync(Guid userId, IEnumerable ciphers, IEnumerable folders);
Task CreateAsync(IEnumerable ciphers, IEnumerable collections,
IEnumerable collectionCiphers, IEnumerable collectionUsers);
Task SoftDeleteAsync(IEnumerable ids, Guid userId);
diff --git a/src/Events/Startup.cs b/src/Events/Startup.cs
index 366b562485..5fc12854b6 100644
--- a/src/Events/Startup.cs
+++ b/src/Events/Startup.cs
@@ -1,6 +1,4 @@
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;
@@ -63,37 +61,7 @@ public class Startup
services.AddSingleton();
}
- if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString))
- {
- services.AddKeyedSingleton("storage");
-
- if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) &&
- CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName))
- {
- services.AddKeyedSingleton("broadcast");
- }
- else
- {
- services.AddKeyedSingleton("broadcast");
- }
- }
- else
- {
- services.AddKeyedSingleton("storage");
-
- if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) &&
- CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) &&
- CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) &&
- CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName))
- {
- services.AddKeyedSingleton("broadcast");
- }
- else
- {
- services.AddKeyedSingleton("broadcast");
- }
- }
- services.AddScoped();
+ services.AddEventWriteServices(globalSettings);
services.AddScoped();
services.AddOptionality();
@@ -109,49 +77,7 @@ public class Startup
services.AddHostedService();
}
- // Optional RabbitMQ Listeners
- if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) &&
- CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) &&
- CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) &&
- CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName))
- {
- services.AddSingleton();
- services.AddKeyedSingleton("persistent");
- services.AddSingleton(provider =>
- new RabbitMqEventListenerService(
- provider.GetRequiredService(),
- provider.GetRequiredService>(),
- globalSettings,
- globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName));
-
- if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) &&
- CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) &&
- CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes))
- {
- services.AddHttpClient(SlackService.HttpClientName);
- services.AddSingleton();
- }
- else
- {
- services.AddSingleton();
- }
- services.AddSingleton();
- services.AddSingleton(provider =>
- new RabbitMqEventListenerService(
- provider.GetRequiredService(),
- provider.GetRequiredService>(),
- globalSettings,
- globalSettings.EventLogging.RabbitMq.SlackQueueName));
-
- services.AddHttpClient(WebhookEventHandler.HttpClientName);
- services.AddSingleton();
- services.AddSingleton(provider =>
- new RabbitMqEventListenerService(
- provider.GetRequiredService(),
- provider.GetRequiredService>(),
- globalSettings,
- globalSettings.EventLogging.RabbitMq.WebhookQueueName));
- }
+ services.AddRabbitMqListeners(globalSettings);
}
public void Configure(
diff --git a/src/EventsProcessor/Startup.cs b/src/EventsProcessor/Startup.cs
index e397bd326b..67676a8afc 100644
--- a/src/EventsProcessor/Startup.cs
+++ b/src/EventsProcessor/Startup.cs
@@ -1,12 +1,8 @@
using System.Globalization;
-using Bit.Core.AdminConsole.Services.NoopImplementations;
-using Bit.Core.Repositories;
-using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Bit.SharedWeb.Utilities;
using Microsoft.IdentityModel.Logging;
-using TableStorageRepos = Bit.Core.Repositories.TableStorage;
namespace Bit.EventsProcessor;
@@ -37,50 +33,7 @@ public class Startup
services.AddDatabaseRepositories(globalSettings);
// Hosted Services
-
- // Optional Azure Service Bus Listeners
- if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) &&
- CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName))
- {
- services.AddSingleton();
- services.AddSingleton();
- services.AddKeyedSingleton("persistent");
- services.AddSingleton(provider =>
- new AzureServiceBusEventListenerService(
- provider.GetRequiredService(),
- provider.GetRequiredService>(),
- globalSettings,
- globalSettings.EventLogging.AzureServiceBus.EventRepositorySubscriptionName));
-
- if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) &&
- CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) &&
- CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes))
- {
- services.AddHttpClient(SlackService.HttpClientName);
- services.AddSingleton();
- }
- else
- {
- services.AddSingleton();
- }
- services.AddSingleton();
- services.AddSingleton(provider =>
- new AzureServiceBusEventListenerService(
- provider.GetRequiredService(),
- provider.GetRequiredService>(),
- globalSettings,
- globalSettings.EventLogging.AzureServiceBus.SlackSubscriptionName));
-
- services.AddSingleton();
- services.AddHttpClient(WebhookEventHandler.HttpClientName);
-
- services.AddSingleton(provider =>
- new AzureServiceBusEventListenerService(
- provider.GetRequiredService(),
- provider.GetRequiredService>(),
- globalSettings,
- globalSettings.EventLogging.AzureServiceBus.WebhookSubscriptionName));
- }
+ services.AddAzureServiceBusListeners(globalSettings);
services.AddHostedService();
}
diff --git a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs
index 3df365330c..e0a89b1685 100644
--- a/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs
+++ b/src/Infrastructure.Dapper/Vault/Repositories/CipherRepository.cs
@@ -484,7 +484,7 @@ public class CipherRepository : Repository, ICipherRepository
}
}
- public async Task CreateAsync(IEnumerable ciphers, IEnumerable folders)
+ public async Task CreateAsync(Guid userId, IEnumerable ciphers, IEnumerable folders)
{
if (!ciphers.Any())
{
@@ -518,7 +518,7 @@ public class CipherRepository : Repository, ICipherRepository
await connection.ExecuteAsync(
$"[{Schema}].[User_BumpAccountRevisionDate]",
- new { Id = ciphers.First().UserId },
+ new { Id = userId },
commandType: CommandType.StoredProcedure, transaction: transaction);
transaction.Commit();
diff --git a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs
index 10d92357fe..5ef59d51db 100644
--- a/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs
+++ b/src/Infrastructure.EntityFramework/AdminConsole/Repositories/OrganizationUserRepository.cs
@@ -1,4 +1,5 @@
-using AutoMapper;
+using System.Diagnostics;
+using AutoMapper;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.Enums;
@@ -7,11 +8,12 @@ using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Infrastructure.EntityFramework.Models;
+using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories.Queries;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
-namespace Bit.Infrastructure.EntityFramework.Repositories;
+namespace Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
public class OrganizationUserRepository : Repository, IOrganizationUserRepository
{
@@ -440,15 +442,23 @@ public class OrganizationUserRepository : Repository requestedCollections)
diff --git a/src/Infrastructure.EntityFramework/Repositories/DatabaseContextExtensions.cs b/src/Infrastructure.EntityFramework/Repositories/DatabaseContextExtensions.cs
index 6e954e030c..40f2a79887 100644
--- a/src/Infrastructure.EntityFramework/Repositories/DatabaseContextExtensions.cs
+++ b/src/Infrastructure.EntityFramework/Repositories/DatabaseContextExtensions.cs
@@ -1,4 +1,6 @@
-using System.Diagnostics;
+#nullable enable
+
+using System.Diagnostics;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Auth.Enums;
using Bit.Core.Enums;
@@ -11,8 +13,18 @@ namespace Bit.Infrastructure.EntityFramework.Repositories;
public static class DatabaseContextExtensions
{
+ ///
+ /// Bump the account revision date for the user.
+ /// The caller is responsible for providing a valid UserId (not a null or default Guid) for a user that exists
+ /// in the database.
+ ///
public static async Task UserBumpAccountRevisionDateAsync(this DatabaseContext context, Guid userId)
{
+ if (userId == Guid.Empty)
+ {
+ throw new ArgumentException("Invalid UserId.");
+ }
+
var user = await context.Users.FindAsync(userId);
Debug.Assert(user is not null, "The user id is expected to be validated as a true-in database user before making this call.");
user.AccountRevisionDate = DateTime.UtcNow;
diff --git a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs
index 090c36ff29..befb835e26 100644
--- a/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs
+++ b/src/Infrastructure.EntityFramework/Vault/Repositories/CipherRepository.cs
@@ -142,8 +142,10 @@ public class CipherRepository : Repository ciphers, IEnumerable folders)
+ public async Task CreateAsync(Guid userId, IEnumerable ciphers,
+ IEnumerable folders)
{
+ ciphers = ciphers.ToList();
if (!ciphers.Any())
{
return;
@@ -156,7 +158,8 @@ public class CipherRepository : Repository>(ciphers);
await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, cipherEntities);
- await dbContext.UserBumpAccountRevisionDateAsync(ciphers.First().UserId.GetValueOrDefault());
+ await dbContext.UserBumpAccountRevisionDateAsync(userId);
+
await dbContext.SaveChangesAsync();
}
}
diff --git a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs
index 598d93b177..e425cf7254 100644
--- a/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs
+++ b/src/SharedWeb/Utilities/ServiceCollectionExtensions.cs
@@ -5,6 +5,7 @@ using System.Security.Cryptography.X509Certificates;
using AspNetCoreRateLimit;
using Azure.Storage.Queues;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
+using Bit.Core.AdminConsole.Models.Data.Integrations;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.Services;
using Bit.Core.AdminConsole.Services.Implementations;
@@ -324,42 +325,7 @@ public static class ServiceCollectionExtensions
services.AddSingleton();
}
- if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString))
- {
- services.AddKeyedSingleton("storage");
-
- if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) &&
- CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName))
- {
- services.AddKeyedSingleton("broadcast");
- }
- else
- {
- services.AddKeyedSingleton("broadcast");
- }
- }
- else if (globalSettings.SelfHosted)
- {
- services.AddKeyedSingleton("storage");
-
- if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) &&
- CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) &&
- CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) &&
- CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName))
- {
- services.AddKeyedSingleton("broadcast");
- }
- else
- {
- services.AddKeyedSingleton("broadcast");
- }
- }
- else
- {
- services.AddKeyedSingleton("storage");
- services.AddKeyedSingleton("broadcast");
- }
- services.AddScoped();
+ services.AddEventWriteServices(globalSettings);
if (CoreHelpers.SettingHasValue(globalSettings.Attachment.ConnectionString))
{
@@ -584,6 +550,193 @@ public static class ServiceCollectionExtensions
return globalSettings;
}
+ public static IServiceCollection AddEventWriteServices(this IServiceCollection services, GlobalSettings globalSettings)
+ {
+ if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString))
+ {
+ services.AddKeyedSingleton("storage");
+
+ if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) &&
+ CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName))
+ {
+ services.AddKeyedSingleton("broadcast");
+ }
+ else
+ {
+ services.AddKeyedSingleton("broadcast");
+ }
+ }
+ else if (globalSettings.SelfHosted)
+ {
+ services.AddKeyedSingleton("storage");
+
+ if (IsRabbitMqEnabled(globalSettings))
+ {
+ services.AddKeyedSingleton("broadcast");
+ }
+ else
+ {
+ services.AddKeyedSingleton("broadcast");
+ }
+ }
+ else
+ {
+ services.AddKeyedSingleton("storage");
+ services.AddKeyedSingleton("broadcast");
+ }
+
+ services.AddScoped();
+ return services;
+ }
+
+ public static IServiceCollection AddAzureServiceBusListeners(this IServiceCollection services, GlobalSettings globalSettings)
+ {
+ if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) &&
+ CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName))
+ {
+ services.AddSingleton();
+ services.AddSingleton();
+ services.AddKeyedSingleton("persistent");
+ services.AddSingleton(provider =>
+ new AzureServiceBusEventListenerService(
+ provider.GetRequiredService(),
+ provider.GetRequiredService>(),
+ globalSettings,
+ globalSettings.EventLogging.AzureServiceBus.EventRepositorySubscriptionName));
+
+
+ services.AddSlackService(globalSettings);
+ services.AddSingleton();
+ services.AddSingleton(provider =>
+ new AzureServiceBusEventListenerService(
+ provider.GetRequiredService(),
+ provider.GetRequiredService>(),
+ globalSettings,
+ globalSettings.EventLogging.AzureServiceBus.SlackSubscriptionName));
+
+ services.AddSingleton();
+ services.AddHttpClient(WebhookEventHandler.HttpClientName);
+ services.AddSingleton(provider =>
+ new AzureServiceBusEventListenerService(
+ provider.GetRequiredService(),
+ provider.GetRequiredService>(),
+ globalSettings,
+ globalSettings.EventLogging.AzureServiceBus.WebhookSubscriptionName));
+ }
+
+ return services;
+ }
+
+ public static IServiceCollection AddRabbitMqListeners(this IServiceCollection services, GlobalSettings globalSettings)
+ {
+ if (IsRabbitMqEnabled(globalSettings))
+ {
+ services.AddRabbitMqEventRepositoryListener(globalSettings);
+
+ services.AddSlackService(globalSettings);
+ services.AddRabbitMqIntegration(
+ globalSettings.EventLogging.RabbitMq.SlackEventsQueueName,
+ globalSettings.EventLogging.RabbitMq.SlackIntegrationQueueName,
+ globalSettings.EventLogging.RabbitMq.SlackIntegrationRetryQueueName,
+ globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName,
+ IntegrationType.Slack,
+ globalSettings);
+
+ services.AddHttpClient(WebhookIntegrationHandler.HttpClientName);
+ services.AddRabbitMqIntegration(
+ globalSettings.EventLogging.RabbitMq.WebhookEventsQueueName,
+ globalSettings.EventLogging.RabbitMq.WebhookIntegrationQueueName,
+ globalSettings.EventLogging.RabbitMq.WebhookIntegrationRetryQueueName,
+ globalSettings.EventLogging.RabbitMq.IntegrationDeadLetterQueueName,
+ IntegrationType.Webhook,
+ globalSettings);
+ }
+
+ return services;
+ }
+
+ public static IServiceCollection AddSlackService(this IServiceCollection services, GlobalSettings globalSettings)
+ {
+ if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) &&
+ CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) &&
+ CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes))
+ {
+ services.AddHttpClient(SlackService.HttpClientName);
+ services.AddSingleton();
+ }
+ else
+ {
+ services.AddSingleton();
+ }
+
+ return services;
+ }
+
+ private static IServiceCollection AddRabbitMqEventRepositoryListener(this IServiceCollection services, GlobalSettings globalSettings)
+ {
+ services.AddSingleton();
+ services.AddKeyedSingleton("persistent");
+
+ services.AddSingleton(provider =>
+ new RabbitMqEventListenerService(
+ provider.GetRequiredService(),
+ provider.GetRequiredService>(),
+ globalSettings,
+ globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName));
+
+ return services;
+ }
+
+ private static IServiceCollection AddRabbitMqIntegration(this IServiceCollection services,
+ string eventQueueName,
+ string integrationQueueName,
+ string integrationRetryQueueName,
+ string integrationDeadLetterQueueName,
+ IntegrationType integrationType,
+ GlobalSettings globalSettings)
+ where TConfig : class
+ where THandler : class, IIntegrationHandler
+ {
+ var routingKey = integrationType.ToRoutingKey();
+
+ services.AddSingleton();
+ services.AddKeyedSingleton(routingKey, (provider, _) =>
+ new EventIntegrationHandler(
+ integrationType,
+ provider.GetRequiredService(),
+ provider.GetRequiredService(),
+ provider.GetRequiredService(),
+ provider.GetRequiredService()));
+
+ services.AddSingleton(provider =>
+ new RabbitMqEventListenerService(
+ provider.GetRequiredKeyedService(routingKey),
+ provider.GetRequiredService>(),
+ globalSettings,
+ eventQueueName));
+
+ services.AddSingleton, THandler>();
+ services.AddSingleton(provider =>
+ new RabbitMqIntegrationListenerService(
+ handler: provider.GetRequiredService>(),
+ routingKey: routingKey,
+ queueName: integrationQueueName,
+ retryQueueName: integrationRetryQueueName,
+ deadLetterQueueName: integrationDeadLetterQueueName,
+ globalSettings: globalSettings,
+ logger: provider.GetRequiredService>()));
+
+ return services;
+ }
+
+ private static bool IsRabbitMqEnabled(GlobalSettings settings)
+ {
+ return CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.HostName) &&
+ CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Username) &&
+ CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.Password) &&
+ CoreHelpers.SettingHasValue(settings.EventLogging.RabbitMq.EventExchangeName);
+ }
+
public static void UseDefaultMiddleware(this IApplicationBuilder app,
IWebHostEnvironment env, GlobalSettings globalSettings)
{
diff --git a/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Create.sql b/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Create.sql
index 6979a10ae2..7c1b168f5c 100644
--- a/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Create.sql
+++ b/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Create.sql
@@ -2,7 +2,7 @@
@Id BIGINT OUTPUT,
@UserId UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
- @ExternalId NVARCHAR(50),
+ @ExternalId NVARCHAR(300),
@CreationDate DATETIME2(7)
AS
BEGIN
diff --git a/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Update.sql b/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Update.sql
index facbc44572..bbd0720ba2 100644
--- a/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Update.sql
+++ b/src/Sql/Auth/dbo/Stored Procedures/SsoUser_Update.sql
@@ -2,7 +2,7 @@
@Id BIGINT OUTPUT,
@UserId UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER,
- @ExternalId NVARCHAR(50),
+ @ExternalId NVARCHAR(300),
@CreationDate DATETIME2(7)
AS
BEGIN
diff --git a/src/Sql/Auth/dbo/Tables/SsoUser.sql b/src/Sql/Auth/dbo/Tables/SsoUser.sql
index f6477f56c2..2f8cd2f190 100644
--- a/src/Sql/Auth/dbo/Tables/SsoUser.sql
+++ b/src/Sql/Auth/dbo/Tables/SsoUser.sql
@@ -2,7 +2,7 @@
[Id] BIGINT IDENTITY (1, 1) NOT NULL,
[UserId] UNIQUEIDENTIFIER NOT NULL,
[OrganizationId] UNIQUEIDENTIFIER NULL,
- [ExternalId] NVARCHAR(50) NOT NULL,
+ [ExternalId] NVARCHAR(300) NOT NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
CONSTRAINT [PK_SsoUser] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_SsoUser_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE,
diff --git a/src/Sql/Tools/dbo/Stored Procedures/Send_Create.sql b/src/Sql/Tools/dbo/Stored Procedures/Send_Create.sql
index 396c619ba4..58375a0397 100644
--- a/src/Sql/Tools/dbo/Stored Procedures/Send_Create.sql
+++ b/src/Sql/Tools/dbo/Stored Procedures/Send_Create.sql
@@ -14,7 +14,10 @@
@DeletionDate DATETIME2(7),
@Disabled BIT,
@HideEmail BIT,
- @CipherId UNIQUEIDENTIFIER = NULL
+ @CipherId UNIQUEIDENTIFIER = NULL,
+-- FIXME: remove null default value once this argument has been
+-- in 2 server releases
+ @Emails NVARCHAR(1024) = NULL
AS
BEGIN
SET NOCOUNT ON
@@ -36,7 +39,8 @@ BEGIN
[DeletionDate],
[Disabled],
[HideEmail],
- [CipherId]
+ [CipherId],
+ [Emails]
)
VALUES
(
@@ -55,7 +59,8 @@ BEGIN
@DeletionDate,
@Disabled,
@HideEmail,
- @CipherId
+ @CipherId,
+ @Emails
)
IF @UserId IS NOT NULL
diff --git a/src/Sql/Tools/dbo/Stored Procedures/Send_Update.sql b/src/Sql/Tools/dbo/Stored Procedures/Send_Update.sql
index e975e18ad9..43873b4b88 100644
--- a/src/Sql/Tools/dbo/Stored Procedures/Send_Update.sql
+++ b/src/Sql/Tools/dbo/Stored Procedures/Send_Update.sql
@@ -14,7 +14,8 @@
@DeletionDate DATETIME2(7),
@Disabled BIT,
@HideEmail BIT,
- @CipherId UNIQUEIDENTIFIER = NULL
+ @CipherId UNIQUEIDENTIFIER = NULL,
+ @Emails NVARCHAR(1024) = NULL
AS
BEGIN
SET NOCOUNT ON
@@ -36,7 +37,8 @@ BEGIN
[DeletionDate] = @DeletionDate,
[Disabled] = @Disabled,
[HideEmail] = @HideEmail,
- [CipherId] = @CipherId
+ [CipherId] = @CipherId,
+ [Emails] = @Emails
WHERE
[Id] = @Id
diff --git a/src/Sql/Tools/dbo/Tables/Send.sql b/src/Sql/Tools/dbo/Tables/Send.sql
index 71acf4a9f3..2130dbc07e 100644
--- a/src/Sql/Tools/dbo/Tables/Send.sql
+++ b/src/Sql/Tools/dbo/Tables/Send.sql
@@ -6,6 +6,7 @@
[Data] VARCHAR(MAX) NOT NULL,
[Key] VARCHAR (MAX) NOT NULL,
[Password] NVARCHAR (300) NULL,
+ [Emails] NVARCHAR (1024) NULL,
[MaxAccessCount] INT NULL,
[AccessCount] INT NOT NULL,
[CreationDate] DATETIME2 (7) NOT NULL,
diff --git a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs
index 8a33e17053..f7863401b5 100644
--- a/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs
+++ b/test/Api.Test/AdminConsole/Controllers/OrganizationIntegrationsConfigurationControllerTests.cs
@@ -3,10 +3,10 @@ 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.AdminConsole.Models.Data.Integrations;
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;
diff --git a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs
index 0076d8bca1..77ce06f4f8 100644
--- a/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs
+++ b/test/Api.Test/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModelTests.cs
@@ -1,7 +1,7 @@
using System.Text.Json;
using Bit.Api.AdminConsole.Models.Request.Organizations;
+using Bit.Core.AdminConsole.Models.Data.Integrations;
using Bit.Core.Enums;
-using Bit.Core.Models.Data.Integrations;
using Xunit;
namespace Bit.Api.Test.AdminConsole.Models.Request.Organizations;
diff --git a/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs b/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs
new file mode 100644
index 0000000000..44774449c1
--- /dev/null
+++ b/test/Core.Test/AdminConsole/Models/Data/Integrations/IntegrationMessageTests.cs
@@ -0,0 +1,53 @@
+using System.Text.Json;
+using Bit.Core.AdminConsole.Models.Data.Integrations;
+using Bit.Core.Enums;
+using Xunit;
+
+namespace Bit.Core.Test.Models.Data.Integrations;
+
+public class IntegrationMessageTests
+{
+ [Fact]
+ public void ApplyRetry_IncrementsRetryCountAndSetsDelayUntilDate()
+ {
+ var message = new IntegrationMessage
+ {
+ RetryCount = 2,
+ DelayUntilDate = null
+ };
+
+ var baseline = DateTime.UtcNow;
+ message.ApplyRetry(baseline);
+
+ Assert.Equal(3, message.RetryCount);
+ Assert.True(message.DelayUntilDate > baseline);
+ }
+
+ [Fact]
+ public void FromToJson_SerializesCorrectly()
+ {
+ var message = new IntegrationMessage
+ {
+ Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"),
+ RenderedTemplate = "This is the message",
+ IntegrationType = IntegrationType.Webhook,
+ RetryCount = 2,
+ DelayUntilDate = null
+ };
+
+ var json = message.ToJson();
+ var result = IntegrationMessage.FromJson(json);
+
+ Assert.Equal(message.Configuration, result.Configuration);
+ Assert.Equal(message.RenderedTemplate, result.RenderedTemplate);
+ Assert.Equal(message.IntegrationType, result.IntegrationType);
+ Assert.Equal(message.RetryCount, result.RetryCount);
+ }
+
+ [Fact]
+ public void FromJson_InvalidJson_ThrowsJsonException()
+ {
+ var json = "{ Invalid JSON";
+ Assert.Throws(() => IntegrationMessage.FromJson(json));
+ }
+}
diff --git a/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs
new file mode 100644
index 0000000000..f0a0d1d724
--- /dev/null
+++ b/test/Core.Test/AdminConsole/Services/EventIntegrationHandlerTests.cs
@@ -0,0 +1,212 @@
+using System.Text.Json;
+using Bit.Core.AdminConsole.Entities;
+using Bit.Core.AdminConsole.Models.Data.Integrations;
+using Bit.Core.Entities;
+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 EventIntegrationHandlerTests
+{
+ private const string _templateBase = "Date: #Date#, Type: #Type#, UserId: #UserId#";
+ private const string _templateWithOrganization = "Org: #OrganizationName#";
+ private const string _templateWithUser = "#UserName#, #UserEmail#";
+ private const string _templateWithActingUser = "#ActingUserName#, #ActingUserEmail#";
+ private const string _url = "https://localhost";
+ private const string _url2 = "https://example.com";
+ private readonly IIntegrationPublisher _integrationPublisher = Substitute.For();
+
+ private SutProvider> GetSutProvider(
+ List configurations)
+ {
+ var configurationRepository = Substitute.For();
+ configurationRepository.GetConfigurationDetailsAsync(Arg.Any(),
+ IntegrationType.Webhook, Arg.Any()).Returns(configurations);
+
+ return new SutProvider>()
+ .SetDependency(configurationRepository)
+ .SetDependency(_integrationPublisher)
+ .SetDependency(IntegrationType.Webhook)
+ .Create();
+ }
+
+ private static IntegrationMessage expectedMessage(string template)
+ {
+ return new IntegrationMessage()
+ {
+ IntegrationType = IntegrationType.Webhook,
+ Configuration = new WebhookIntegrationConfigurationDetails(_url),
+ RenderedTemplate = template,
+ RetryCount = 0,
+ DelayUntilDate = null
+ };
+ }
+
+ private static List NoConfigurations()
+ {
+ return [];
+ }
+
+ private static List OneConfiguration(string template)
+ {
+ var config = Substitute.For();
+ config.Configuration = null;
+ config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url });
+ config.Template = template;
+
+ return [config];
+ }
+
+ private static List TwoConfigurations(string template)
+ {
+ var config = Substitute.For();
+ config.Configuration = null;
+ config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url });
+ config.Template = template;
+ var config2 = Substitute.For();
+ config2.Configuration = null;
+ config2.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url2 });
+ config2.Template = template;
+
+ return [config, config2];
+ }
+
+ [Theory, BitAutoData]
+ public async Task HandleEventAsync_BaseTemplateNoConfigurations_DoesNothing(EventMessage eventMessage)
+ {
+ var sutProvider = GetSutProvider(NoConfigurations());
+
+ await sutProvider.Sut.HandleEventAsync(eventMessage);
+ Assert.Empty(_integrationPublisher.ReceivedCalls());
+ }
+
+ [Theory, BitAutoData]
+ public async Task HandleEventAsync_BaseTemplateOneConfiguration_CallsProcessEventIntegrationAsync(EventMessage eventMessage)
+ {
+ var sutProvider = GetSutProvider(OneConfiguration(_templateBase));
+
+ await sutProvider.Sut.HandleEventAsync(eventMessage);
+
+ var expectedMessage = EventIntegrationHandlerTests.expectedMessage(
+ $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
+ );
+
+ Assert.Single(_integrationPublisher.ReceivedCalls());
+ await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage)));
+ await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any());
+ await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any());
+ }
+
+ [Theory, BitAutoData]
+ public async Task HandleEventAsync_ActingUserTemplate_LoadsUserFromRepository(EventMessage eventMessage)
+ {
+ var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));
+ var user = Substitute.For();
+ user.Email = "test@example.com";
+ user.Name = "Test";
+
+ sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(user);
+ await sutProvider.Sut.HandleEventAsync(eventMessage);
+
+ var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"{user.Name}, {user.Email}");
+
+ Assert.Single(_integrationPublisher.ReceivedCalls());
+ await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage)));
+ await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any());
+ await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.ActingUserId ?? Guid.Empty);
+ }
+
+ [Theory, BitAutoData]
+ public async Task HandleEventAsync_OrganizationTemplate_LoadsOrganizationFromRepository(EventMessage eventMessage)
+ {
+ var sutProvider = GetSutProvider(OneConfiguration(_templateWithOrganization));
+ var organization = Substitute.For();
+ organization.Name = "Test";
+
+ sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(organization);
+ await sutProvider.Sut.HandleEventAsync(eventMessage);
+
+ Assert.Single(_integrationPublisher.ReceivedCalls());
+
+ var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"Org: {organization.Name}");
+
+ Assert.Single(_integrationPublisher.ReceivedCalls());
+ await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage)));
+ await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.OrganizationId ?? Guid.Empty);
+ await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any());
+ }
+
+ [Theory, BitAutoData]
+ public async Task HandleEventAsync_UserTemplate_LoadsUserFromRepository(EventMessage eventMessage)
+ {
+ var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
+ var user = Substitute.For();
+ user.Email = "test@example.com";
+ user.Name = "Test";
+
+ sutProvider.GetDependency().GetByIdAsync(Arg.Any()).Returns(user);
+ await sutProvider.Sut.HandleEventAsync(eventMessage);
+
+ var expectedMessage = EventIntegrationHandlerTests.expectedMessage($"{user.Name}, {user.Email}");
+
+ Assert.Single(_integrationPublisher.ReceivedCalls());
+ await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage)));
+ await sutProvider.GetDependency().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any());
+ await sutProvider.GetDependency().Received(1).GetByIdAsync(eventMessage.UserId ?? Guid.Empty);
+ }
+
+ [Theory, BitAutoData]
+ public async Task HandleManyEventsAsync_BaseTemplateNoConfigurations_DoesNothing(List eventMessages)
+ {
+ var sutProvider = GetSutProvider(NoConfigurations());
+
+ await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
+ Assert.Empty(_integrationPublisher.ReceivedCalls());
+ }
+
+ [Theory, BitAutoData]
+ public async Task HandleManyEventsAsync_BaseTemplateOneConfiguration_CallsProcessEventIntegrationAsync(List eventMessages)
+ {
+ var sutProvider = GetSutProvider(OneConfiguration(_templateBase));
+
+ await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
+
+ foreach (var eventMessage in eventMessages)
+ {
+ var expectedMessage = EventIntegrationHandlerTests.expectedMessage(
+ $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
+ );
+ await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage)));
+ }
+ }
+
+ [Theory, BitAutoData]
+ public async Task HandleManyEventsAsync_BaseTemplateTwoConfigurations_CallsProcessEventIntegrationAsyncMultipleTimes(
+ List eventMessages)
+ {
+ var sutProvider = GetSutProvider(TwoConfigurations(_templateBase));
+
+ await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
+
+ foreach (var eventMessage in eventMessages)
+ {
+ var expectedMessage = EventIntegrationHandlerTests.expectedMessage(
+ $"Date: {eventMessage.Date}, Type: {eventMessage.Type}, UserId: {eventMessage.UserId}"
+ );
+ await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage)));
+
+ expectedMessage.Configuration = new WebhookIntegrationConfigurationDetails(_url2);
+ await _integrationPublisher.Received(1).PublishAsync(Arg.Is(AssertHelper.AssertPropertyEqual(expectedMessage)));
+ }
+ }
+}
diff --git a/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs b/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs
new file mode 100644
index 0000000000..10f39665d5
--- /dev/null
+++ b/test/Core.Test/AdminConsole/Services/IntegrationHandlerTests.cs
@@ -0,0 +1,41 @@
+using Bit.Core.AdminConsole.Models.Data.Integrations;
+using Bit.Core.Enums;
+using Bit.Core.Services;
+using Xunit;
+
+namespace Bit.Core.Test.Services;
+
+public class IntegrationHandlerTests
+{
+
+ [Fact]
+ public async Task HandleAsync_ConvertsJsonToTypedIntegrationMessage()
+ {
+ var sut = new TestIntegrationHandler();
+ var expected = new IntegrationMessage()
+ {
+ Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"),
+ IntegrationType = IntegrationType.Webhook,
+ RenderedTemplate = "Template",
+ DelayUntilDate = null,
+ RetryCount = 0
+ };
+
+ var result = await sut.HandleAsync(expected.ToJson());
+ var typedResult = Assert.IsType>(result.Message);
+
+ Assert.Equal(expected.Configuration, typedResult.Configuration);
+ Assert.Equal(expected.RenderedTemplate, typedResult.RenderedTemplate);
+ Assert.Equal(expected.IntegrationType, typedResult.IntegrationType);
+ }
+
+ private class TestIntegrationHandler : IntegrationHandlerBase
+ {
+ public override Task