1
0
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:
Ike 2025-05-27 16:04:07 -04:00 committed by GitHub
commit 404616d918
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
98 changed files with 21330 additions and 690 deletions

View File

@ -15,8 +15,7 @@
matchManagers: ["github-actions"], matchManagers: ["github-actions"],
matchFileNames: [ matchFileNames: [
".github/workflows/publish.yml", ".github/workflows/publish.yml",
".github/workflows/release.yml", ".github/workflows/release.yml"
".github/workflows/repository-management.yml"
], ],
commitMessagePrefix: "[deps] BRE:", commitMessagePrefix: "[deps] BRE:",
reviewers: ["team:dept-bre"], reviewers: ["team:dept-bre"],
@ -134,8 +133,8 @@
reviewers: ["team:dept-dbops"], reviewers: ["team:dept-dbops"],
}, },
{ {
matchPackageNames: ["CommandDotNet", "YamlDotNet"], matchPackageNames: ["YamlDotNet"],
description: "DevOps owned dependencies", description: "BRE owned dependencies",
commitMessagePrefix: "[deps] BRE:", commitMessagePrefix: "[deps] BRE:",
reviewers: ["team:dept-bre"], reviewers: ["team:dept-bre"],
}, },

View File

@ -9,7 +9,7 @@
"version": "0.0.0", "version": "0.0.0",
"license": "-", "license": "-",
"dependencies": { "dependencies": {
"bootstrap": "5.3.3", "bootstrap": "5.3.6",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"jquery": "3.7.1" "jquery": "3.7.1"
}, },
@ -17,9 +17,9 @@
"css-loader": "7.1.2", "css-loader": "7.1.2",
"expose-loader": "5.0.1", "expose-loader": "5.0.1",
"mini-css-extract-plugin": "2.9.2", "mini-css-extract-plugin": "2.9.2",
"sass": "1.85.0", "sass": "1.88.0",
"sass-loader": "16.0.4", "sass-loader": "16.0.5",
"webpack": "5.97.1", "webpack": "5.99.8",
"webpack-cli": "5.1.4" "webpack-cli": "5.1.4"
} }
}, },
@ -455,13 +455,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.13.14", "version": "22.15.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
"integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.20.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@webassemblyjs/ast": { "node_modules/@webassemblyjs/ast": {
@ -748,9 +748,9 @@
} }
}, },
"node_modules/bootstrap": { "node_modules/bootstrap": {
"version": "5.3.3", "version": "5.3.6",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz",
"integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", "integrity": "sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -781,9 +781,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.24.4", "version": "4.24.5",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
"integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -801,10 +801,10 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001688", "caniuse-lite": "^1.0.30001716",
"electron-to-chromium": "^1.5.73", "electron-to-chromium": "^1.5.149",
"node-releases": "^2.0.19", "node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.1" "update-browserslist-db": "^1.1.3"
}, },
"bin": { "bin": {
"browserslist": "cli.js" "browserslist": "cli.js"
@ -821,9 +821,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001707", "version": "1.0.30001718",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz",
"integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -975,9 +975,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.128", "version": "1.5.155",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz",
"integrity": "sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==", "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@ -1009,9 +1009,9 @@
} }
}, },
"node_modules/es-module-lexer": { "node_modules/es-module-lexer": {
"version": "1.6.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
"integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -1106,13 +1106,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/fast-uri": {
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
@ -1248,9 +1241,9 @@
} }
}, },
"node_modules/immutable": { "node_modules/immutable": {
"version": "5.1.1", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
"integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==", "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -1754,16 +1747,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/randombytes": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -1877,9 +1860,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/sass": { "node_modules/sass": {
"version": "1.85.0", "version": "1.88.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz", "resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz",
"integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==", "integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1898,9 +1881,9 @@
} }
}, },
"node_modules/sass-loader": { "node_modules/sass-loader": {
"version": "16.0.4", "version": "16.0.5",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz",
"integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==", "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1939,9 +1922,9 @@
} }
}, },
"node_modules/schema-utils": { "node_modules/schema-utils": {
"version": "4.3.0", "version": "4.3.2",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
"integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1959,9 +1942,9 @@
} }
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.1", "version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
@ -2078,9 +2061,9 @@
} }
}, },
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.2.1", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -2088,14 +2071,14 @@
} }
}, },
"node_modules/terser": { "node_modules/terser": {
"version": "5.39.0", "version": "5.39.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz",
"integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@jridgewell/source-map": "^0.3.3", "@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2", "acorn": "^8.14.0",
"commander": "^2.20.0", "commander": "^2.20.0",
"source-map-support": "~0.5.20" "source-map-support": "~0.5.20"
}, },
@ -2156,9 +2139,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "6.20.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -2193,16 +2176,6 @@
"browserslist": ">= 4.21.0" "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": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -2211,9 +2184,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/watchpack": { "node_modules/watchpack": {
"version": "2.4.2", "version": "2.4.4",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
"integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -2225,14 +2198,15 @@
} }
}, },
"node_modules/webpack": { "node_modules/webpack": {
"version": "5.97.1", "version": "5.99.8",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz",
"integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/eslint-scope": "^3.7.7", "@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6", "@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/ast": "^1.14.1",
"@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1",
"@webassemblyjs/wasm-parser": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1",
@ -2249,9 +2223,9 @@
"loader-runner": "^4.2.0", "loader-runner": "^4.2.0",
"mime-types": "^2.1.27", "mime-types": "^2.1.27",
"neo-async": "^2.6.2", "neo-async": "^2.6.2",
"schema-utils": "^3.2.0", "schema-utils": "^4.3.2",
"tapable": "^2.1.1", "tapable": "^2.1.1",
"terser-webpack-plugin": "^5.3.10", "terser-webpack-plugin": "^5.3.11",
"watchpack": "^2.4.1", "watchpack": "^2.4.1",
"webpack-sources": "^3.2.3" "webpack-sources": "^3.2.3"
}, },
@ -2352,59 +2326,6 @@
"node": ">=10.13.0" "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": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -8,7 +8,7 @@
"build": "webpack" "build": "webpack"
}, },
"dependencies": { "dependencies": {
"bootstrap": "5.3.3", "bootstrap": "5.3.6",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"jquery": "3.7.1" "jquery": "3.7.1"
}, },
@ -16,9 +16,9 @@
"css-loader": "7.1.2", "css-loader": "7.1.2",
"expose-loader": "5.0.1", "expose-loader": "5.0.1",
"mini-css-extract-plugin": "2.9.2", "mini-css-extract-plugin": "2.9.2",
"sass": "1.85.0", "sass": "1.88.0",
"sass-loader": "16.0.4", "sass-loader": "16.0.5",
"webpack": "5.97.1", "webpack": "5.99.8",
"webpack-cli": "5.1.4" "webpack-cli": "5.1.4"
} }
} }

