mirror of
https://github.com/bitwarden/server.git
synced 2025-06-17 09:33:51 -05:00
Merge branch 'main' into auth/pm-19438/emergency-access-docs
This commit is contained in:
commit
404616d918
7
.github/renovate.json5
vendored
7
.github/renovate.json5
vendored
@ -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"],
|
||||
},
|
||||
|
199
bitwarden_license/src/Sso/package-lock.json
generated
199
bitwarden_license/src/Sso/package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
199
src/Admin/package-lock.json
generated
199
src/Admin/package-lock.json
generated
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
||||
|
@ -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<Core.HostedServices.ApplicationCacheHostedService>();
|
||||
}
|
||||
|
||||
// Slack
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes))
|
||||
{
|
||||
services.AddHttpClient(SlackService.HttpClientName);
|
||||
services.AddSingleton<ISlackService, SlackService>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<ISlackService, NoopSlackService>();
|
||||
}
|
||||
// Add SlackService for OAuth API requests - if configured
|
||||
services.AddSlackService(globalSettings);
|
||||
}
|
||||
|
||||
public void Configure(
|
||||
|
@ -151,6 +151,16 @@ public class CiphersController : Controller
|
||||
public async Task<CipherResponseModel> 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<CipherResponseModel> 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)
|
||||
{
|
||||
|
@ -11,6 +11,10 @@ namespace Bit.Api.Vault.Models.Request;
|
||||
|
||||
public class CipherRequestModel
|
||||
{
|
||||
/// <summary>
|
||||
/// The Id of the user that encrypted the cipher. It should always represent a UserId.
|
||||
/// </summary>
|
||||
public Guid? EncryptedFor { get; set; }
|
||||
public CipherType Type { get; set; }
|
||||
|
||||
[StringLength(36)]
|
||||
|
@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
@ -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;
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
using System.Text.Json;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||
|
||||
public class IntegrationMessage<T> : 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<T> FromJson(string json)
|
||||
{
|
||||
return JsonSerializer.Deserialize<IntegrationMessage<T>>(json);
|
||||
}
|
||||
}
|
@ -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)
|
||||
{
|
||||
|
@ -1,3 +1,3 @@
|
||||
namespace Bit.Core.Models.Data.Integrations;
|
||||
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||
|
||||
public record SlackIntegration(string token);
|
||||
|
@ -1,3 +1,3 @@
|
||||
namespace Bit.Core.Models.Data.Integrations;
|
||||
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||
|
||||
public record SlackIntegrationConfiguration(string channelId);
|
||||
|
@ -1,3 +1,3 @@
|
||||
namespace Bit.Core.Models.Data.Integrations;
|
||||
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||
|
||||
public record SlackIntegrationConfigurationDetails(string channelId, string token);
|
||||
|
@ -1,3 +1,3 @@
|
||||
namespace Bit.Core.Models.Data.Integrations;
|
||||
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||
|
||||
public record WebhookIntegrationConfiguration(string url);
|
||||
|
@ -0,0 +1,3 @@
|
||||
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||
|
||||
public record WebhookIntegrationConfigurationDetails(string url);
|
@ -1,3 +0,0 @@
|
||||
namespace Bit.Core.Models.Data.Integrations;
|
||||
|
||||
public record WebhookIntegrationConfigurationDetils(string url);
|
24
src/Core/AdminConsole/Services/IIntegrationHandler.cs
Normal file
24
src/Core/AdminConsole/Services/IIntegrationHandler.cs
Normal file
@ -0,0 +1,24 @@
|
||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
public interface IIntegrationHandler
|
||||
{
|
||||
Task<IntegrationHandlerResult> HandleAsync(string json);
|
||||
}
|
||||
|
||||
public interface IIntegrationHandler<T> : IIntegrationHandler
|
||||
{
|
||||
Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<T> message);
|
||||
}
|
||||
|
||||
public abstract class IntegrationHandlerBase<T> : IIntegrationHandler<T>
|
||||
{
|
||||
public async Task<IntegrationHandlerResult> HandleAsync(string json)
|
||||
{
|
||||
var message = IntegrationMessage<T>.FromJson(json);
|
||||
return await HandleAsync(message);
|
||||
}
|
||||
|
||||
public abstract Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<T> message);
|
||||
}
|
8
src/Core/AdminConsole/Services/IIntegrationPublisher.cs
Normal file
8
src/Core/AdminConsole/Services/IIntegrationPublisher.cs
Normal file
@ -0,0 +1,8 @@
|
||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
public interface IIntegrationPublisher
|
||||
{
|
||||
Task PublishAsync(IIntegrationMessage message);
|
||||
}
|
@ -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<T>(
|
||||
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<T>()
|
||||
?? throw new InvalidOperationException($"Failed to deserialize to {typeof(T).Name}");
|
||||
|
||||
var message = new IntegrationMessage<T>
|
||||
{
|
||||
IntegrationType = integrationType,
|
||||
Configuration = config,
|
||||
RenderedTemplate = renderedTemplate,
|
||||
RetryCount = 0,
|
||||
DelayUntilDate = null
|
||||
};
|
||||
|
||||
await integrationPublisher.PublishAsync(message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task HandleManyEventsAsync(IEnumerable<EventMessage> eventMessages)
|
||||
{
|
||||
foreach (var eventMessage in eventMessages)
|
||||
{
|
||||
await HandleEventAsync(eventMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IntegrationTemplateContext> 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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<Task<IConnection>>(CreateConnectionAsync);
|
||||
}
|
||||
|
@ -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<RabbitMqIntegrationListenerService> _logger;
|
||||
private readonly int _retryTiming;
|
||||
|
||||
public RabbitMqIntegrationListenerService(IIntegrationHandler handler,
|
||||
string routingKey,
|
||||
string queueName,
|
||||
string retryQueueName,
|
||||
string deadLetterQueueName,
|
||||
GlobalSettings globalSettings,
|
||||
ILogger<RabbitMqIntegrationListenerService> 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<string, object>
|
||||
{
|
||||
{ "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();
|
||||
}
|
||||
}
|
@ -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<Task<IConnection>> _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<Task<IConnection>>(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<IConnection> CreateConnectionAsync()
|
||||
{
|
||||
return await _factory.CreateConnectionAsync();
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -0,0 +1,19 @@
|
||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
public class SlackIntegrationHandler(
|
||||
ISlackService slackService)
|
||||
: IntegrationHandlerBase<SlackIntegrationConfigurationDetails>
|
||||
{
|
||||
public override async Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<SlackIntegrationConfigurationDetails> message)
|
||||
{
|
||||
await slackService.SendSlackMessageByChannelIdAsync(
|
||||
message.Configuration.token,
|
||||
message.RenderedTemplate,
|
||||
message.Configuration.channelId
|
||||
);
|
||||
|
||||
return new IntegrationHandlerResult(success: true, message: message);
|
||||
}
|
||||
}
|
@ -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<WebhookIntegrationConfigurationDetils>();
|
||||
var config = mergedConfiguration.Deserialize<WebhookIntegrationConfigurationDetails>();
|
||||
if (config is null || string.IsNullOrEmpty(config.url))
|
||||
{
|
||||
return;
|
||||
|
@ -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<WebhookIntegrationConfigurationDetails>
|
||||
{
|
||||
private readonly HttpClient _httpClient = httpClientFactory.CreateClient(HttpClientName);
|
||||
|
||||
public const string HttpClientName = "WebhookIntegrationHandlerHttpClient";
|
||||
|
||||
public override async Task<IntegrationHandlerResult> HandleAsync(IntegrationMessage<WebhookIntegrationConfigurationDetails> 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ public class SsoUser : ITableObject<long>
|
||||
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;
|
||||
|
||||
|
@ -34,9 +34,9 @@
|
||||
<PackageReference Include="DnsClient" Version="1.8.0" />
|
||||
<PackageReference Include="Fido2.AspNet" Version="3.0.1" />
|
||||
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
|
||||
<PackageReference Include="MailKit" Version="4.12.0" />
|
||||
<PackageReference Include="MailKit" Version="4.12.1" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.49.0" />
|
||||
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.51.0" />
|
||||
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.7.0" />
|
||||
|
@ -36,6 +36,11 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
|
||||
public string? TwoFactorRecoveryCode { get; set; }
|
||||
public string? EquivalentDomains { get; set; }
|
||||
public string? ExcludedGlobalEquivalentDomains { get; set; }
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public DateTime AccountRevisionDate { get; set; } = DateTime.UtcNow;
|
||||
public string? Key { get; set; }
|
||||
public string? PublicKey { get; set; }
|
||||
|
@ -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('"');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,9 +60,21 @@ public class Send : ITableObject<Guid>
|
||||
/// <summary>
|
||||
/// Password provided by the user. Protected with pbkdf2.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This field is mutually exclusive with <see cref="Emails" />
|
||||
/// </remarks>
|
||||
[MaxLength(300)]
|
||||
public string? Password { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Comma-separated list of emails for OTP authentication.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This field is mutually exclusive with <see cref="Password" />
|
||||
/// </remarks>
|
||||
[MaxLength(1024)]
|
||||
public string? Emails { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The send becomes unavailable to API callers when
|
||||
/// <see cref="AccessCount"/> >= <see cref="MaxAccessCount"/>.
|
||||
|
@ -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);
|
||||
|
50
src/Core/Tools/Models/Data/SendAuthenticationTypes.cs
Normal file
50
src/Core/Tools/Models/Data/SendAuthenticationTypes.cs
Normal file
@ -0,0 +1,50 @@
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.Tools.Models.Data;
|
||||
|
||||
/// <summary>
|
||||
/// A discriminated union for send authentication.
|
||||
/// </summary>
|
||||
/// <example>
|
||||
/// 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()
|
||||
/// };
|
||||
/// </example>
|
||||
public abstract record SendAuthenticationMethod;
|
||||
|
||||
/// <summary>
|
||||
/// Never issue a send claim.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This claim is issued when a send does not exist or when a send
|
||||
/// has exceeded its max access attempts.
|
||||
/// </remarks>
|
||||
public record NeverAuthenticate : SendAuthenticationMethod;
|
||||
|
||||
/// <summary>
|
||||
/// Create a send claim automatically.
|
||||
/// </summary>
|
||||
public record NotAuthenticated : SendAuthenticationMethod;
|
||||
|
||||
/// <summary>
|
||||
/// Create a send claim by requesting a password confirmation hash.
|
||||
/// </summary>
|
||||
/// <param name="Hash">
|
||||
/// A base64 encoded hash that permits access to the send.
|
||||
/// </param>
|
||||
public record ResourcePassword(string Hash) : SendAuthenticationMethod;
|
||||
|
||||
/// <summary>
|
||||
/// Create a send claim by requesting a one time password (OTP) confirmation code.
|
||||
/// </summary>
|
||||
/// <param name="Emails">
|
||||
/// The list of email addresses permitted access to the send.
|
||||
/// </param>
|
||||
public record EmailOtp(string[] Emails) : SendAuthenticationMethod;
|
@ -0,0 +1,20 @@
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.Tools.SendFeatures.Queries.Interfaces;
|
||||
|
||||
/// <summary>
|
||||
/// Integration with authentication layer for generating send access claims.
|
||||
/// </summary>
|
||||
public interface ISendAuthenticationQuery
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves the authentication method of a Send.
|
||||
/// </summary>
|
||||
/// <param name="sendId">Identifies the send to inspect.</param>
|
||||
/// <returns>
|
||||
/// The authentication method that should be performed for the send.
|
||||
/// </returns>
|
||||
Task<SendAuthenticationMethod> GetAuthenticationMethod(Guid sendId);
|
||||
}
|
@ -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;
|
||||
|
||||
/// <inheritdoc cref="ISendAuthenticationQuery"/>
|
||||
public class SendAuthenticationQuery : ISendAuthenticationQuery
|
||||
{
|
||||
private static readonly NotAuthenticated NOT_AUTHENTICATED = new NotAuthenticated();
|
||||
private static readonly NeverAuthenticate NEVER_AUTHENTICATE = new NeverAuthenticate();
|
||||
|
||||
private readonly ISendRepository _sendRepository;
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates the command
|
||||
/// </summary>
|
||||
/// <param name="sendRepository">
|
||||
/// Retrieves send records
|
||||
/// </param>
|
||||
/// <exception cref="ArgumentNullException">
|
||||
/// Thrown when <paramref name="sendRepository"/> is <see langword="null"/>.
|
||||
/// </exception>
|
||||
public SendAuthenticationQuery(ISendRepository sendRepository)
|
||||
{
|
||||
_sendRepository = sendRepository ?? throw new ArgumentNullException(nameof(sendRepository));
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="ISendAuthenticationQuery.GetAuthenticationMethod"/>
|
||||
public async Task<SendAuthenticationMethod> 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);
|
||||
}
|
||||
}
|
@ -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<ISendAuthorizationService, SendAuthorizationService>();
|
||||
services.AddScoped<ISendValidationService, SendValidationService>();
|
||||
services.AddScoped<ISendCoreHelperService, SendCoreHelperService>();
|
||||
services.AddScoped<ISendAuthenticationQuery, SendAuthenticationQuery>();
|
||||
}
|
||||
}
|
||||
|
@ -32,7 +32,10 @@ public interface ICipherRepository : IRepository<Cipher, Guid>
|
||||
Task DeleteByUserIdAsync(Guid userId);
|
||||
Task DeleteByOrganizationIdAsync(Guid organizationId);
|
||||
Task UpdateCiphersAsync(Guid userId, IEnumerable<Cipher> ciphers);
|
||||
Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders);
|
||||
/// <summary>
|
||||
/// Create ciphers and folders for the specified UserId. Must not be used to create organization owned items.
|
||||
/// </summary>
|
||||
Task CreateAsync(Guid userId, IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders);
|
||||
Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
|
||||
IEnumerable<CollectionCipher> collectionCiphers, IEnumerable<CollectionUser> collectionUsers);
|
||||
Task SoftDeleteAsync(IEnumerable<Guid> ids, Guid userId);
|
||||
|
@ -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<IApplicationCacheService, InMemoryApplicationCacheService>();
|
||||
}
|
||||
|
||||
if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString))
|
||||
{
|
||||
services.AddKeyedSingleton<IEventWriteService, AzureQueueEventWriteService>("storage");
|
||||
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName))
|
||||
{
|
||||
services.AddKeyedSingleton<IEventWriteService, AzureServiceBusEventWriteService>("broadcast");
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("storage");
|
||||
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName))
|
||||
{
|
||||
services.AddKeyedSingleton<IEventWriteService, RabbitMqEventWriteService>("broadcast");
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
|
||||
}
|
||||
}
|
||||
services.AddScoped<IEventWriteService, EventRouteService>();
|
||||
services.AddEventWriteServices(globalSettings);
|
||||
services.AddScoped<IEventService, EventService>();
|
||||
|
||||
services.AddOptionality();
|
||||
@ -109,49 +77,7 @@ public class Startup
|
||||
services.AddHostedService<Core.HostedServices.ApplicationCacheHostedService>();
|
||||
}
|
||||
|
||||
// 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<EventRepositoryHandler>();
|
||||
services.AddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("persistent");
|
||||
services.AddSingleton<IHostedService>(provider =>
|
||||
new RabbitMqEventListenerService(
|
||||
provider.GetRequiredService<EventRepositoryHandler>(),
|
||||
provider.GetRequiredService<ILogger<RabbitMqEventListenerService>>(),
|
||||
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<ISlackService, SlackService>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<ISlackService, NoopSlackService>();
|
||||
}
|
||||
services.AddSingleton<SlackEventHandler>();
|
||||
services.AddSingleton<IHostedService>(provider =>
|
||||
new RabbitMqEventListenerService(
|
||||
provider.GetRequiredService<SlackEventHandler>(),
|
||||
provider.GetRequiredService<ILogger<RabbitMqEventListenerService>>(),
|
||||
globalSettings,
|
||||
globalSettings.EventLogging.RabbitMq.SlackQueueName));
|
||||
|
||||
services.AddHttpClient(WebhookEventHandler.HttpClientName);
|
||||
services.AddSingleton<WebhookEventHandler>();
|
||||
services.AddSingleton<IHostedService>(provider =>
|
||||
new RabbitMqEventListenerService(
|
||||
provider.GetRequiredService<WebhookEventHandler>(),
|
||||
provider.GetRequiredService<ILogger<RabbitMqEventListenerService>>(),
|
||||
globalSettings,
|
||||
globalSettings.EventLogging.RabbitMq.WebhookQueueName));
|
||||
}
|
||||
services.AddRabbitMqListeners(globalSettings);
|
||||
}
|
||||
|
||||
public void Configure(
|
||||
|
@ -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<IEventRepository, TableStorageRepos.EventRepository>();
|
||||
services.AddSingleton<AzureTableStorageEventHandler>();
|
||||
services.AddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("persistent");
|
||||
services.AddSingleton<IHostedService>(provider =>
|
||||
new AzureServiceBusEventListenerService(
|
||||
provider.GetRequiredService<AzureTableStorageEventHandler>(),
|
||||
provider.GetRequiredService<ILogger<AzureServiceBusEventListenerService>>(),
|
||||
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<ISlackService, SlackService>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<ISlackService, NoopSlackService>();
|
||||
}
|
||||
services.AddSingleton<SlackEventHandler>();
|
||||
services.AddSingleton<IHostedService>(provider =>
|
||||
new AzureServiceBusEventListenerService(
|
||||
provider.GetRequiredService<SlackEventHandler>(),
|
||||
provider.GetRequiredService<ILogger<AzureServiceBusEventListenerService>>(),
|
||||
globalSettings,
|
||||
globalSettings.EventLogging.AzureServiceBus.SlackSubscriptionName));
|
||||
|
||||
services.AddSingleton<WebhookEventHandler>();
|
||||
services.AddHttpClient(WebhookEventHandler.HttpClientName);
|
||||
|
||||
services.AddSingleton<IHostedService>(provider =>
|
||||
new AzureServiceBusEventListenerService(
|
||||
provider.GetRequiredService<WebhookEventHandler>(),
|
||||
provider.GetRequiredService<ILogger<AzureServiceBusEventListenerService>>(),
|
||||
globalSettings,
|
||||
globalSettings.EventLogging.AzureServiceBus.WebhookSubscriptionName));
|
||||
}
|
||||
services.AddAzureServiceBusListeners(globalSettings);
|
||||
services.AddHostedService<AzureQueueHostedService>();
|
||||
}
|
||||
|
||||
|
@ -484,7 +484,7 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders)
|
||||
public async Task CreateAsync(Guid userId, IEnumerable<Cipher> ciphers, IEnumerable<Folder> folders)
|
||||
{
|
||||
if (!ciphers.Any())
|
||||
{
|
||||
@ -518,7 +518,7 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
|
||||
|
||||
await connection.ExecuteAsync(
|
||||
$"[{Schema}].[User_BumpAccountRevisionDate]",
|
||||
new { Id = ciphers.First().UserId },
|
||||
new { Id = userId },
|
||||
commandType: CommandType.StoredProcedure, transaction: transaction);
|
||||
|
||||
transaction.Commit();
|
||||
|
@ -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<Core.Entities.OrganizationUser, OrganizationUser, Guid>, IOrganizationUserRepository
|
||||
{
|
||||
@ -440,15 +442,23 @@ public class OrganizationUserRepository : Repository<Core.Entities.OrganizationU
|
||||
}
|
||||
}
|
||||
|
||||
public async override Task ReplaceAsync(Core.Entities.OrganizationUser organizationUser)
|
||||
public override async Task ReplaceAsync(Core.Entities.OrganizationUser organizationUser)
|
||||
{
|
||||
await base.ReplaceAsync(organizationUser);
|
||||
using (var scope = ServiceScopeFactory.CreateScope())
|
||||
|
||||
// Only bump account revision dates for confirmed OrgUsers,
|
||||
// as this is the only status that receives sync data from the organization
|
||||
if (organizationUser.Status is not OrganizationUserStatusType.Confirmed)
|
||||
{
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
await dbContext.UserBumpAccountRevisionDateAsync(organizationUser.UserId.GetValueOrDefault());
|
||||
await dbContext.SaveChangesAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Assert(organizationUser.UserId is not null, "OrganizationUser is confirmed but does not have a UserId.");
|
||||
|
||||
using var scope = ServiceScopeFactory.CreateScope();
|
||||
var dbContext = GetDatabaseContext(scope);
|
||||
await dbContext.UserBumpAccountRevisionDateAsync(organizationUser.UserId.Value);
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task ReplaceAsync(Core.Entities.OrganizationUser obj, IEnumerable<CollectionAccessSelection> requestedCollections)
|
||||
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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;
|
||||
|
@ -142,8 +142,10 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CreateAsync(IEnumerable<Core.Vault.Entities.Cipher> ciphers, IEnumerable<Core.Vault.Entities.Folder> folders)
|
||||
public async Task CreateAsync(Guid userId, IEnumerable<Core.Vault.Entities.Cipher> ciphers,
|
||||
IEnumerable<Core.Vault.Entities.Folder> folders)
|
||||
{
|
||||
ciphers = ciphers.ToList();
|
||||
if (!ciphers.Any())
|
||||
{
|
||||
return;
|
||||
@ -156,7 +158,8 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
|
||||
await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, folderEntities);
|
||||
var cipherEntities = Mapper.Map<List<Cipher>>(ciphers);
|
||||
await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, cipherEntities);
|
||||
await dbContext.UserBumpAccountRevisionDateAsync(ciphers.First().UserId.GetValueOrDefault());
|
||||
await dbContext.UserBumpAccountRevisionDateAsync(userId);
|
||||
|
||||
await dbContext.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
@ -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<IMailEnqueuingService, BlockingMailEnqueuingService>();
|
||||
}
|
||||
|
||||
if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString))
|
||||
{
|
||||
services.AddKeyedSingleton<IEventWriteService, AzureQueueEventWriteService>("storage");
|
||||
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName))
|
||||
{
|
||||
services.AddKeyedSingleton<IEventWriteService, AzureServiceBusEventWriteService>("broadcast");
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
|
||||
}
|
||||
}
|
||||
else if (globalSettings.SelfHosted)
|
||||
{
|
||||
services.AddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("storage");
|
||||
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.HostName) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Username) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.Password) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.EventLogging.RabbitMq.ExchangeName))
|
||||
{
|
||||
services.AddKeyedSingleton<IEventWriteService, RabbitMqEventWriteService>("broadcast");
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("storage");
|
||||
services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
|
||||
}
|
||||
services.AddScoped<IEventWriteService, EventRouteService>();
|
||||
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<IEventWriteService, AzureQueueEventWriteService>("storage");
|
||||
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.ConnectionString) &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.EventLogging.AzureServiceBus.TopicName))
|
||||
{
|
||||
services.AddKeyedSingleton<IEventWriteService, AzureServiceBusEventWriteService>("broadcast");
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
|
||||
}
|
||||
}
|
||||
else if (globalSettings.SelfHosted)
|
||||
{
|
||||
services.AddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("storage");
|
||||
|
||||
if (IsRabbitMqEnabled(globalSettings))
|
||||
{
|
||||
services.AddKeyedSingleton<IEventWriteService, RabbitMqEventWriteService>("broadcast");
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("storage");
|
||||
services.AddKeyedSingleton<IEventWriteService, NoopEventWriteService>("broadcast");
|
||||
}
|
||||
|
||||
services.AddScoped<IEventWriteService, EventRouteService>();
|
||||
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<IEventRepository, TableStorageRepos.EventRepository>();
|
||||
services.AddSingleton<AzureTableStorageEventHandler>();
|
||||
services.AddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("persistent");
|
||||
services.AddSingleton<IHostedService>(provider =>
|
||||
new AzureServiceBusEventListenerService(
|
||||
provider.GetRequiredService<AzureTableStorageEventHandler>(),
|
||||
provider.GetRequiredService<ILogger<AzureServiceBusEventListenerService>>(),
|
||||
globalSettings,
|
||||
globalSettings.EventLogging.AzureServiceBus.EventRepositorySubscriptionName));
|
||||
|
||||
|
||||
services.AddSlackService(globalSettings);
|
||||
services.AddSingleton<SlackEventHandler>();
|
||||
services.AddSingleton<IHostedService>(provider =>
|
||||
new AzureServiceBusEventListenerService(
|
||||
provider.GetRequiredService<SlackEventHandler>(),
|
||||
provider.GetRequiredService<ILogger<AzureServiceBusEventListenerService>>(),
|
||||
globalSettings,
|
||||
globalSettings.EventLogging.AzureServiceBus.SlackSubscriptionName));
|
||||
|
||||
services.AddSingleton<WebhookEventHandler>();
|
||||
services.AddHttpClient(WebhookEventHandler.HttpClientName);
|
||||
services.AddSingleton<IHostedService>(provider =>
|
||||
new AzureServiceBusEventListenerService(
|
||||
provider.GetRequiredService<WebhookEventHandler>(),
|
||||
provider.GetRequiredService<ILogger<AzureServiceBusEventListenerService>>(),
|
||||
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<SlackIntegrationConfigurationDetails, SlackIntegrationHandler>(
|
||||
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<WebhookIntegrationConfigurationDetails, WebhookIntegrationHandler>(
|
||||
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<ISlackService, SlackService>();
|
||||
}
|
||||
else
|
||||
{
|
||||
services.AddSingleton<ISlackService, NoopSlackService>();
|
||||
}
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddRabbitMqEventRepositoryListener(this IServiceCollection services, GlobalSettings globalSettings)
|
||||
{
|
||||
services.AddSingleton<EventRepositoryHandler>();
|
||||
services.AddKeyedSingleton<IEventWriteService, RepositoryEventWriteService>("persistent");
|
||||
|
||||
services.AddSingleton<IHostedService>(provider =>
|
||||
new RabbitMqEventListenerService(
|
||||
provider.GetRequiredService<EventRepositoryHandler>(),
|
||||
provider.GetRequiredService<ILogger<RabbitMqEventListenerService>>(),
|
||||
globalSettings,
|
||||
globalSettings.EventLogging.RabbitMq.EventRepositoryQueueName));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
private static IServiceCollection AddRabbitMqIntegration<TConfig, THandler>(this IServiceCollection services,
|
||||
string eventQueueName,
|
||||
string integrationQueueName,
|
||||
string integrationRetryQueueName,
|
||||
string integrationDeadLetterQueueName,
|
||||
IntegrationType integrationType,
|
||||
GlobalSettings globalSettings)
|
||||
where TConfig : class
|
||||
where THandler : class, IIntegrationHandler<TConfig>
|
||||
{
|
||||
var routingKey = integrationType.ToRoutingKey();
|
||||
|
||||
services.AddSingleton<IIntegrationPublisher, RabbitMqIntegrationPublisher>();
|
||||
services.AddKeyedSingleton<IEventMessageHandler>(routingKey, (provider, _) =>
|
||||
new EventIntegrationHandler<TConfig>(
|
||||
integrationType,
|
||||
provider.GetRequiredService<IIntegrationPublisher>(),
|
||||
provider.GetRequiredService<IOrganizationIntegrationConfigurationRepository>(),
|
||||
provider.GetRequiredService<IUserRepository>(),
|
||||
provider.GetRequiredService<IOrganizationRepository>()));
|
||||
|
||||
services.AddSingleton<IHostedService>(provider =>
|
||||
new RabbitMqEventListenerService(
|
||||
provider.GetRequiredKeyedService<IEventMessageHandler>(routingKey),
|
||||
provider.GetRequiredService<ILogger<RabbitMqEventListenerService>>(),
|
||||
globalSettings,
|
||||
eventQueueName));
|
||||
|
||||
services.AddSingleton<IIntegrationHandler<TConfig>, THandler>();
|
||||
services.AddSingleton<IHostedService>(provider =>
|
||||
new RabbitMqIntegrationListenerService(
|
||||
handler: provider.GetRequiredService<IIntegrationHandler<TConfig>>(),
|
||||
routingKey: routingKey,
|
||||
queueName: integrationQueueName,
|
||||
retryQueueName: integrationRetryQueueName,
|
||||
deadLetterQueueName: integrationDeadLetterQueueName,
|
||||
globalSettings: globalSettings,
|
||||
logger: provider.GetRequiredService<ILogger<RabbitMqIntegrationListenerService>>()));
|
||||
|
||||
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)
|
||||
{
|
||||
|
@ -2,7 +2,7 @@
|
||||
@Id BIGINT OUTPUT,
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@ExternalId NVARCHAR(50),
|
||||
@ExternalId NVARCHAR(300),
|
||||
@CreationDate DATETIME2(7)
|
||||
AS
|
||||
BEGIN
|
||||
|
@ -2,7 +2,7 @@
|
||||
@Id BIGINT OUTPUT,
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@ExternalId NVARCHAR(50),
|
||||
@ExternalId NVARCHAR(300),
|
||||
@CreationDate DATETIME2(7)
|
||||
AS
|
||||
BEGIN
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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<WebhookIntegrationConfigurationDetails>
|
||||
{
|
||||
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<WebhookIntegrationConfigurationDetails>
|
||||
{
|
||||
Configuration = new WebhookIntegrationConfigurationDetails("https://localhost"),
|
||||
RenderedTemplate = "This is the message",
|
||||
IntegrationType = IntegrationType.Webhook,
|
||||
RetryCount = 2,
|
||||
DelayUntilDate = null
|
||||
};
|
||||
|
||||
var json = message.ToJson();
|
||||
var result = IntegrationMessage<WebhookIntegrationConfigurationDetails>.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<JsonException>(() => IntegrationMessage<WebhookIntegrationConfigurationDetails>.FromJson(json));
|
||||
}
|
||||
}
|
@ -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<IIntegrationPublisher>();
|
||||
|
||||
private SutProvider<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>> GetSutProvider(
|
||||
List<OrganizationIntegrationConfigurationDetails> configurations)
|
||||
{
|
||||
var configurationRepository = Substitute.For<IOrganizationIntegrationConfigurationRepository>();
|
||||
configurationRepository.GetConfigurationDetailsAsync(Arg.Any<Guid>(),
|
||||
IntegrationType.Webhook, Arg.Any<EventType>()).Returns(configurations);
|
||||
|
||||
return new SutProvider<EventIntegrationHandler<WebhookIntegrationConfigurationDetails>>()
|
||||
.SetDependency(configurationRepository)
|
||||
.SetDependency(_integrationPublisher)
|
||||
.SetDependency(IntegrationType.Webhook)
|
||||
.Create();
|
||||
}
|
||||
|
||||
private static IntegrationMessage<WebhookIntegrationConfigurationDetails> expectedMessage(string template)
|
||||
{
|
||||
return new IntegrationMessage<WebhookIntegrationConfigurationDetails>()
|
||||
{
|
||||
IntegrationType = IntegrationType.Webhook,
|
||||
Configuration = new WebhookIntegrationConfigurationDetails(_url),
|
||||
RenderedTemplate = template,
|
||||
RetryCount = 0,
|
||||
DelayUntilDate = null
|
||||
};
|
||||
}
|
||||
|
||||
private static List<OrganizationIntegrationConfigurationDetails> NoConfigurations()
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
private static List<OrganizationIntegrationConfigurationDetails> OneConfiguration(string template)
|
||||
{
|
||||
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||
config.Configuration = null;
|
||||
config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url });
|
||||
config.Template = template;
|
||||
|
||||
return [config];
|
||||
}
|
||||
|
||||
private static List<OrganizationIntegrationConfigurationDetails> TwoConfigurations(string template)
|
||||
{
|
||||
var config = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||
config.Configuration = null;
|
||||
config.IntegrationConfiguration = JsonSerializer.Serialize(new { url = _url });
|
||||
config.Template = template;
|
||||
var config2 = Substitute.For<OrganizationIntegrationConfigurationDetails>();
|
||||
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<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_ActingUserTemplate_LoadsUserFromRepository(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithActingUser));
|
||||
var user = Substitute.For<User>();
|
||||
user.Email = "test@example.com";
|
||||
user.Name = "Test";
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(Arg.Any<Guid>()).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<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IUserRepository>().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>();
|
||||
organization.Name = "Test";
|
||||
|
||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(Arg.Any<Guid>()).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<IOrganizationRepository>().Received(1).GetByIdAsync(eventMessage.OrganizationId ?? Guid.Empty);
|
||||
await sutProvider.GetDependency<IUserRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleEventAsync_UserTemplate_LoadsUserFromRepository(EventMessage eventMessage)
|
||||
{
|
||||
var sutProvider = GetSutProvider(OneConfiguration(_templateWithUser));
|
||||
var user = Substitute.For<User>();
|
||||
user.Email = "test@example.com";
|
||||
user.Name = "Test";
|
||||
|
||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(Arg.Any<Guid>()).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<IOrganizationRepository>().DidNotReceiveWithAnyArgs().GetByIdAsync(Arg.Any<Guid>());
|
||||
await sutProvider.GetDependency<IUserRepository>().Received(1).GetByIdAsync(eventMessage.UserId ?? Guid.Empty);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleManyEventsAsync_BaseTemplateNoConfigurations_DoesNothing(List<EventMessage> eventMessages)
|
||||
{
|
||||
var sutProvider = GetSutProvider(NoConfigurations());
|
||||
|
||||
await sutProvider.Sut.HandleManyEventsAsync(eventMessages);
|
||||
Assert.Empty(_integrationPublisher.ReceivedCalls());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleManyEventsAsync_BaseTemplateOneConfiguration_CallsProcessEventIntegrationAsync(List<EventMessage> 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<EventMessage> 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)));
|
||||
}
|
||||
}
|
||||
}
|
@ -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<WebhookIntegrationConfigurationDetails>()
|
||||
{
|
||||
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<IntegrationMessage<WebhookIntegrationConfigurationDetails>>(result.Message);
|
||||
|
||||
Assert.Equal(expected.Configuration, typedResult.Configuration);
|
||||
Assert.Equal(expected.RenderedTemplate, typedResult.RenderedTemplate);
|
||||
Assert.Equal(expected.IntegrationType, typedResult.IntegrationType);
|
||||
}
|
||||
|
||||
private class TestIntegrationHandler : IntegrationHandlerBase<WebhookIntegrationConfigurationDetails>
|
||||
{
|
||||
public override Task<IntegrationHandlerResult> HandleAsync(
|
||||
IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var result = new IntegrationHandlerResult(success: true, message: message);
|
||||
return Task.FromResult(result);
|
||||
}
|
||||
}
|
||||
}
|
30
test/Core.Test/AdminConsole/Services/IntegrationTypeTests.cs
Normal file
30
test/Core.Test/AdminConsole/Services/IntegrationTypeTests.cs
Normal file
@ -0,0 +1,30 @@
|
||||
using Bit.Core.Enums;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
public class IntegrationTypeTests
|
||||
{
|
||||
[Fact]
|
||||
public void ToRoutingKey_Slack_Succeeds()
|
||||
{
|
||||
Assert.Equal("slack", IntegrationType.Slack.ToRoutingKey());
|
||||
}
|
||||
[Fact]
|
||||
public void ToRoutingKey_Webhook_Succeeds()
|
||||
{
|
||||
Assert.Equal("webhook", IntegrationType.Webhook.ToRoutingKey());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToRoutingKey_CloudBillingSync_ThrowsException()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => IntegrationType.CloudBillingSync.ToRoutingKey());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToRoutingKey_Scim_ThrowsException()
|
||||
{
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => IntegrationType.Scim.ToRoutingKey());
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||
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 SlackIntegrationHandlerTests
|
||||
{
|
||||
private readonly ISlackService _slackService = Substitute.For<ISlackService>();
|
||||
private readonly string _channelId = "C12345";
|
||||
private readonly string _token = "xoxb-test-token";
|
||||
|
||||
private SutProvider<SlackIntegrationHandler> GetSutProvider()
|
||||
{
|
||||
return new SutProvider<SlackIntegrationHandler>()
|
||||
.SetDependency(_slackService)
|
||||
.Create();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessage<SlackIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new SlackIntegrationConfigurationDetails(_channelId, _token);
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(result.Message, message);
|
||||
|
||||
await sutProvider.GetDependency<ISlackService>().Received(1).SendSlackMessageByChannelIdAsync(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_token)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(message.RenderedTemplate)),
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(_channelId))
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,139 @@
|
||||
using System.Net;
|
||||
using Bit.Core.AdminConsole.Models.Data.Integrations;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Bit.Test.Common.Helpers;
|
||||
using Bit.Test.Common.MockedHttpClient;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Services;
|
||||
|
||||
[SutProviderCustomize]
|
||||
public class WebhookIntegrationHandlerTests
|
||||
{
|
||||
private readonly MockedHttpMessageHandler _handler;
|
||||
private readonly HttpClient _httpClient;
|
||||
private const string _webhookUrl = "http://localhost/test/event";
|
||||
|
||||
public WebhookIntegrationHandlerTests()
|
||||
{
|
||||
_handler = new MockedHttpMessageHandler();
|
||||
_handler.Fallback
|
||||
.WithStatusCode(HttpStatusCode.OK)
|
||||
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
|
||||
_httpClient = _handler.ToHttpClient();
|
||||
}
|
||||
|
||||
private SutProvider<WebhookIntegrationHandler> GetSutProvider()
|
||||
{
|
||||
var clientFactory = Substitute.For<IHttpClientFactory>();
|
||||
clientFactory.CreateClient(WebhookIntegrationHandler.HttpClientName).Returns(_httpClient);
|
||||
|
||||
return new SutProvider<WebhookIntegrationHandler>()
|
||||
.SetDependency(clientFactory)
|
||||
.Create();
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_SuccessfulRequest_ReturnsSuccess(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.True(result.Success);
|
||||
Assert.Equal(result.Message, message);
|
||||
|
||||
sutProvider.GetDependency<IHttpClientFactory>().Received(1).CreateClient(
|
||||
Arg.Is(AssertHelper.AssertPropertyEqual(WebhookIntegrationHandler.HttpClientName))
|
||||
);
|
||||
|
||||
Assert.Single(_handler.CapturedRequests);
|
||||
var request = _handler.CapturedRequests[0];
|
||||
Assert.NotNull(request);
|
||||
var returned = await request.Content.ReadAsStringAsync();
|
||||
|
||||
Assert.Equal(HttpMethod.Post, request.Method);
|
||||
Assert.Equal(_webhookUrl, request.RequestUri.ToString());
|
||||
AssertHelper.AssertPropertyEqual(message.RenderedTemplate, returned);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_TooManyRequests_ReturnsFailureSetsNotBeforUtc(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
|
||||
|
||||
_handler.Fallback
|
||||
.WithStatusCode(HttpStatusCode.TooManyRequests)
|
||||
.WithHeader("Retry-After", "60")
|
||||
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.True(result.Retryable);
|
||||
Assert.Equal(result.Message, message);
|
||||
Assert.True(result.DelayUntilDate.HasValue);
|
||||
Assert.InRange(result.DelayUntilDate.Value, DateTime.UtcNow.AddSeconds(59), DateTime.UtcNow.AddSeconds(61));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_TooManyRequestsWithDate_ReturnsFailureSetsNotBeforUtc(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
|
||||
|
||||
_handler.Fallback
|
||||
.WithStatusCode(HttpStatusCode.TooManyRequests)
|
||||
.WithHeader("Retry-After", DateTime.UtcNow.AddSeconds(60).ToString("r")) // "r" is the round-trip format: RFC1123
|
||||
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.True(result.Retryable);
|
||||
Assert.Equal(result.Message, message);
|
||||
Assert.True(result.DelayUntilDate.HasValue);
|
||||
Assert.InRange(result.DelayUntilDate.Value, DateTime.UtcNow.AddSeconds(59), DateTime.UtcNow.AddSeconds(61));
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_InternalServerError_ReturnsFailureSetsRetryable(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
|
||||
|
||||
_handler.Fallback
|
||||
.WithStatusCode(HttpStatusCode.InternalServerError)
|
||||
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.True(result.Retryable);
|
||||
Assert.Equal(result.Message, message);
|
||||
Assert.False(result.DelayUntilDate.HasValue);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task HandleAsync_UnexpectedRedirect_ReturnsFailureNotRetryable(IntegrationMessage<WebhookIntegrationConfigurationDetails> message)
|
||||
{
|
||||
var sutProvider = GetSutProvider();
|
||||
message.Configuration = new WebhookIntegrationConfigurationDetails(_webhookUrl);
|
||||
|
||||
_handler.Fallback
|
||||
.WithStatusCode(HttpStatusCode.TemporaryRedirect)
|
||||
.WithContent(new StringContent("<html><head><title>test</title></head><body>test</body></html>"));
|
||||
|
||||
var result = await sutProvider.Sut.HandleAsync(message);
|
||||
|
||||
Assert.False(result.Success);
|
||||
Assert.False(result.Retryable);
|
||||
Assert.Equal(result.Message, message);
|
||||
Assert.Null(result.DelayUntilDate);
|
||||
}
|
||||
}
|
@ -1,4 +1,6 @@
|
||||
using Bit.Core.AdminConsole.Utilities;
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Utilities;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using Xunit;
|
||||
@ -76,18 +78,6 @@ public class IntegrationTemplateProcessorTests
|
||||
var expectedEmpty = "";
|
||||
|
||||
Assert.Equal(expectedEmpty, IntegrationTemplateProcessor.ReplaceTokens(emptyTemplate, eventMessage));
|
||||
Assert.Null(IntegrationTemplateProcessor.ReplaceTokens(null, eventMessage));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReplaceTokens_DataObjectIsNull_ReturnsOriginalString()
|
||||
{
|
||||
var template = "Event #Type#, User (id: #UserId#).";
|
||||
var expected = "Event #Type#, User (id: #UserId#).";
|
||||
|
||||
var result = IntegrationTemplateProcessor.ReplaceTokens(template, null);
|
||||
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
@ -49,7 +49,7 @@ public class ImportCiphersAsyncCommandTests
|
||||
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
|
||||
|
||||
// Assert
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(ciphers, Arg.Any<List<Folder>>());
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(importingUserId, ciphers, Arg.Any<List<Folder>>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||
}
|
||||
|
||||
@ -77,7 +77,7 @@ public class ImportCiphersAsyncCommandTests
|
||||
|
||||
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
|
||||
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(ciphers, Arg.Any<List<Folder>>());
|
||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(importingUserId, ciphers, Arg.Any<List<Folder>>());
|
||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||
}
|
||||
|
||||
|
135
test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs
Normal file
135
test/Core.Test/Tools/Services/SendAuthenticationQueryTests.cs
Normal file
@ -0,0 +1,135 @@
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Tools.Models.Data;
|
||||
using Bit.Core.Tools.Repositories;
|
||||
using Bit.Core.Tools.SendFeatures.Queries;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Core.Test.Tools.Services;
|
||||
|
||||
public class SendAuthenticationQueryTests
|
||||
{
|
||||
private readonly ISendRepository _sendRepository;
|
||||
private readonly SendAuthenticationQuery _sendAuthenticationQuery;
|
||||
|
||||
public SendAuthenticationQueryTests()
|
||||
{
|
||||
_sendRepository = Substitute.For<ISendRepository>();
|
||||
_sendAuthenticationQuery = new SendAuthenticationQuery(_sendRepository);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_WithNullRepository_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
var exception = Assert.Throws<ArgumentNullException>(() => new SendAuthenticationQuery(null));
|
||||
Assert.Equal("sendRepository", exception.ParamName);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(AuthenticationMethodTestCases))]
|
||||
public async Task GetAuthenticationMethod_ReturnsExpectedAuthenticationMethod(Send? send, Type expectedType)
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||
|
||||
// Act
|
||||
var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);
|
||||
|
||||
// Assert
|
||||
Assert.IsType(expectedType, result);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(EmailParsingTestCases))]
|
||||
public async Task GetAuthenticationMethod_WithEmails_ParsesEmailsCorrectly(string emailString, string[] expectedEmails)
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: emailString, password: null);
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||
|
||||
// Act
|
||||
var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);
|
||||
|
||||
// Assert
|
||||
var emailOtp = Assert.IsType<EmailOtp>(result);
|
||||
Assert.Equal(expectedEmails, emailOtp.Emails);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAuthenticationMethod_WithBothEmailsAndPassword_ReturnsEmailOtp()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: "test@example.com", password: "hashedpassword");
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||
|
||||
// Act
|
||||
var result = await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<EmailOtp>(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAuthenticationMethod_CallsRepositoryWithCorrectSendId()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var send = CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: null);
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(send);
|
||||
|
||||
// Act
|
||||
await _sendAuthenticationQuery.GetAuthenticationMethod(sendId);
|
||||
|
||||
// Assert
|
||||
await _sendRepository.Received(1).GetByIdAsync(sendId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetAuthenticationMethod_WhenRepositoryThrows_PropagatesException()
|
||||
{
|
||||
// Arrange
|
||||
var sendId = Guid.NewGuid();
|
||||
var expectedException = new InvalidOperationException("Repository error");
|
||||
_sendRepository.GetByIdAsync(sendId).Returns(Task.FromException<Send?>(expectedException));
|
||||
|
||||
// Act & Assert
|
||||
var exception = await Assert.ThrowsAsync<InvalidOperationException>(() =>
|
||||
_sendAuthenticationQuery.GetAuthenticationMethod(sendId));
|
||||
Assert.Same(expectedException, exception);
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> AuthenticationMethodTestCases()
|
||||
{
|
||||
yield return new object[] { null, typeof(NeverAuthenticate) };
|
||||
yield return new object[] { CreateSend(accessCount: 5, maxAccessCount: 5, emails: null, password: null), typeof(NeverAuthenticate) };
|
||||
yield return new object[] { CreateSend(accessCount: 6, maxAccessCount: 5, emails: null, password: null), typeof(NeverAuthenticate) };
|
||||
yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: "test@example.com", password: null), typeof(EmailOtp) };
|
||||
yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: "hashedpassword"), typeof(ResourcePassword) };
|
||||
yield return new object[] { CreateSend(accessCount: 0, maxAccessCount: 10, emails: null, password: null), typeof(NotAuthenticated) };
|
||||
}
|
||||
|
||||
public static IEnumerable<object[]> EmailParsingTestCases()
|
||||
{
|
||||
yield return new object[] { "test@example.com", new[] { "test@example.com" } };
|
||||
yield return new object[] { "test1@example.com,test2@example.com", new[] { "test1@example.com", "test2@example.com" } };
|
||||
yield return new object[] { " test@example.com , other@example.com ", new[] { "test@example.com", "other@example.com" } };
|
||||
yield return new object[] { "test@example.com,,other@example.com", new[] { "test@example.com", "other@example.com" } };
|
||||
yield return new object[] { " , test@example.com, ,other@example.com, ", new[] { "test@example.com", "other@example.com" } };
|
||||
}
|
||||
|
||||
private static Send CreateSend(int accessCount, int? maxAccessCount, string? emails, string? password)
|
||||
{
|
||||
return new Send
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
AccessCount = accessCount,
|
||||
MaxAccessCount = maxAccessCount,
|
||||
Emails = emails,
|
||||
Password = password
|
||||
};
|
||||
}
|
||||
}
|
@ -28,4 +28,31 @@ public class StaticStoreTests
|
||||
Assert.NotNull(plan);
|
||||
Assert.Equal(planType, plan.Type);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StaticStore_GlobalEquivalentDomains_OnlyAsciiAllowed()
|
||||
{
|
||||
// Ref: https://daniel.haxx.se/blog/2025/05/16/detecting-malicious-unicode/
|
||||
// URLs can contain unicode characters that to a computer would point to completely seperate domains but to the
|
||||
// naked eye look completely identical. For example 'g' and 'ց' look incredibly similar but when included in a
|
||||
// URL would lead you somewhere different. There is an opening for an attacker to contribute to Bitwarden with a
|
||||
// url update that could be missed in code review and then if they got a user to that URL Bitwarden could
|
||||
// consider it equivalent with a cipher in the users vault and offer autofill when we should not.
|
||||
// GitHub does now show a warning on non-ascii characters but it could still be missed.
|
||||
// https://github.blog/changelog/2025-05-01-github-now-provides-a-warning-about-hidden-unicode-text/
|
||||
|
||||
// To defend against this:
|
||||
// Loop through all equivalent domains and fail if any contain a non-ascii character
|
||||
// non-ascii character can make a valid URL so it's possible that in the future we have a domain
|
||||
// we want to allow list, that should be done through `continue`ing in the below foreach loop
|
||||
// only if the domain strictly equals (do NOT use InvariantCulture comparison) the one added to our allow list.
|
||||
foreach (var domain in StaticStore.GlobalDomains.SelectMany(p => p.Value))
|
||||
{
|
||||
for (var i = 0; i < domain.Length; i++)
|
||||
{
|
||||
var character = domain[i];
|
||||
Assert.True(char.IsAscii(character), $"Domain: {domain} contains non-ASCII character: '{character}' at index: {i}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ using Bit.Core.Test.AutoFixture.Attributes;
|
||||
using Bit.Infrastructure.EFIntegration.Test.AutoFixture;
|
||||
using Bit.Infrastructure.EFIntegration.Test.Repositories.EqualityComparers;
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
|
||||
using Xunit;
|
||||
using EfRepo = Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Organization = Bit.Core.AdminConsole.Entities.Organization;
|
||||
@ -161,7 +162,7 @@ public class OrganizationRepositoryTests
|
||||
|
||||
[CiSkippedTheory, EfOrganizationUserAutoData]
|
||||
public async Task SearchUnassignedAsync_Works(OrganizationUser orgUser, User user, Organization org,
|
||||
List<EfRepo.OrganizationUserRepository> efOrgUserRepos, List<EfRepo.OrganizationRepository> efOrgRepos, List<EfRepo.UserRepository> efUserRepos,
|
||||
List<OrganizationUserRepository> efOrgUserRepos, List<EfRepo.OrganizationRepository> efOrgRepos, List<EfRepo.UserRepository> efUserRepos,
|
||||
SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.OrganizationRepository sqlOrgRepo, SqlRepo.UserRepository sqlUserRepo)
|
||||
{
|
||||
orgUser.Type = OrganizationUserType.Owner;
|
||||
|
@ -24,7 +24,7 @@ public class OrganizationUserRepositoryTests
|
||||
{
|
||||
[CiSkippedTheory, EfOrganizationUserAutoData]
|
||||
public async Task CreateAsync_Works_DataMatches(OrganizationUser orgUser, User user, Organization org,
|
||||
OrganizationUserCompare equalityComparer, List<EfRepo.OrganizationUserRepository> suts,
|
||||
OrganizationUserCompare equalityComparer, List<EfAdminConsoleRepo.OrganizationUserRepository> suts,
|
||||
List<EfRepo.OrganizationRepository> efOrgRepos, List<EfRepo.UserRepository> efUserRepos,
|
||||
SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.UserRepository sqlUserRepo,
|
||||
SqlRepo.OrganizationRepository sqlOrgRepo)
|
||||
@ -67,7 +67,7 @@ public class OrganizationUserRepositoryTests
|
||||
User user,
|
||||
Organization org,
|
||||
OrganizationUserCompare equalityComparer,
|
||||
List<EfRepo.OrganizationUserRepository> suts,
|
||||
List<EfAdminConsoleRepo.OrganizationUserRepository> suts,
|
||||
List<EfRepo.UserRepository> efUserRepos,
|
||||
List<EfRepo.OrganizationRepository> efOrgRepos,
|
||||
SqlRepo.OrganizationUserRepository sqlOrgUserRepo,
|
||||
@ -113,7 +113,7 @@ public class OrganizationUserRepositoryTests
|
||||
}
|
||||
|
||||
[CiSkippedTheory, EfOrganizationUserAutoData]
|
||||
public async Task DeleteAsync_Works_DataMatches(OrganizationUser orgUser, User user, Organization org, List<EfRepo.OrganizationUserRepository> suts,
|
||||
public async Task DeleteAsync_Works_DataMatches(OrganizationUser orgUser, User user, Organization org, List<EfAdminConsoleRepo.OrganizationUserRepository> suts,
|
||||
List<EfRepo.UserRepository> efUserRepos, List<EfRepo.OrganizationRepository> efOrgRepos,
|
||||
SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.UserRepository sqlUserRepo,
|
||||
SqlRepo.OrganizationRepository sqlOrgRepo)
|
||||
@ -188,7 +188,7 @@ public class OrganizationUserRepositoryTests
|
||||
List<EfAdminConsoleRepo.PolicyRepository> efPolicyRepository,
|
||||
List<EfRepo.UserRepository> efUserRepository,
|
||||
List<EfRepo.OrganizationRepository> efOrganizationRepository,
|
||||
List<EfRepo.OrganizationUserRepository> suts,
|
||||
List<EfAdminConsoleRepo.OrganizationUserRepository> suts,
|
||||
List<EfAdminConsoleRepo.ProviderRepository> efProviderRepository,
|
||||
List<EfAdminConsoleRepo.ProviderOrganizationRepository> efProviderOrganizationRepository,
|
||||
List<EfAdminConsoleRepo.ProviderUserRepository> efProviderUserRepository,
|
||||
|
@ -7,6 +7,7 @@ using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||
using Bit.Core.Test.AutoFixture.UserFixtures;
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
|
@ -5,6 +5,7 @@ using Bit.Core.Test.AutoFixture.UserFixtures;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
using Bit.Infrastructure.EFIntegration.Test.AutoFixture.Relays;
|
||||
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using Bit.Infrastructure.EntityFramework.Vault.Repositories;
|
||||
using Bit.Test.Common.AutoFixture;
|
||||
|
@ -9,6 +9,7 @@ using Bit.Infrastructure.EntityFramework.Repositories.Queries;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
using LinqToDB;
|
||||
using Xunit;
|
||||
using EfAdminConsoleRepo = Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
|
||||
using EfRepo = Bit.Infrastructure.EntityFramework.Repositories;
|
||||
using EfVaultRepo = Bit.Infrastructure.EntityFramework.Vault.Repositories;
|
||||
using SqlRepo = Bit.Infrastructure.Dapper.Repositories;
|
||||
@ -112,7 +113,7 @@ public class CipherRepositoryTests
|
||||
[CiSkippedTheory, EfOrganizationCipherCustomize, BitAutoData]
|
||||
public async Task CreateAsync_BumpsOrgUserAccountRevisionDates(Cipher cipher, List<User> users,
|
||||
List<OrganizationUser> orgUsers, Collection collection, Organization org, List<EfVaultRepo.CipherRepository> suts, List<EfRepo.UserRepository> efUserRepos, List<EfRepo.OrganizationRepository> efOrgRepos,
|
||||
List<EfRepo.OrganizationUserRepository> efOrgUserRepos, List<EfRepo.CollectionRepository> efCollectionRepos)
|
||||
List<EfAdminConsoleRepo.OrganizationUserRepository> efOrgUserRepos, List<EfRepo.CollectionRepository> efCollectionRepos)
|
||||
{
|
||||
var savedCiphers = new List<Cipher>();
|
||||
foreach (var sut in suts)
|
||||
|
@ -1,5 +1,6 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
@ -26,15 +27,23 @@ public static class OrganizationTestHelpers
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an Enterprise organization.
|
||||
/// </summary>
|
||||
public static Task<Organization> CreateTestOrganizationAsync(this IOrganizationRepository organizationRepository,
|
||||
string identifier = "test")
|
||||
=> organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = $"{identifier}-{Guid.NewGuid()}",
|
||||
BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL
|
||||
Plan = "Test", // TODO: EF does not enforce this being NOT NULl
|
||||
Plan = "Enterprise (Annually)", // TODO: EF does not enforce this being NOT NULl
|
||||
PlanType = PlanType.EnterpriseAnnually
|
||||
});
|
||||
|
||||
/// <summary>
|
||||
/// Creates a confirmed Owner for the specified organization and user.
|
||||
/// Does not include any cryptographic material.
|
||||
/// </summary>
|
||||
public static Task<OrganizationUser> CreateTestOrganizationUserAsync(
|
||||
this IOrganizationUserRepository organizationUserRepository,
|
||||
Organization organization,
|
||||
@ -47,6 +56,17 @@ public static class OrganizationTestHelpers
|
||||
Type = OrganizationUserType.Owner
|
||||
});
|
||||
|
||||
public static Task<OrganizationUser> CreateTestOrganizationUserInviteAsync(
|
||||
this IOrganizationUserRepository organizationUserRepository,
|
||||
Organization organization)
|
||||
=> organizationUserRepository.CreateAsync(new OrganizationUser
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = null, // Invites are not linked to a UserId
|
||||
Status = OrganizationUserStatusType.Invited,
|
||||
Type = OrganizationUserType.Owner
|
||||
});
|
||||
|
||||
public static Task<Group> CreateTestGroupAsync(
|
||||
this IGroupRepository groupRepository,
|
||||
Organization organization,
|
||||
@ -54,4 +74,14 @@ public static class OrganizationTestHelpers
|
||||
=> groupRepository.CreateAsync(
|
||||
new Group { OrganizationId = organization.Id, Name = $"{identifier} {Guid.NewGuid()}" }
|
||||
);
|
||||
|
||||
public static Task<Collection> CreateTestCollectionAsync(
|
||||
this ICollectionRepository collectionRepository,
|
||||
Organization organization,
|
||||
string identifier = "test")
|
||||
=> collectionRepository.CreateAsync(new Collection
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
Name = $"{identifier} {Guid.NewGuid()}"
|
||||
});
|
||||
}
|
||||
|
@ -0,0 +1,105 @@
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository;
|
||||
|
||||
public class CollectionRepositoryCreateTests
|
||||
{
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task CreateAsync_WithAccess_Works(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IGroupRepository groupRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
// Arrange
|
||||
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||
|
||||
var user1 = await userRepository.CreateTestUserAsync();
|
||||
var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user1);
|
||||
|
||||
var user2 = await userRepository.CreateTestUserAsync();
|
||||
var orgUser2 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user2);
|
||||
|
||||
var group1 = await groupRepository.CreateTestGroupAsync(organization);
|
||||
var group2 = await groupRepository.CreateTestGroupAsync(organization);
|
||||
|
||||
var collection = new Collection
|
||||
{
|
||||
Name = "Test Collection Name",
|
||||
OrganizationId = organization.Id,
|
||||
};
|
||||
|
||||
// Act
|
||||
await collectionRepository.CreateAsync(collection,
|
||||
[
|
||||
new CollectionAccessSelection { Id = group1.Id, Manage = true, HidePasswords = true, ReadOnly = false, },
|
||||
new CollectionAccessSelection { Id = group2.Id, Manage = false, HidePasswords = false, ReadOnly = true, },
|
||||
],
|
||||
[
|
||||
new CollectionAccessSelection { Id = orgUser1.Id, Manage = true, HidePasswords = false, ReadOnly = true },
|
||||
new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = true, ReadOnly = false },
|
||||
]
|
||||
);
|
||||
|
||||
// Assert
|
||||
var (actualCollection, actualAccess) = await collectionRepository.GetByIdWithAccessAsync(collection.Id);
|
||||
|
||||
Assert.NotNull(actualCollection);
|
||||
Assert.Equal("Test Collection Name", actualCollection.Name);
|
||||
|
||||
var groups = actualAccess.Groups.ToArray();
|
||||
Assert.Equal(2, groups.Length);
|
||||
Assert.Single(groups, g => g.Id == group1.Id && g.Manage && g.HidePasswords && !g.ReadOnly);
|
||||
Assert.Single(groups, g => g.Id == group2.Id && !g.Manage && !g.HidePasswords && g.ReadOnly);
|
||||
|
||||
var users = actualAccess.Users.ToArray();
|
||||
Assert.Equal(2, users.Length);
|
||||
Assert.Single(users, u => u.Id == orgUser1.Id && u.Manage && !u.HidePasswords && u.ReadOnly);
|
||||
Assert.Single(users, u => u.Id == orgUser2.Id && !u.Manage && u.HidePasswords && !u.ReadOnly);
|
||||
|
||||
// Clean up data
|
||||
await userRepository.DeleteAsync(user1);
|
||||
await userRepository.DeleteAsync(user2);
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
await groupRepository.DeleteManyAsync([group1.Id, group2.Id]);
|
||||
await organizationUserRepository.DeleteManyAsync([orgUser1.Id, orgUser2.Id]);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Makes sure that the sproc handles empty sets.
|
||||
/// </remarks>
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task CreateAsync_WithNoAccess_Works(
|
||||
IOrganizationRepository organizationRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
// Arrange
|
||||
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||
|
||||
var collection = new Collection
|
||||
{
|
||||
Name = "Test Collection Name",
|
||||
OrganizationId = organization.Id,
|
||||
};
|
||||
|
||||
// Act
|
||||
await collectionRepository.CreateAsync(collection, [], []);
|
||||
|
||||
// Assert
|
||||
var (actualCollection, actualAccess) = await collectionRepository.GetByIdWithAccessAsync(collection.Id);
|
||||
|
||||
Assert.NotNull(actualCollection);
|
||||
Assert.Equal("Test Collection Name", actualCollection.Name);
|
||||
|
||||
Assert.Empty(actualAccess.Groups);
|
||||
Assert.Empty(actualAccess.Users);
|
||||
|
||||
// Clean up
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
}
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository;
|
||||
|
||||
public class CollectionRepositoryReplaceTests
|
||||
{
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task ReplaceAsync_WithAccess_Works(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IGroupRepository groupRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
// Arrange
|
||||
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||
|
||||
var user1 = await userRepository.CreateTestUserAsync();
|
||||
var orgUser1 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user1);
|
||||
|
||||
var user2 = await userRepository.CreateTestUserAsync();
|
||||
var orgUser2 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user2);
|
||||
|
||||
var user3 = await userRepository.CreateTestUserAsync();
|
||||
var orgUser3 = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user3);
|
||||
|
||||
var group1 = await groupRepository.CreateTestGroupAsync(organization);
|
||||
var group2 = await groupRepository.CreateTestGroupAsync(organization);
|
||||
var group3 = await groupRepository.CreateTestGroupAsync(organization);
|
||||
|
||||
var collection = new Collection
|
||||
{
|
||||
Name = "Test Collection Name",
|
||||
OrganizationId = organization.Id,
|
||||
};
|
||||
|
||||
await collectionRepository.CreateAsync(collection,
|
||||
[
|
||||
new CollectionAccessSelection { Id = group1.Id, Manage = true, HidePasswords = true, ReadOnly = false, },
|
||||
new CollectionAccessSelection { Id = group2.Id, Manage = false, HidePasswords = false, ReadOnly = true, },
|
||||
],
|
||||
[
|
||||
new CollectionAccessSelection { Id = orgUser1.Id, Manage = true, HidePasswords = false, ReadOnly = true },
|
||||
new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = true, ReadOnly = false },
|
||||
]
|
||||
);
|
||||
|
||||
// Act
|
||||
collection.Name = "Updated Collection Name";
|
||||
|
||||
await collectionRepository.ReplaceAsync(collection,
|
||||
[
|
||||
// Delete group1
|
||||
// Update group2:
|
||||
new CollectionAccessSelection { Id = group2.Id, Manage = true, HidePasswords = true, ReadOnly = false, },
|
||||
// Add group3:
|
||||
new CollectionAccessSelection { Id = group3.Id, Manage = false, HidePasswords = false, ReadOnly = true, },
|
||||
],
|
||||
[
|
||||
// Delete orgUser1
|
||||
// Update orgUser2:
|
||||
new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = false, ReadOnly = true },
|
||||
// Add orgUser3:
|
||||
new CollectionAccessSelection { Id = orgUser3.Id, Manage = true, HidePasswords = false, ReadOnly = true },
|
||||
]
|
||||
);
|
||||
|
||||
// Assert
|
||||
var (actualCollection, actualAccess) = await collectionRepository.GetByIdWithAccessAsync(collection.Id);
|
||||
|
||||
Assert.NotNull(actualCollection);
|
||||
Assert.Equal("Updated Collection Name", actualCollection.Name);
|
||||
|
||||
var groups = actualAccess.Groups.ToArray();
|
||||
Assert.Equal(2, groups.Length);
|
||||
Assert.Single(groups, g => g.Id == group2.Id && g.Manage && g.HidePasswords && !g.ReadOnly);
|
||||
Assert.Single(groups, g => g.Id == group3.Id && !g.Manage && !g.HidePasswords && g.ReadOnly);
|
||||
|
||||
var users = actualAccess.Users.ToArray();
|
||||
|
||||
Assert.Equal(2, users.Length);
|
||||
Assert.Single(users, u => u.Id == orgUser2.Id && !u.Manage && !u.HidePasswords && u.ReadOnly);
|
||||
Assert.Single(users, u => u.Id == orgUser3.Id && u.Manage && !u.HidePasswords && u.ReadOnly);
|
||||
|
||||
// Clean up data
|
||||
await userRepository.DeleteAsync(user1);
|
||||
await userRepository.DeleteAsync(user2);
|
||||
await userRepository.DeleteAsync(user3);
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// Makes sure that the sproc handles empty sets.
|
||||
/// </remarks>
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task ReplaceAsync_WithNoAccess_Works(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IGroupRepository groupRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
// Arrange
|
||||
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user);
|
||||
|
||||
var group = await groupRepository.CreateTestGroupAsync(organization);
|
||||
|
||||
var collection = new Collection
|
||||
{
|
||||
Name = "Test Collection Name",
|
||||
OrganizationId = organization.Id,
|
||||
};
|
||||
|
||||
await collectionRepository.CreateAsync(collection,
|
||||
[
|
||||
new CollectionAccessSelection { Id = group.Id, Manage = true, HidePasswords = false, ReadOnly = true },
|
||||
],
|
||||
[
|
||||
new CollectionAccessSelection { Id = orgUser.Id, Manage = true, HidePasswords = false, ReadOnly = true },
|
||||
]);
|
||||
|
||||
// Act
|
||||
collection.Name = "Updated Collection Name";
|
||||
|
||||
await collectionRepository.ReplaceAsync(collection, [], []);
|
||||
|
||||
// Assert
|
||||
var (actualCollection, actualAccess) = await collectionRepository.GetByIdWithAccessAsync(collection.Id);
|
||||
|
||||
Assert.NotNull(actualCollection);
|
||||
Assert.Equal("Updated Collection Name", actualCollection.Name);
|
||||
|
||||
Assert.Empty(actualAccess.Groups);
|
||||
Assert.Empty(actualAccess.Users);
|
||||
|
||||
// Clean up
|
||||
await userRepository.DeleteAsync(user);
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@ using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Infrastructure.IntegrationTest.Repositories;
|
||||
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository;
|
||||
|
||||
public class CollectionRepositoryTests
|
||||
{
|
||||
@ -463,147 +463,4 @@ public class CollectionRepositoryTests
|
||||
Assert.False(c3.Unmanaged);
|
||||
});
|
||||
}
|
||||
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task ReplaceAsync_Works(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IGroupRepository groupRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
var user = await userRepository.CreateAsync(new User
|
||||
{
|
||||
Name = "Test User",
|
||||
Email = $"test+{Guid.NewGuid()}@email.com",
|
||||
ApiKey = "TEST",
|
||||
SecurityStamp = "stamp",
|
||||
});
|
||||
|
||||
var organization = await organizationRepository.CreateAsync(new Organization
|
||||
{
|
||||
Name = "Test Org",
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
Plan = "Test Plan",
|
||||
BillingEmail = "billing@email.com"
|
||||
});
|
||||
|
||||
var orgUser1 = await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
});
|
||||
|
||||
var orgUser2 = await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
});
|
||||
|
||||
var orgUser3 = await organizationUserRepository.CreateAsync(new OrganizationUser
|
||||
{
|
||||
OrganizationId = organization.Id,
|
||||
UserId = user.Id,
|
||||
Status = OrganizationUserStatusType.Confirmed,
|
||||
});
|
||||
|
||||
var group1 = await groupRepository.CreateAsync(new Group
|
||||
{
|
||||
Name = "Test Group #1",
|
||||
OrganizationId = organization.Id,
|
||||
});
|
||||
|
||||
var group2 = await groupRepository.CreateAsync(new Group
|
||||
{
|
||||
Name = "Test Group #2",
|
||||
OrganizationId = organization.Id,
|
||||
});
|
||||
|
||||
var group3 = await groupRepository.CreateAsync(new Group
|
||||
{
|
||||
Name = "Test Group #3",
|
||||
OrganizationId = organization.Id,
|
||||
});
|
||||
|
||||
var collection = new Collection
|
||||
{
|
||||
Name = "Test Collection Name",
|
||||
OrganizationId = organization.Id,
|
||||
};
|
||||
|
||||
await collectionRepository.CreateAsync(collection,
|
||||
[
|
||||
new CollectionAccessSelection { Id = group1.Id, Manage = true, HidePasswords = true, ReadOnly = false, },
|
||||
new CollectionAccessSelection { Id = group2.Id, Manage = false, HidePasswords = false, ReadOnly = true, },
|
||||
],
|
||||
[
|
||||
new CollectionAccessSelection { Id = orgUser1.Id, Manage = true, HidePasswords = false, ReadOnly = true },
|
||||
new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = true, ReadOnly = false },
|
||||
]
|
||||
);
|
||||
|
||||
collection.Name = "Updated Collection Name";
|
||||
|
||||
await collectionRepository.ReplaceAsync(collection,
|
||||
[
|
||||
// Should delete group1
|
||||
new CollectionAccessSelection { Id = group2.Id, Manage = true, HidePasswords = true, ReadOnly = false, },
|
||||
// Should add group3
|
||||
new CollectionAccessSelection { Id = group3.Id, Manage = false, HidePasswords = false, ReadOnly = true, },
|
||||
],
|
||||
[
|
||||
// Should delete orgUser1
|
||||
new CollectionAccessSelection { Id = orgUser2.Id, Manage = false, HidePasswords = false, ReadOnly = true },
|
||||
// Should add orgUser3
|
||||
new CollectionAccessSelection { Id = orgUser3.Id, Manage = true, HidePasswords = false, ReadOnly = true },
|
||||
]
|
||||
);
|
||||
|
||||
// Assert it
|
||||
var info = await collectionRepository.GetByIdWithPermissionsAsync(collection.Id, user.Id, true);
|
||||
|
||||
Assert.NotNull(info);
|
||||
|
||||
Assert.Equal("Updated Collection Name", info.Name);
|
||||
|
||||
var groups = info.Groups.ToArray();
|
||||
|
||||
Assert.Equal(2, groups.Length);
|
||||
|
||||
var actualGroup2 = Assert.Single(groups.Where(g => g.Id == group2.Id));
|
||||
|
||||
Assert.True(actualGroup2.Manage);
|
||||
Assert.True(actualGroup2.HidePasswords);
|
||||
Assert.False(actualGroup2.ReadOnly);
|
||||
|
||||
var actualGroup3 = Assert.Single(groups.Where(g => g.Id == group3.Id));
|
||||
|
||||
Assert.False(actualGroup3.Manage);
|
||||
Assert.False(actualGroup3.HidePasswords);
|
||||
Assert.True(actualGroup3.ReadOnly);
|
||||
|
||||
var users = info.Users.ToArray();
|
||||
|
||||
Assert.Equal(2, users.Length);
|
||||
|
||||
var actualOrgUser2 = Assert.Single(users.Where(u => u.Id == orgUser2.Id));
|
||||
|
||||
Assert.False(actualOrgUser2.Manage);
|
||||
Assert.False(actualOrgUser2.HidePasswords);
|
||||
Assert.True(actualOrgUser2.ReadOnly);
|
||||
|
||||
var actualOrgUser3 = Assert.Single(users.Where(u => u.Id == orgUser3.Id));
|
||||
|
||||
Assert.True(actualOrgUser3.Manage);
|
||||
Assert.False(actualOrgUser3.HidePasswords);
|
||||
Assert.True(actualOrgUser3.ReadOnly);
|
||||
|
||||
// Clean up data
|
||||
await userRepository.DeleteAsync(user);
|
||||
await organizationRepository.DeleteAsync(organization);
|
||||
await groupRepository.DeleteManyAsync([group1.Id, group2.Id, group3.Id]);
|
||||
await organizationUserRepository.DeleteManyAsync([orgUser1.Id, orgUser2.Id, orgUser3.Id]);
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.OrganizationUserRepository;
|
||||
|
||||
public class OrganizationUserReplaceTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifically tests OrganizationUsers in the invited state, which is unique because
|
||||
/// they're not linked to a UserId.
|
||||
/// </summary>
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task ReplaceAsync_WithCollectionAccess_WhenUserIsInvited_Success(
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||
|
||||
var orgUser = await organizationUserRepository.CreateTestOrganizationUserInviteAsync(organization);
|
||||
|
||||
// Act: update the user, including collection access so we test this overloaded method
|
||||
orgUser.Type = OrganizationUserType.Admin;
|
||||
orgUser.AccessSecretsManager = true;
|
||||
var collection = await collectionRepository.CreateTestCollectionAsync(organization);
|
||||
|
||||
await organizationUserRepository.ReplaceAsync(orgUser, [
|
||||
new CollectionAccessSelection { Id = collection.Id, Manage = true }
|
||||
]);
|
||||
|
||||
// Assert
|
||||
var (actualOrgUser, actualCollections) = await organizationUserRepository.GetByIdWithCollectionsAsync(orgUser.Id);
|
||||
Assert.NotNull(actualOrgUser);
|
||||
Assert.Equal(OrganizationUserType.Admin, actualOrgUser.Type);
|
||||
Assert.True(actualOrgUser.AccessSecretsManager);
|
||||
|
||||
var collectionAccess = Assert.Single(actualCollections);
|
||||
Assert.Equal(collection.Id, collectionAccess.Id);
|
||||
Assert.True(collectionAccess.Manage);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests OrganizationUsers in the Confirmed status, which is a stand-in for all other
|
||||
/// non-Invited statuses (which are all linked to a UserId).
|
||||
/// </summary>
|
||||
/// <param name="organizationRepository"></param>
|
||||
/// <param name="organizationUserRepository"></param>
|
||||
/// <param name="collectionRepository"></param>
|
||||
[DatabaseTheory, DatabaseData]
|
||||
public async Task ReplaceAsync_WithCollectionAccess_WhenUserIsConfirmed_Success(
|
||||
IUserRepository userRepository,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
ICollectionRepository collectionRepository)
|
||||
{
|
||||
var organization = await organizationRepository.CreateTestOrganizationAsync();
|
||||
|
||||
var user = await userRepository.CreateTestUserAsync();
|
||||
// OrganizationUser is linked with the User in the Confirmed status
|
||||
var orgUser = await organizationUserRepository.CreateTestOrganizationUserAsync(organization, user);
|
||||
|
||||
// Act: update the user, including collection access so we test this overloaded method
|
||||
orgUser.Type = OrganizationUserType.Admin;
|
||||
orgUser.AccessSecretsManager = true;
|
||||
var collection = await collectionRepository.CreateTestCollectionAsync(organization);
|
||||
|
||||
await organizationUserRepository.ReplaceAsync(orgUser, [
|
||||
new CollectionAccessSelection { Id = collection.Id, Manage = true }
|
||||
]);
|
||||
|
||||
// Assert
|
||||
var (actualOrgUser, actualCollections) = await organizationUserRepository.GetByIdWithCollectionsAsync(orgUser.Id);
|
||||
Assert.NotNull(actualOrgUser);
|
||||
Assert.Equal(OrganizationUserType.Admin, actualOrgUser.Type);
|
||||
Assert.True(actualOrgUser.AccessSecretsManager);
|
||||
|
||||
var collectionAccess = Assert.Single(actualCollections);
|
||||
Assert.Equal(collection.Id, collectionAccess.Id);
|
||||
Assert.True(collectionAccess.Manage);
|
||||
|
||||
// Account revision date should be updated to a later date
|
||||
var actualUser = await userRepository.GetByIdAsync(user.Id);
|
||||
Assert.NotNull(actualUser);
|
||||
Assert.True(actualUser.AccountRevisionDate.CompareTo(user.AccountRevisionDate) > 0);
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ using Bit.Core.Repositories;
|
||||
using Bit.Core.Utilities;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories;
|
||||
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.OrganizationUserRepository;
|
||||
|
||||
public class OrganizationUserRepositoryTests
|
||||
{
|
139
util/Migrator/DbScripts/2025-05-20_00_AddSendEmails.sql
Normal file
139
util/Migrator/DbScripts/2025-05-20_00_AddSendEmails.sql
Normal file
@ -0,0 +1,139 @@
|
||||
-- Add `Emails` field that stores a comma-separated list of email addresses for
|
||||
-- email/OTP authentication to table and write methods. The read methods
|
||||
-- don't need to be updated because they all use `*`.
|
||||
IF NOT EXISTS(
|
||||
SELECT *
|
||||
FROM [sys].[columns]
|
||||
WHERE [object_id] = OBJECT_ID(N'[dbo].[Send]')
|
||||
AND [name] = 'Emails')
|
||||
BEGIN
|
||||
ALTER TABLE [dbo].[Send] ADD [Emails] NVARCHAR(1024) NULL;
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[Send_Update]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@Type TINYINT,
|
||||
@Data VARCHAR(MAX),
|
||||
@Key VARCHAR(MAX),
|
||||
@Password NVARCHAR(300),
|
||||
@MaxAccessCount INT,
|
||||
@AccessCount INT,
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7),
|
||||
@ExpirationDate DATETIME2(7),
|
||||
@DeletionDate DATETIME2(7),
|
||||
@Disabled BIT,
|
||||
@HideEmail BIT,
|
||||
@CipherId UNIQUEIDENTIFIER = NULL,
|
||||
@Emails NVARCHAR(1024) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
UPDATE
|
||||
[dbo].[Send]
|
||||
SET
|
||||
[UserId] = @UserId,
|
||||
[OrganizationId] = @OrganizationId,
|
||||
[Type] = @Type,
|
||||
[Data] = @Data,
|
||||
[Key] = @Key,
|
||||
[Password] = @Password,
|
||||
[MaxAccessCount] = @MaxAccessCount,
|
||||
[AccessCount] = @AccessCount,
|
||||
[CreationDate] = @CreationDate,
|
||||
[RevisionDate] = @RevisionDate,
|
||||
[ExpirationDate] = @ExpirationDate,
|
||||
[DeletionDate] = @DeletionDate,
|
||||
[Disabled] = @Disabled,
|
||||
[HideEmail] = @HideEmail,
|
||||
[CipherId] = @CipherId,
|
||||
[Emails] = @Emails
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
|
||||
IF @UserId IS NOT NULL
|
||||
BEGIN
|
||||
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
|
||||
END
|
||||
-- TODO: OrganizationId bump?
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[Send_Create]
|
||||
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@Type TINYINT,
|
||||
@Data VARCHAR(MAX),
|
||||
@Key VARCHAR(MAX),
|
||||
@Password NVARCHAR(300),
|
||||
@MaxAccessCount INT,
|
||||
@AccessCount INT,
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7),
|
||||
@ExpirationDate DATETIME2(7),
|
||||
@DeletionDate DATETIME2(7),
|
||||
@Disabled BIT,
|
||||
@HideEmail BIT,
|
||||
@CipherId UNIQUEIDENTIFIER = NULL,
|
||||
@Emails NVARCHAR(1024) = NULL
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
INSERT INTO [dbo].[Send]
|
||||
(
|
||||
[Id],
|
||||
[UserId],
|
||||
[OrganizationId],
|
||||
[Type],
|
||||
[Data],
|
||||
[Key],
|
||||
[Password],
|
||||
[MaxAccessCount],
|
||||
[AccessCount],
|
||||
[CreationDate],
|
||||
[RevisionDate],
|
||||
[ExpirationDate],
|
||||
[DeletionDate],
|
||||
[Disabled],
|
||||
[HideEmail],
|
||||
[CipherId],
|
||||
[Emails]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@Id,
|
||||
@UserId,
|
||||
@OrganizationId,
|
||||
@Type,
|
||||
@Data,
|
||||
@Key,
|
||||
@Password,
|
||||
@MaxAccessCount,
|
||||
@AccessCount,
|
||||
@CreationDate,
|
||||
@RevisionDate,
|
||||
@ExpirationDate,
|
||||
@DeletionDate,
|
||||
@Disabled,
|
||||
@HideEmail,
|
||||
@CipherId,
|
||||
@Emails
|
||||
)
|
||||
|
||||
IF @UserId IS NOT NULL
|
||||
BEGIN
|
||||
IF @Type = 1 --File
|
||||
BEGIN
|
||||
EXEC [dbo].[User_UpdateStorage] @UserId
|
||||
END
|
||||
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
|
||||
END
|
||||
-- TODO: OrganizationId bump?
|
||||
END
|
||||
GO
|
67
util/Migrator/DbScripts/2025-05-27_00_SsoExternalId.sql
Normal file
67
util/Migrator/DbScripts/2025-05-27_00_SsoExternalId.sql
Normal file
@ -0,0 +1,67 @@
|
||||
IF EXISTS (
|
||||
SELECT 1
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'dbo'
|
||||
AND TABLE_NAME = 'SsoUser'
|
||||
AND COLUMN_NAME = 'ExternalId'
|
||||
AND DATA_TYPE = 'nvarchar'
|
||||
AND CHARACTER_MAXIMUM_LENGTH < 300
|
||||
)
|
||||
BEGIN
|
||||
-- Update table ExternalId column size
|
||||
ALTER TABLE [dbo].[SsoUser]
|
||||
ALTER COLUMN [ExternalId] NVARCHAR(300) NOT NULL
|
||||
END
|
||||
GO
|
||||
|
||||
-- Update stored procedures to handle the new ExternalId column size
|
||||
CREATE OR ALTER PROCEDURE [dbo].[SsoUser_Create]
|
||||
@Id BIGINT OUTPUT,
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@ExternalId NVARCHAR(300),
|
||||
@CreationDate DATETIME2(7)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
INSERT INTO [dbo].[SsoUser]
|
||||
(
|
||||
[UserId],
|
||||
[OrganizationId],
|
||||
[ExternalId],
|
||||
[CreationDate]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@UserId,
|
||||
@OrganizationId,
|
||||
@ExternalId,
|
||||
@CreationDate
|
||||
)
|
||||
|
||||
SET @Id = SCOPE_IDENTITY();
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[SsoUser_Update]
|
||||
@Id BIGINT OUTPUT,
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@ExternalId NVARCHAR(300),
|
||||
@CreationDate DATETIME2(7)
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
UPDATE
|
||||
[dbo].[SsoUser]
|
||||
SET
|
||||
[UserId] = @UserId,
|
||||
[OrganizationId] = @OrganizationId,
|
||||
[ExternalId] = @ExternalId,
|
||||
[CreationDate] = @CreationDate
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
||||
GO
|
3112
util/MySqlMigrations/Migrations/20250429113731_SsoExternalId.Designer.cs
generated
Normal file
3112
util/MySqlMigrations/Migrations/20250429113731_SsoExternalId.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,43 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.MySqlMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class SsoExternalId : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ExternalId",
|
||||
table: "SsoUser",
|
||||
type: "varchar(300)",
|
||||
maxLength: 300,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "varchar(50)",
|
||||
oldMaxLength: 50,
|
||||
oldNullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ExternalId",
|
||||
table: "SsoUser",
|
||||
type: "varchar(50)",
|
||||
maxLength: 50,
|
||||
nullable: true,
|
||||
oldClrType: typeof(string),
|
||||
oldType: "varchar(300)",
|
||||
oldMaxLength: 300,
|
||||
oldNullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4")
|
||||
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
}
|
3119
util/MySqlMigrations/Migrations/20250522205018_2025-05-20_00_AddSendEmails.Designer.cs
generated
Normal file
3119
util/MySqlMigrations/Migrations/20250522205018_2025-05-20_00_AddSendEmails.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,29 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.MySqlMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class _20250520_00_AddSendEmails : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Emails",
|
||||
table: "Send",
|
||||
type: "varchar(1024)",
|
||||
maxLength: 1024,
|
||||
nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Emails",
|
||||
table: "Send");
|
||||
}
|
||||
}
|
@ -679,8 +679,8 @@ namespace Bit.MySqlMigrations.Migrations
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
b.Property<string>("ExternalId")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("varchar(50)");
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("varchar(300)");
|
||||
|
||||
b.Property<Guid?>("OrganizationId")
|
||||
.HasColumnType("char(36)");
|
||||
@ -1437,6 +1437,10 @@ namespace Bit.MySqlMigrations.Migrations
|
||||
b.Property<bool>("Disabled")
|
||||
.HasColumnType("tinyint(1)");
|
||||
|
||||
b.Property<string>("Emails")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("varchar(1024)");
|
||||
|
||||
b.Property<DateTime?>("ExpirationDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
|
3118
util/PostgresMigrations/Migrations/20250429113739_SsoExternalId.Designer.cs
generated
Normal file
3118
util/PostgresMigrations/Migrations/20250429113739_SsoExternalId.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,43 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.PostgresMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class SsoExternalId : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ExternalId",
|
||||
table: "SsoUser",
|
||||
type: "character varying(300)",
|
||||
maxLength: 300,
|
||||
nullable: true,
|
||||
collation: "postgresIndetermanisticCollation",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(50)",
|
||||
oldMaxLength: 50,
|
||||
oldNullable: true,
|
||||
oldCollation: "postgresIndetermanisticCollation");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AlterColumn<string>(
|
||||
name: "ExternalId",
|
||||
table: "SsoUser",
|
||||
type: "character varying(50)",
|
||||
maxLength: 50,
|
||||
nullable: true,
|
||||
collation: "postgresIndetermanisticCollation",
|
||||
oldClrType: typeof(string),
|
||||
oldType: "character varying(300)",
|
||||
oldMaxLength: 300,
|
||||
oldNullable: true,
|
||||
oldCollation: "postgresIndetermanisticCollation");
|
||||
}
|
||||
}
|
3125
util/PostgresMigrations/Migrations/20250520201209_2025-05-20_00_AddSendEmails.Designer.cs
generated
Normal file
3125
util/PostgresMigrations/Migrations/20250520201209_2025-05-20_00_AddSendEmails.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.PostgresMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class _20250520_00_AddSendEmails : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Emails",
|
||||
table: "Send",
|
||||
type: "character varying(1024)",
|
||||
maxLength: 1024,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Emails",
|
||||
table: "Send");
|
||||
}
|
||||
}
|
@ -682,8 +682,8 @@ namespace Bit.PostgresMigrations.Migrations
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
b.Property<string>("ExternalId")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("character varying(50)")
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("character varying(300)")
|
||||
.UseCollation("postgresIndetermanisticCollation");
|
||||
|
||||
b.Property<Guid?>("OrganizationId")
|
||||
@ -1442,6 +1442,10 @@ namespace Bit.PostgresMigrations.Migrations
|
||||
b.Property<bool>("Disabled")
|
||||
.HasColumnType("boolean");
|
||||
|
||||
b.Property<string>("Emails")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("character varying(1024)");
|
||||
|
||||
b.Property<DateTime?>("ExpirationDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
|
3101
util/SqliteMigrations/Migrations/20250429113747_SsoExternalId.Designer.cs
generated
Normal file
3101
util/SqliteMigrations/Migrations/20250429113747_SsoExternalId.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,21 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.SqliteMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class SsoExternalId : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
3108
util/SqliteMigrations/Migrations/20250520201216_2025-05-20_00_AddSendEmails.Designer.cs
generated
Normal file
3108
util/SqliteMigrations/Migrations/20250520201216_2025-05-20_00_AddSendEmails.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.SqliteMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class _20250520_00_AddSendEmails : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<string>(
|
||||
name: "Emails",
|
||||
table: "Send",
|
||||
type: "TEXT",
|
||||
maxLength: 1024,
|
||||
nullable: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Emails",
|
||||
table: "Send");
|
||||
}
|
||||
}
|
@ -668,7 +668,7 @@ namespace Bit.SqliteMigrations.Migrations
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<string>("ExternalId")
|
||||
.HasMaxLength(50)
|
||||
.HasMaxLength(300)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<Guid?>("OrganizationId")
|
||||
@ -1426,6 +1426,10 @@ namespace Bit.SqliteMigrations.Migrations
|
||||
b.Property<bool>("Disabled")
|
||||
.HasColumnType("INTEGER");
|
||||
|
||||
b.Property<string>("Emails")
|
||||
.HasMaxLength(1024)
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
b.Property<DateTime?>("ExpirationDate")
|
||||
.HasColumnType("TEXT");
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user