View File

@ -9,7 +9,7 @@
"version": "0.0.0", "version": "0.0.0",
"license": "GPL-3.0", "license": "GPL-3.0",
"dependencies": { "dependencies": {
"bootstrap": "5.3.3", "bootstrap": "5.3.6",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"jquery": "3.7.1", "jquery": "3.7.1",
"toastr": "2.1.4" "toastr": "2.1.4"
@ -18,9 +18,9 @@
"css-loader": "7.1.2", "css-loader": "7.1.2",
"expose-loader": "5.0.1", "expose-loader": "5.0.1",
"mini-css-extract-plugin": "2.9.2", "mini-css-extract-plugin": "2.9.2",
"sass": "1.85.0", "sass": "1.88.0",
"sass-loader": "16.0.4", "sass-loader": "16.0.5",
"webpack": "5.97.1", "webpack": "5.99.8",
"webpack-cli": "5.1.4" "webpack-cli": "5.1.4"
} }
}, },
@ -456,13 +456,13 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.13.14", "version": "22.15.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.14.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
"integrity": "sha512-Zs/Ollc1SJ8nKUAgc7ivOEdIBM8JAKgrqqUYi2J997JuKO7/tpQC+WCetQ1sypiKCQWHdvdg9wBNpUPEWZae7w==", "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.20.0" "undici-types": "~6.21.0"
} }
}, },
"node_modules/@webassemblyjs/ast": { "node_modules/@webassemblyjs/ast": {
@ -749,9 +749,9 @@
} }
}, },
"node_modules/bootstrap": { "node_modules/bootstrap": {
"version": "5.3.3", "version": "5.3.6",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.6.tgz",
"integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", "integrity": "sha512-jX0GAcRzvdwISuvArXn3m7KZscWWFAf1MKBcnzaN02qWMb3jpMoUX4/qgeiGzqyIb4ojulRzs89UCUmGcFSzTA==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -782,9 +782,9 @@
} }
}, },
"node_modules/browserslist": { "node_modules/browserslist": {
"version": "4.24.4", "version": "4.24.5",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
"integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -802,10 +802,10 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"caniuse-lite": "^1.0.30001688", "caniuse-lite": "^1.0.30001716",
"electron-to-chromium": "^1.5.73", "electron-to-chromium": "^1.5.149",
"node-releases": "^2.0.19", "node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.1" "update-browserslist-db": "^1.1.3"
}, },
"bin": { "bin": {
"browserslist": "cli.js" "browserslist": "cli.js"
@ -822,9 +822,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/caniuse-lite": { "node_modules/caniuse-lite": {
"version": "1.0.30001707", "version": "1.0.30001718",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz",
"integrity": "sha512-3qtRjw/HQSMlDWf+X79N206fepf4SOOU6SQLMaq/0KkZLmSjPxAkBOQQ+FxbHKfHmYLZFfdWsO3KA90ceHPSnw==", "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@ -976,9 +976,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.128", "version": "1.5.155",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz",
"integrity": "sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==", "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@ -1010,9 +1010,9 @@
} }
}, },
"node_modules/es-module-lexer": { "node_modules/es-module-lexer": {
"version": "1.6.0", "version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
"integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -1107,13 +1107,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/fast-uri": {
"version": "3.0.6", "version": "3.0.6",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
@ -1249,9 +1242,9 @@
} }
}, },
"node_modules/immutable": { "node_modules/immutable": {
"version": "5.1.1", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz", "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
"integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==", "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -1755,16 +1748,6 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/randombytes": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
@ -1878,9 +1861,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/sass": { "node_modules/sass": {
"version": "1.85.0", "version": "1.88.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.85.0.tgz", "resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz",
"integrity": "sha512-3ToiC1xZ1Y8aU7+CkgCI/tqyuPXEmYGJXO7H4uqp0xkLXUqp88rQQ4j1HmP37xSJLbCJPaIiv+cT1y+grssrww==", "integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1899,9 +1882,9 @@
} }
}, },
"node_modules/sass-loader": { "node_modules/sass-loader": {
"version": "16.0.4", "version": "16.0.5",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz", "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz",
"integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==", "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1940,9 +1923,9 @@
} }
}, },
"node_modules/schema-utils": { "node_modules/schema-utils": {
"version": "4.3.0", "version": "4.3.2",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
"integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -1960,9 +1943,9 @@
} }
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.7.1", "version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true, "dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
@ -2079,9 +2062,9 @@
} }
}, },
"node_modules/tapable": { "node_modules/tapable": {
"version": "2.2.1", "version": "2.2.2",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
"integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -2089,14 +2072,14 @@
} }
}, },
"node_modules/terser": { "node_modules/terser": {
"version": "5.39.0", "version": "5.39.2",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz",
"integrity": "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw==", "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@jridgewell/source-map": "^0.3.3", "@jridgewell/source-map": "^0.3.3",
"acorn": "^8.8.2", "acorn": "^8.14.0",
"commander": "^2.20.0", "commander": "^2.20.0",
"source-map-support": "~0.5.20" "source-map-support": "~0.5.20"
}, },
@ -2165,9 +2148,9 @@
} }
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "6.20.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
@ -2202,16 +2185,6 @@
"browserslist": ">= 4.21.0" "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": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -2220,9 +2193,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/watchpack": { "node_modules/watchpack": {
"version": "2.4.2", "version": "2.4.4",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz",
"integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@ -2234,14 +2207,15 @@
} }
}, },
"node_modules/webpack": { "node_modules/webpack": {
"version": "5.97.1", "version": "5.99.8",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz",
"integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", "integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/eslint-scope": "^3.7.7", "@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6", "@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/ast": "^1.14.1",
"@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1",
"@webassemblyjs/wasm-parser": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1",
@ -2258,9 +2232,9 @@
"loader-runner": "^4.2.0", "loader-runner": "^4.2.0",
"mime-types": "^2.1.27", "mime-types": "^2.1.27",
"neo-async": "^2.6.2", "neo-async": "^2.6.2",
"schema-utils": "^3.2.0", "schema-utils": "^4.3.2",
"tapable": "^2.1.1", "tapable": "^2.1.1",
"terser-webpack-plugin": "^5.3.10", "terser-webpack-plugin": "^5.3.11",
"watchpack": "^2.4.1", "watchpack": "^2.4.1",
"webpack-sources": "^3.2.3" "webpack-sources": "^3.2.3"
}, },
@ -2361,59 +2335,6 @@
"node": ">=10.13.0" "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": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@ -8,7 +8,7 @@
"build": "webpack" "build": "webpack"
}, },
"dependencies": { "dependencies": {
"bootstrap": "5.3.3", "bootstrap": "5.3.6",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"jquery": "3.7.1", "jquery": "3.7.1",
"toastr": "2.1.4" "toastr": "2.1.4"
@ -17,9 +17,9 @@
"css-loader": "7.1.2", "css-loader": "7.1.2",
"expose-loader": "5.0.1", "expose-loader": "5.0.1",
"mini-css-extract-plugin": "2.9.2", "mini-css-extract-plugin": "2.9.2",
"sass": "1.85.0", "sass": "1.88.0",
"sass-loader": "16.0.4", "sass-loader": "16.0.5",
"webpack": "5.97.1", "webpack": "5.99.8",
"webpack-cli": "5.1.4" "webpack-cli": "5.1.4"
} }
} }

View File

@ -2,10 +2,10 @@
using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Core; using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.Integrations;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Integrations;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Utilities; using Bit.Core.Utilities;

View File

@ -1,8 +1,8 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using System.Text.Json; using System.Text.Json;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.Integrations;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data.Integrations;
#nullable enable #nullable enable

View File

@ -28,10 +28,8 @@ using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities; using Bit.Core.Vault.Entities;
using Bit.Api.Auth.Models.Request.WebAuthn; using Bit.Api.Auth.Models.Request.WebAuthn;
using Bit.Api.Billing; using Bit.Api.Billing;
using Bit.Core.AdminConsole.Services.NoopImplementations;
using Bit.Core.Auth.Models.Data; using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Identity.TokenProviders; using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Services;
using Bit.Core.Tools.ImportFeatures; using Bit.Core.Tools.ImportFeatures;
using Bit.Core.Tools.ReportFeatures; using Bit.Core.Tools.ReportFeatures;
using Bit.Core.Auth.Models.Api.Request; using Bit.Core.Auth.Models.Api.Request;
@ -224,18 +222,8 @@ public class Startup
services.AddHostedService<Core.HostedServices.ApplicationCacheHostedService>(); services.AddHostedService<Core.HostedServices.ApplicationCacheHostedService>();
} }
// Slack // Add SlackService for OAuth API requests - if configured
if (CoreHelpers.SettingHasValue(globalSettings.Slack.ClientId) && services.AddSlackService(globalSettings);
CoreHelpers.SettingHasValue(globalSettings.Slack.ClientSecret) &&
CoreHelpers.SettingHasValue(globalSettings.Slack.Scopes))
{
services.AddHttpClient(SlackService.HttpClientName);
services.AddSingleton<ISlackService, SlackService>();
}
else
{
services.AddSingleton<ISlackService, NoopSlackService>();
}
} }
public void Configure( public void Configure(

View File

@ -151,6 +151,16 @@ public class CiphersController : Controller
public async Task<CipherResponseModel> Post([FromBody] CipherRequestModel model) public async Task<CipherResponseModel> Post([FromBody] CipherRequestModel model)
{ {
var user = await _userService.GetUserByPrincipalAsync(User); 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); var cipher = model.ToCipherDetails(user.Id);
if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value)) 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) public async Task<CipherResponseModel> PostCreate([FromBody] CipherCreateRequestModel model)
{ {
var user = await _userService.GetUserByPrincipalAsync(User); 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); var cipher = model.Cipher.ToCipherDetails(user.Id);
if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value)) if (cipher.OrganizationId.HasValue && !await _currentContext.OrganizationUser(cipher.OrganizationId.Value))
{ {
@ -192,6 +212,16 @@ public class CiphersController : Controller
} }
var userId = _userService.GetProperUserId(User).Value; 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); await _cipherService.SaveAsync(cipher, userId, model.Cipher.LastKnownRevisionDate, model.CollectionIds, true, false);
var response = new CipherMiniResponseModel(cipher, _globalSettings, false); var response = new CipherMiniResponseModel(cipher, _globalSettings, false);
@ -209,6 +239,15 @@ public class CiphersController : Controller
throw new NotFoundException(); 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); ValidateClientVersionForFido2CredentialSupport(cipher);
var collectionIds = (await _collectionCipherRepository.GetManyByUserIdCipherIdAsync(user.Id, id)).Select(c => c.CollectionId).ToList(); 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 userId = _userService.GetProperUserId(User).Value;
var cipher = await _cipherRepository.GetOrganizationDetailsByIdAsync(id); 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); ValidateClientVersionForFido2CredentialSupport(cipher);
if (cipher == null || !cipher.OrganizationId.HasValue || if (cipher == null || !cipher.OrganizationId.HasValue ||
@ -658,6 +706,15 @@ public class CiphersController : Controller
throw new NotFoundException(); 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); ValidateClientVersionForFido2CredentialSupport(cipher);
var original = cipher.Clone(); var original = cipher.Clone();
@ -1019,6 +1076,18 @@ public class CiphersController : Controller
var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: false); var ciphers = await _cipherRepository.GetManyByUserIdAsync(userId, withOrganizations: false);
var ciphersDict = ciphers.ToDictionary(c => c.Id); 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?)>(); var shareCiphers = new List<(Cipher, DateTime?)>();
foreach (var cipher in model.Ciphers) foreach (var cipher in model.Ciphers)
{ {

View File

@ -11,6 +11,10 @@ namespace Bit.Api.Vault.Models.Request;
public class CipherRequestModel 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; } public CipherType Type { get; set; }
[StringLength(36)] [StringLength(36)]

View File

@ -7,3 +7,19 @@ public enum IntegrationType : int
Slack = 3, Slack = 3,
Webhook = 4, 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}");
}
}
}

View File

@ -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();
}

View File

@ -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;
}

View File

@ -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);
}
}

View File

@ -1,10 +1,11 @@
using Bit.Core.AdminConsole.Entities; #nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data;
#nullable enable namespace Bit.Core.AdminConsole.Models.Data.Integrations;
namespace Bit.Core.Models.Data.Integrations;
public class IntegrationTemplateContext(EventMessage eventMessage) public class IntegrationTemplateContext(EventMessage eventMessage)
{ {

View File

@ -1,3 +1,3 @@
namespace Bit.Core.Models.Data.Integrations; namespace Bit.Core.AdminConsole.Models.Data.Integrations;
public record SlackIntegration(string token); public record SlackIntegration(string token);

View File

@ -1,3 +1,3 @@
namespace Bit.Core.Models.Data.Integrations; namespace Bit.Core.AdminConsole.Models.Data.Integrations;
public record SlackIntegrationConfiguration(string channelId); public record SlackIntegrationConfiguration(string channelId);

View File

@ -1,3 +1,3 @@
namespace Bit.Core.Models.Data.Integrations; namespace Bit.Core.AdminConsole.Models.Data.Integrations;
public record SlackIntegrationConfigurationDetails(string channelId, string token); public record SlackIntegrationConfigurationDetails(string channelId, string token);

View File

@ -1,3 +1,3 @@
namespace Bit.Core.Models.Data.Integrations; namespace Bit.Core.AdminConsole.Models.Data.Integrations;
public record WebhookIntegrationConfiguration(string url); public record WebhookIntegrationConfiguration(string url);

View File

@ -0,0 +1,3 @@
namespace Bit.Core.AdminConsole.Models.Data.Integrations;
public record WebhookIntegrationConfigurationDetails(string url);

View File

@ -1,3 +0,0 @@
namespace Bit.Core.Models.Data.Integrations;
public record WebhookIntegrationConfigurationDetils(string url);

View 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);
}

View File

@ -0,0 +1,8 @@
using Bit.Core.AdminConsole.Models.Data.Integrations;
namespace Bit.Core.Services;
public interface IIntegrationPublisher
{
Task PublishAsync(IIntegrationMessage message);
}

View File

@ -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;
}
}

View File

@ -1,8 +1,8 @@
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using Bit.Core.AdminConsole.Models.Data.Integrations;
using Bit.Core.AdminConsole.Utilities; using Bit.Core.AdminConsole.Utilities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Integrations;
using Bit.Core.Repositories; using Bit.Core.Repositories;
namespace Bit.Core.Services; namespace Bit.Core.Services;

View File

@ -29,7 +29,7 @@ public class RabbitMqEventListenerService : EventLoggingListenerService
UserName = globalSettings.EventLogging.RabbitMq.Username, UserName = globalSettings.EventLogging.RabbitMq.Username,
Password = globalSettings.EventLogging.RabbitMq.Password Password = globalSettings.EventLogging.RabbitMq.Password
}; };
_exchangeName = globalSettings.EventLogging.RabbitMq.ExchangeName; _exchangeName = globalSettings.EventLogging.RabbitMq.EventExchangeName;
_logger = logger; _logger = logger;
_queueName = queueName; _queueName = queueName;
} }

View File

@ -18,7 +18,7 @@ public class RabbitMqEventWriteService : IEventWriteService, IAsyncDisposable
UserName = globalSettings.EventLogging.RabbitMq.Username, UserName = globalSettings.EventLogging.RabbitMq.Username,
Password = globalSettings.EventLogging.RabbitMq.Password Password = globalSettings.EventLogging.RabbitMq.Password
}; };
_exchangeName = globalSettings.EventLogging.RabbitMq.ExchangeName; _exchangeName = globalSettings.EventLogging.RabbitMq.EventExchangeName;
_lazyConnection = new Lazy<Task<IConnection>>(CreateConnectionAsync); _lazyConnection = new Lazy<Task<IConnection>>(CreateConnectionAsync);
} }

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -1,7 +1,7 @@
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using Bit.Core.AdminConsole.Models.Data.Integrations;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data.Integrations;
using Bit.Core.Repositories; using Bit.Core.Repositories;
#nullable enable #nullable enable

View File

@ -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);
}
}

View File

@ -1,8 +1,8 @@
using System.Text; using System.Text;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using Bit.Core.AdminConsole.Models.Data.Integrations;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data.Integrations;
using Bit.Core.Repositories; using Bit.Core.Repositories;
#nullable enable #nullable enable
@ -25,7 +25,7 @@ public class WebhookEventHandler(
protected override async Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration, protected override async Task ProcessEventIntegrationAsync(JsonObject mergedConfiguration,
string renderedTemplate) string renderedTemplate)
{ {
var config = mergedConfiguration.Deserialize<WebhookIntegrationConfigurationDetils>(); var config = mergedConfiguration.Deserialize<WebhookIntegrationConfigurationDetails>();
if (config is null || string.IsNullOrEmpty(config.url)) if (config is null || string.IsNullOrEmpty(config.url))
{ {
return; return;

View File

@ -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;
}
}

View File

@ -1,4 +1,6 @@
using System.Text.RegularExpressions; #nullable enable
using System.Text.RegularExpressions;
namespace Bit.Core.AdminConsole.Utilities; namespace Bit.Core.AdminConsole.Utilities;
@ -9,7 +11,7 @@ public static partial class IntegrationTemplateProcessor
public static string ReplaceTokens(string template, object values) public static string ReplaceTokens(string template, object values)
{ {
if (string.IsNullOrEmpty(template) || values == null) if (string.IsNullOrEmpty(template))
{ {
return template; return template;
} }

View File

@ -8,7 +8,7 @@ public class SsoUser : ITableObject<long>
public long Id { get; set; } public long Id { get; set; }
public Guid UserId { get; set; } public Guid UserId { get; set; }
public Guid? OrganizationId { get; set; } public Guid? OrganizationId { get; set; }
[MaxLength(50)] [MaxLength(300)]
public string ExternalId { get; set; } public string ExternalId { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow; public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;

View File

@ -34,9 +34,9 @@
<PackageReference Include="DnsClient" Version="1.8.0" /> <PackageReference Include="DnsClient" Version="1.8.0" />
<PackageReference Include="Fido2.AspNet" Version="3.0.1" /> <PackageReference Include="Fido2.AspNet" Version="3.0.1" />
<PackageReference Include="Handlebars.Net" Version="2.1.6" /> <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.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.Azure.NotificationHubs" Version="4.2.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" /> <PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.7.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.7.0" />

View File

@ -36,6 +36,11 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
public string? TwoFactorRecoveryCode { get; set; } public string? TwoFactorRecoveryCode { get; set; }
public string? EquivalentDomains { get; set; } public string? EquivalentDomains { get; set; }
public string? ExcludedGlobalEquivalentDomains { 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 DateTime AccountRevisionDate { get; set; } = DateTime.UtcNow;
public string? Key { get; set; } public string? Key { get; set; }
public string? PublicKey { get; set; } public string? PublicKey { get; set; }

View File

@ -312,11 +312,19 @@ public class GlobalSettings : IGlobalSettings
private string _hostName; private string _hostName;
private string _username; private string _username;
private string _password; 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 EventRepositoryQueueName { get; set; } = "events-write-queue";
public virtual string WebhookQueueName { get; set; } = "events-webhook-queue"; public virtual string IntegrationDeadLetterQueueName { get; set; } = "integration-dead-letter-queue";
public virtual string SlackQueueName { get; set; } = "events-slack-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 public string HostName
{ {
@ -333,10 +341,15 @@ public class GlobalSettings : IGlobalSettings
get => _password; get => _password;
set => _password = value.Trim('"'); set => _password = value.Trim('"');
} }
public string ExchangeName public string EventExchangeName
{ {
get => _exchangeName; get => _eventExchangeName;
set => _exchangeName = value.Trim('"'); set => _eventExchangeName = value.Trim('"');
}
public string IntegrationExchangeName
{
get => _integrationExchangeName;
set => _integrationExchangeName = value.Trim('"');
} }
} }
} }

View File

@ -60,9 +60,21 @@ public class Send : ITableObject<Guid>
/// <summary> /// <summary>
/// Password provided by the user. Protected with pbkdf2. /// Password provided by the user. Protected with pbkdf2.
/// </summary> /// </summary>
/// <remarks>
/// This field is mutually exclusive with <see cref="Emails" />
/// </remarks>
[MaxLength(300)] [MaxLength(300)]
public string? Password { get; set; } 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> /// <summary>
/// The send becomes unavailable to API callers when /// The send becomes unavailable to API callers when
/// <see cref="AccessCount"/> &gt;= <see cref="MaxAccessCount"/>. /// <see cref="AccessCount"/> &gt;= <see cref="MaxAccessCount"/>.

View File

@ -115,7 +115,7 @@ public class ImportCiphersCommand : IImportCiphersCommand
} }
// Create it all // Create it all
await _cipherRepository.CreateAsync(ciphers, newFolders); await _cipherRepository.CreateAsync(importingUserId, ciphers, newFolders);
// push // push
await _pushService.PushSyncVaultAsync(importingUserId); await _pushService.PushSyncVaultAsync(importingUserId);

View 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;

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -1,5 +1,7 @@
using Bit.Core.Tools.SendFeatures.Commands; using Bit.Core.Tools.SendFeatures.Commands;
using Bit.Core.Tools.SendFeatures.Commands.Interfaces; 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 Bit.Core.Tools.Services;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -14,5 +16,6 @@ public static class SendServiceCollectionExtension
services.AddScoped<ISendAuthorizationService, SendAuthorizationService>(); services.AddScoped<ISendAuthorizationService, SendAuthorizationService>();
services.AddScoped<ISendValidationService, SendValidationService>(); services.AddScoped<ISendValidationService, SendValidationService>();
services.AddScoped<ISendCoreHelperService, SendCoreHelperService>(); services.AddScoped<ISendCoreHelperService, SendCoreHelperService>();
services.AddScoped<ISendAuthenticationQuery, SendAuthenticationQuery>();
} }
} }

View File

@ -32,7 +32,10 @@ public interface ICipherRepository : IRepository<Cipher, Guid>
Task DeleteByUserIdAsync(Guid userId); Task DeleteByUserIdAsync(Guid userId);
Task DeleteByOrganizationIdAsync(Guid organizationId); Task DeleteByOrganizationIdAsync(Guid organizationId);
Task UpdateCiphersAsync(Guid userId, IEnumerable<Cipher> ciphers); 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, Task CreateAsync(IEnumerable<Cipher> ciphers, IEnumerable<Collection> collections,
IEnumerable<CollectionCipher> collectionCiphers, IEnumerable<CollectionUser> collectionUsers); IEnumerable<CollectionCipher> collectionCiphers, IEnumerable<CollectionUser> collectionUsers);
Task SoftDeleteAsync(IEnumerable<Guid> ids, Guid userId); Task SoftDeleteAsync(IEnumerable<Guid> ids, Guid userId);

View File

@ -1,6 +1,4 @@
using System.Globalization; using System.Globalization;
using Bit.Core.AdminConsole.Services.Implementations;
using Bit.Core.AdminConsole.Services.NoopImplementations;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.IdentityServer; using Bit.Core.IdentityServer;
using Bit.Core.Services; using Bit.Core.Services;
@ -63,37 +61,7 @@ public class Startup
services.AddSingleton<IApplicationCacheService, InMemoryApplicationCacheService>(); services.AddSingleton<IApplicationCacheService, InMemoryApplicationCacheService>();
} }
if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString)) services.AddEventWriteServices(globalSettings);
{
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.AddScoped<IEventService, EventService>(); services.AddScoped<IEventService, EventService>();
services.AddOptionality(); services.AddOptionality();
@ -109,49 +77,7 @@ public class Startup
services.AddHostedService<Core.HostedServices.ApplicationCacheHostedService>(); services.AddHostedService<Core.HostedServices.ApplicationCacheHostedService>();
} }
// Optional RabbitMQ Listeners services.AddRabbitMqListeners(globalSettings);
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));
}
} }
public void Configure( public void Configure(

View File

@ -1,12 +1,8 @@
using System.Globalization; using System.Globalization;
using Bit.Core.AdminConsole.Services.NoopImplementations;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Bit.SharedWeb.Utilities; using Bit.SharedWeb.Utilities;
using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Logging;
using TableStorageRepos = Bit.Core.Repositories.TableStorage;
namespace Bit.EventsProcessor; namespace Bit.EventsProcessor;
@ -37,50 +33,7 @@ public class Startup
services.AddDatabaseRepositories(globalSettings); services.AddDatabaseRepositories(globalSettings);
// Hosted Services // Hosted Services
services.AddAzureServiceBusListeners(globalSettings);
// 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.AddHostedService<AzureQueueHostedService>(); services.AddHostedService<AzureQueueHostedService>();
} }

View File

@ -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()) if (!ciphers.Any())
{ {
@ -518,7 +518,7 @@ public class CipherRepository : Repository<Cipher, Guid>, ICipherRepository
await connection.ExecuteAsync( await connection.ExecuteAsync(
$"[{Schema}].[User_BumpAccountRevisionDate]", $"[{Schema}].[User_BumpAccountRevisionDate]",
new { Id = ciphers.First().UserId }, new { Id = userId },
commandType: CommandType.StoredProcedure, transaction: transaction); commandType: CommandType.StoredProcedure, transaction: transaction);
transaction.Commit(); transaction.Commit();

View File

@ -1,4 +1,5 @@
using AutoMapper; using System.Diagnostics;
using AutoMapper;
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers.Models;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -7,11 +8,12 @@ using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Infrastructure.EntityFramework.Models; using Bit.Infrastructure.EntityFramework.Models;
using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories.Queries; using Bit.Infrastructure.EntityFramework.Repositories.Queries;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection; 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 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); 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); return;
await dbContext.UserBumpAccountRevisionDateAsync(organizationUser.UserId.GetValueOrDefault());
await dbContext.SaveChangesAsync();
} }
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) public async Task ReplaceAsync(Core.Entities.OrganizationUser obj, IEnumerable<CollectionAccessSelection> requestedCollections)

View File

@ -1,4 +1,6 @@
using System.Diagnostics; #nullable enable
using System.Diagnostics;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -11,8 +13,18 @@ namespace Bit.Infrastructure.EntityFramework.Repositories;
public static class DatabaseContextExtensions 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) 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); 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."); 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; user.AccountRevisionDate = DateTime.UtcNow;

View File

@ -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()) if (!ciphers.Any())
{ {
return; return;
@ -156,7 +158,8 @@ public class CipherRepository : Repository<Core.Vault.Entities.Cipher, Cipher, G
await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, folderEntities); await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, folderEntities);
var cipherEntities = Mapper.Map<List<Cipher>>(ciphers); var cipherEntities = Mapper.Map<List<Cipher>>(ciphers);
await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, cipherEntities); await dbContext.BulkCopyAsync(base.DefaultBulkCopyOptions, cipherEntities);
await dbContext.UserBumpAccountRevisionDateAsync(ciphers.First().UserId.GetValueOrDefault()); await dbContext.UserBumpAccountRevisionDateAsync(userId);
await dbContext.SaveChangesAsync(); await dbContext.SaveChangesAsync();
} }
} }

View File

@ -5,6 +5,7 @@ using System.Security.Cryptography.X509Certificates;
using AspNetCoreRateLimit; using AspNetCoreRateLimit;
using Azure.Storage.Queues; using Azure.Storage.Queues;
using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.Models.Data.Integrations;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies; using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.AdminConsole.Services.Implementations; using Bit.Core.AdminConsole.Services.Implementations;
@ -324,42 +325,7 @@ public static class ServiceCollectionExtensions
services.AddSingleton<IMailEnqueuingService, BlockingMailEnqueuingService>(); services.AddSingleton<IMailEnqueuingService, BlockingMailEnqueuingService>();
} }
if (!globalSettings.SelfHosted && CoreHelpers.SettingHasValue(globalSettings.Events.ConnectionString)) services.AddEventWriteServices(globalSettings);
{
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>();
if (CoreHelpers.SettingHasValue(globalSettings.Attachment.ConnectionString)) if (CoreHelpers.SettingHasValue(globalSettings.Attachment.ConnectionString))
{ {
@ -584,6 +550,193 @@ public static class ServiceCollectionExtensions
return globalSettings; 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, public static void UseDefaultMiddleware(this IApplicationBuilder app,
IWebHostEnvironment env, GlobalSettings globalSettings) IWebHostEnvironment env, GlobalSettings globalSettings)
{ {

View File

@ -2,7 +2,7 @@
@Id BIGINT OUTPUT, @Id BIGINT OUTPUT,
@UserId UNIQUEIDENTIFIER, @UserId UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER, @OrganizationId UNIQUEIDENTIFIER,
@ExternalId NVARCHAR(50), @ExternalId NVARCHAR(300),
@CreationDate DATETIME2(7) @CreationDate DATETIME2(7)
AS AS
BEGIN BEGIN

View File

@ -2,7 +2,7 @@
@Id BIGINT OUTPUT, @Id BIGINT OUTPUT,
@UserId UNIQUEIDENTIFIER, @UserId UNIQUEIDENTIFIER,
@OrganizationId UNIQUEIDENTIFIER, @OrganizationId UNIQUEIDENTIFIER,
@ExternalId NVARCHAR(50), @ExternalId NVARCHAR(300),
@CreationDate DATETIME2(7) @CreationDate DATETIME2(7)
AS AS
BEGIN BEGIN

View File

@ -2,7 +2,7 @@
[Id] BIGINT IDENTITY (1, 1) NOT NULL, [Id] BIGINT IDENTITY (1, 1) NOT NULL,
[UserId] UNIQUEIDENTIFIER NOT NULL, [UserId] UNIQUEIDENTIFIER NOT NULL,
[OrganizationId] UNIQUEIDENTIFIER NULL, [OrganizationId] UNIQUEIDENTIFIER NULL,
[ExternalId] NVARCHAR(50) NOT NULL, [ExternalId] NVARCHAR(300) NOT NULL,
[CreationDate] DATETIME2 (7) NOT NULL, [CreationDate] DATETIME2 (7) NOT NULL,
CONSTRAINT [PK_SsoUser] PRIMARY KEY CLUSTERED ([Id] ASC), CONSTRAINT [PK_SsoUser] PRIMARY KEY CLUSTERED ([Id] ASC),
CONSTRAINT [FK_SsoUser_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE, CONSTRAINT [FK_SsoUser_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id]) ON DELETE CASCADE,

View File

@ -14,7 +14,10 @@
@DeletionDate DATETIME2(7), @DeletionDate DATETIME2(7),
@Disabled BIT, @Disabled BIT,
@HideEmail 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 AS
BEGIN BEGIN
SET NOCOUNT ON SET NOCOUNT ON
@ -36,7 +39,8 @@ BEGIN
[DeletionDate], [DeletionDate],
[Disabled], [Disabled],
[HideEmail], [HideEmail],
[CipherId] [CipherId],
[Emails]
) )
VALUES VALUES
( (
@ -55,7 +59,8 @@ BEGIN
@DeletionDate, @DeletionDate,
@Disabled, @Disabled,
@HideEmail, @HideEmail,
@CipherId @CipherId,
@Emails
) )
IF @UserId IS NOT NULL IF @UserId IS NOT NULL

View File

@ -14,7 +14,8 @@
@DeletionDate DATETIME2(7), @DeletionDate DATETIME2(7),
@Disabled BIT, @Disabled BIT,
@HideEmail BIT, @HideEmail BIT,
@CipherId UNIQUEIDENTIFIER = NULL @CipherId UNIQUEIDENTIFIER = NULL,
@Emails NVARCHAR(1024) = NULL
AS AS
BEGIN BEGIN
SET NOCOUNT ON SET NOCOUNT ON
@ -36,7 +37,8 @@ BEGIN
[DeletionDate] = @DeletionDate, [DeletionDate] = @DeletionDate,
[Disabled] = @Disabled, [Disabled] = @Disabled,
[HideEmail] = @HideEmail, [HideEmail] = @HideEmail,
[CipherId] = @CipherId [CipherId] = @CipherId,
[Emails] = @Emails
WHERE WHERE
[Id] = @Id [Id] = @Id

View File

@ -6,6 +6,7 @@
[Data] VARCHAR(MAX) NOT NULL, [Data] VARCHAR(MAX) NOT NULL,
[Key] VARCHAR (MAX) NOT NULL, [Key] VARCHAR (MAX) NOT NULL,
[Password] NVARCHAR (300) NULL, [Password] NVARCHAR (300) NULL,
[Emails] NVARCHAR (1024) NULL,
[MaxAccessCount] INT NULL, [MaxAccessCount] INT NULL,
[AccessCount] INT NOT NULL, [AccessCount] INT NOT NULL,
[CreationDate] DATETIME2 (7) NOT NULL, [CreationDate] DATETIME2 (7) NOT NULL,

View File

@ -3,10 +3,10 @@ using Bit.Api.AdminConsole.Controllers;
using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations; using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.Integrations;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Integrations;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;

View File

@ -1,7 +1,7 @@
using System.Text.Json; using System.Text.Json;
using Bit.Api.AdminConsole.Models.Request.Organizations; using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Core.AdminConsole.Models.Data.Integrations;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data.Integrations;
using Xunit; using Xunit;
namespace Bit.Api.Test.AdminConsole.Models.Request.Organizations; namespace Bit.Api.Test.AdminConsole.Models.Request.Organizations;

View File

@ -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));
}
}

View File

@ -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)));
}
}
}

View File

@ -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);
}
}
}

View 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());
}
}

View File

@ -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))
);
}
}

View File

@ -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);
}
}

View File

@ -1,4 +1,6 @@
using Bit.Core.AdminConsole.Utilities; #nullable enable
using Bit.Core.AdminConsole.Utilities;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using Xunit; using Xunit;
@ -76,18 +78,6 @@ public class IntegrationTemplateProcessorTests
var expectedEmpty = ""; var expectedEmpty = "";
Assert.Equal(expectedEmpty, IntegrationTemplateProcessor.ReplaceTokens(emptyTemplate, eventMessage)); 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] [Theory]

View File

@ -49,7 +49,7 @@ public class ImportCiphersAsyncCommandTests
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId); await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
// Assert // 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); 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.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); await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
} }

View 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
};
}
}

View File

@ -28,4 +28,31 @@ public class StaticStoreTests
Assert.NotNull(plan); Assert.NotNull(plan);
Assert.Equal(planType, plan.Type); 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}");
}
}
}
} }

View File

@ -7,6 +7,7 @@ using Bit.Core.Test.AutoFixture.Attributes;
using Bit.Infrastructure.EFIntegration.Test.AutoFixture; using Bit.Infrastructure.EFIntegration.Test.AutoFixture;
using Bit.Infrastructure.EFIntegration.Test.Repositories.EqualityComparers; using Bit.Infrastructure.EFIntegration.Test.Repositories.EqualityComparers;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models; using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
using Xunit; using Xunit;
using EfRepo = Bit.Infrastructure.EntityFramework.Repositories; using EfRepo = Bit.Infrastructure.EntityFramework.Repositories;
using Organization = Bit.Core.AdminConsole.Entities.Organization; using Organization = Bit.Core.AdminConsole.Entities.Organization;
@ -161,7 +162,7 @@ public class OrganizationRepositoryTests
[CiSkippedTheory, EfOrganizationUserAutoData] [CiSkippedTheory, EfOrganizationUserAutoData]
public async Task SearchUnassignedAsync_Works(OrganizationUser orgUser, User user, Organization org, 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) SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.OrganizationRepository sqlOrgRepo, SqlRepo.UserRepository sqlUserRepo)
{ {
orgUser.Type = OrganizationUserType.Owner; orgUser.Type = OrganizationUserType.Owner;

View File

@ -24,7 +24,7 @@ public class OrganizationUserRepositoryTests
{ {
[CiSkippedTheory, EfOrganizationUserAutoData] [CiSkippedTheory, EfOrganizationUserAutoData]
public async Task CreateAsync_Works_DataMatches(OrganizationUser orgUser, User user, Organization org, 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, List<EfRepo.OrganizationRepository> efOrgRepos, List<EfRepo.UserRepository> efUserRepos,
SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.UserRepository sqlUserRepo, SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.UserRepository sqlUserRepo,
SqlRepo.OrganizationRepository sqlOrgRepo) SqlRepo.OrganizationRepository sqlOrgRepo)
@ -67,7 +67,7 @@ public class OrganizationUserRepositoryTests
User user, User user,
Organization org, Organization org,
OrganizationUserCompare equalityComparer, OrganizationUserCompare equalityComparer,
List<EfRepo.OrganizationUserRepository> suts, List<EfAdminConsoleRepo.OrganizationUserRepository> suts,
List<EfRepo.UserRepository> efUserRepos, List<EfRepo.UserRepository> efUserRepos,
List<EfRepo.OrganizationRepository> efOrgRepos, List<EfRepo.OrganizationRepository> efOrgRepos,
SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.OrganizationUserRepository sqlOrgUserRepo,
@ -113,7 +113,7 @@ public class OrganizationUserRepositoryTests
} }
[CiSkippedTheory, EfOrganizationUserAutoData] [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, List<EfRepo.UserRepository> efUserRepos, List<EfRepo.OrganizationRepository> efOrgRepos,
SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.UserRepository sqlUserRepo, SqlRepo.OrganizationUserRepository sqlOrgUserRepo, SqlRepo.UserRepository sqlUserRepo,
SqlRepo.OrganizationRepository sqlOrgRepo) SqlRepo.OrganizationRepository sqlOrgRepo)
@ -188,7 +188,7 @@ public class OrganizationUserRepositoryTests
List<EfAdminConsoleRepo.PolicyRepository> efPolicyRepository, List<EfAdminConsoleRepo.PolicyRepository> efPolicyRepository,
List<EfRepo.UserRepository> efUserRepository, List<EfRepo.UserRepository> efUserRepository,
List<EfRepo.OrganizationRepository> efOrganizationRepository, List<EfRepo.OrganizationRepository> efOrganizationRepository,
List<EfRepo.OrganizationUserRepository> suts, List<EfAdminConsoleRepo.OrganizationUserRepository> suts,
List<EfAdminConsoleRepo.ProviderRepository> efProviderRepository, List<EfAdminConsoleRepo.ProviderRepository> efProviderRepository,
List<EfAdminConsoleRepo.ProviderOrganizationRepository> efProviderOrganizationRepository, List<EfAdminConsoleRepo.ProviderOrganizationRepository> efProviderOrganizationRepository,
List<EfAdminConsoleRepo.ProviderUserRepository> efProviderUserRepository, List<EfAdminConsoleRepo.ProviderUserRepository> efProviderUserRepository,

View File

@ -7,6 +7,7 @@ using Bit.Core.Entities;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures; using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
using Bit.Core.Test.AutoFixture.UserFixtures; using Bit.Core.Test.AutoFixture.UserFixtures;
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;

View File

@ -5,6 +5,7 @@ using Bit.Core.Test.AutoFixture.UserFixtures;
using Bit.Core.Vault.Entities; using Bit.Core.Vault.Entities;
using Bit.Core.Vault.Models.Data; using Bit.Core.Vault.Models.Data;
using Bit.Infrastructure.EFIntegration.Test.AutoFixture.Relays; using Bit.Infrastructure.EFIntegration.Test.AutoFixture.Relays;
using Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
using Bit.Infrastructure.EntityFramework.Repositories; using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Infrastructure.EntityFramework.Vault.Repositories; using Bit.Infrastructure.EntityFramework.Vault.Repositories;
using Bit.Test.Common.AutoFixture; using Bit.Test.Common.AutoFixture;

View File

@ -9,6 +9,7 @@ using Bit.Infrastructure.EntityFramework.Repositories.Queries;
using Bit.Test.Common.AutoFixture.Attributes; using Bit.Test.Common.AutoFixture.Attributes;
using LinqToDB; using LinqToDB;
using Xunit; using Xunit;
using EfAdminConsoleRepo = Bit.Infrastructure.EntityFramework.AdminConsole.Repositories;
using EfRepo = Bit.Infrastructure.EntityFramework.Repositories; using EfRepo = Bit.Infrastructure.EntityFramework.Repositories;
using EfVaultRepo = Bit.Infrastructure.EntityFramework.Vault.Repositories; using EfVaultRepo = Bit.Infrastructure.EntityFramework.Vault.Repositories;
using SqlRepo = Bit.Infrastructure.Dapper.Repositories; using SqlRepo = Bit.Infrastructure.Dapper.Repositories;
@ -112,7 +113,7 @@ public class CipherRepositoryTests
[CiSkippedTheory, EfOrganizationCipherCustomize, BitAutoData] [CiSkippedTheory, EfOrganizationCipherCustomize, BitAutoData]
public async Task CreateAsync_BumpsOrgUserAccountRevisionDates(Cipher cipher, List<User> users, 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<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>(); var savedCiphers = new List<Cipher>();
foreach (var sut in suts) foreach (var sut in suts)

View File

@ -1,5 +1,6 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Repositories; 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, public static Task<Organization> CreateTestOrganizationAsync(this IOrganizationRepository organizationRepository,
string identifier = "test") string identifier = "test")
=> organizationRepository.CreateAsync(new Organization => organizationRepository.CreateAsync(new Organization
{ {
Name = $"{identifier}-{Guid.NewGuid()}", Name = $"{identifier}-{Guid.NewGuid()}",
BillingEmail = "billing@example.com", // TODO: EF does not enforce this being NOT NULL 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( public static Task<OrganizationUser> CreateTestOrganizationUserAsync(
this IOrganizationUserRepository organizationUserRepository, this IOrganizationUserRepository organizationUserRepository,
Organization organization, Organization organization,
@ -47,6 +56,17 @@ public static class OrganizationTestHelpers
Type = OrganizationUserType.Owner 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( public static Task<Group> CreateTestGroupAsync(
this IGroupRepository groupRepository, this IGroupRepository groupRepository,
Organization organization, Organization organization,
@ -54,4 +74,14 @@ public static class OrganizationTestHelpers
=> groupRepository.CreateAsync( => groupRepository.CreateAsync(
new Group { OrganizationId = organization.Id, Name = $"{identifier} {Guid.NewGuid()}" } 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()}"
});
} }

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -7,7 +7,7 @@ using Bit.Core.Models.Data;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Xunit; using Xunit;
namespace Bit.Infrastructure.IntegrationTest.Repositories; namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.CollectionRepository;
public class CollectionRepositoryTests public class CollectionRepositoryTests
{ {
@ -463,147 +463,4 @@ public class CollectionRepositoryTests
Assert.False(c3.Unmanaged); 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]);
}
} }

View File

@ -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);
}
}

View File

@ -8,7 +8,7 @@ using Bit.Core.Repositories;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Xunit; using Xunit;
namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories; namespace Bit.Infrastructure.IntegrationTest.AdminConsole.Repositories.OrganizationUserRepository;
public class OrganizationUserRepositoryTests public class OrganizationUserRepositoryTests
{ {

View 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

View 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

File diff suppressed because it is too large Load Diff

View File

@ -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");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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");
}
}

View File

@ -679,8 +679,8 @@ namespace Bit.MySqlMigrations.Migrations
.HasColumnType("datetime(6)"); .HasColumnType("datetime(6)");
b.Property<string>("ExternalId") b.Property<string>("ExternalId")
.HasMaxLength(50) .HasMaxLength(300)
.HasColumnType("varchar(50)"); .HasColumnType("varchar(300)");
b.Property<Guid?>("OrganizationId") b.Property<Guid?>("OrganizationId")
.HasColumnType("char(36)"); .HasColumnType("char(36)");
@ -1437,6 +1437,10 @@ namespace Bit.MySqlMigrations.Migrations
b.Property<bool>("Disabled") b.Property<bool>("Disabled")
.HasColumnType("tinyint(1)"); .HasColumnType("tinyint(1)");
b.Property<string>("Emails")
.HasMaxLength(1024)
.HasColumnType("varchar(1024)");
b.Property<DateTime?>("ExpirationDate") b.Property<DateTime?>("ExpirationDate")
.HasColumnType("datetime(6)"); .HasColumnType("datetime(6)");

File diff suppressed because it is too large Load Diff

View File

@ -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");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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");
}
}

View File

@ -682,8 +682,8 @@ namespace Bit.PostgresMigrations.Migrations
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");
b.Property<string>("ExternalId") b.Property<string>("ExternalId")
.HasMaxLength(50) .HasMaxLength(300)
.HasColumnType("character varying(50)") .HasColumnType("character varying(300)")
.UseCollation("postgresIndetermanisticCollation"); .UseCollation("postgresIndetermanisticCollation");
b.Property<Guid?>("OrganizationId") b.Property<Guid?>("OrganizationId")
@ -1442,6 +1442,10 @@ namespace Bit.PostgresMigrations.Migrations
b.Property<bool>("Disabled") b.Property<bool>("Disabled")
.HasColumnType("boolean"); .HasColumnType("boolean");
b.Property<string>("Emails")
.HasMaxLength(1024)
.HasColumnType("character varying(1024)");
b.Property<DateTime?>("ExpirationDate") b.Property<DateTime?>("ExpirationDate")
.HasColumnType("timestamp with time zone"); .HasColumnType("timestamp with time zone");

File diff suppressed because it is too large Load Diff

View File

@ -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)
{
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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");
}
}

View File

@ -668,7 +668,7 @@ namespace Bit.SqliteMigrations.Migrations
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<string>("ExternalId") b.Property<string>("ExternalId")
.HasMaxLength(50) .HasMaxLength(300)
.HasColumnType("TEXT"); .HasColumnType("TEXT");
b.Property<Guid?>("OrganizationId") b.Property<Guid?>("OrganizationId")
@ -1426,6 +1426,10 @@ namespace Bit.SqliteMigrations.Migrations
b.Property<bool>("Disabled") b.Property<bool>("Disabled")
.HasColumnType("INTEGER"); .HasColumnType("INTEGER");
b.Property<string>("Emails")
.HasMaxLength(1024)
.HasColumnType("TEXT");
b.Property<DateTime?>("ExpirationDate") b.Property<DateTime?>("ExpirationDate")
.HasColumnType("TEXT"); .HasColumnType("TEXT");