diff --git a/src/Admin/package-lock.json b/src/Admin/package-lock.json
index b3f19c4792..2e3a335598 100644
--- a/src/Admin/package-lock.json
+++ b/src/Admin/package-lock.json
@@ -18,9 +18,9 @@
"css-loader": "7.1.2",
"expose-loader": "5.0.1",
"mini-css-extract-plugin": "2.9.2",
- "sass": "1.89.2",
+ "sass": "1.91.0",
"sass-loader": "16.0.5",
- "webpack": "5.99.8",
+ "webpack": "5.101.3",
"webpack-cli": "5.1.4"
}
},
@@ -35,18 +35,14 @@
}
},
"node_modules/@jridgewell/gen-mapping": {
- "version": "0.3.8",
- "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
- "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@jridgewell/set-array": "^1.2.1",
- "@jridgewell/sourcemap-codec": "^1.4.10",
+ "@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
- },
- "engines": {
- "node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
@@ -59,20 +55,10 @@
"node": ">=6.0.0"
}
},
- "node_modules/@jridgewell/set-array": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
- "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=6.0.0"
- }
- },
"node_modules/@jridgewell/source-map": {
- "version": "0.3.6",
- "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
- "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
+ "version": "0.3.11",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
+ "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -81,16 +67,16 @@
}
},
"node_modules/@jridgewell/sourcemap-codec": {
- "version": "1.5.0",
- "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
- "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
- "version": "0.3.25",
- "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
- "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
+ "version": "0.3.30",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
+ "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -442,9 +428,9 @@
}
},
"node_modules/@types/estree": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
- "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT"
},
@@ -456,13 +442,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "22.15.21",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
- "integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
+ "version": "24.3.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz",
+ "integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==",
"dev": true,
"license": "MIT",
"dependencies": {
- "undici-types": "~6.21.0"
+ "undici-types": "~7.10.0"
}
},
"node_modules/@webassemblyjs/ast": {
@@ -688,9 +674,9 @@
"license": "Apache-2.0"
},
"node_modules/acorn": {
- "version": "8.14.1",
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
- "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
+ "version": "8.15.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"bin": {
@@ -700,6 +686,19 @@
"node": ">=0.4.0"
}
},
+ "node_modules/acorn-import-phases": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
+ "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.13.0"
+ },
+ "peerDependencies": {
+ "acorn": "^8.14.0"
+ }
+ },
"node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
@@ -782,9 +781,9 @@
}
},
"node_modules/browserslist": {
- "version": "4.24.5",
- "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
- "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
+ "version": "4.25.4",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz",
+ "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==",
"dev": true,
"funding": [
{
@@ -802,8 +801,8 @@
],
"license": "MIT",
"dependencies": {
- "caniuse-lite": "^1.0.30001716",
- "electron-to-chromium": "^1.5.149",
+ "caniuse-lite": "^1.0.30001737",
+ "electron-to-chromium": "^1.5.211",
"node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.3"
},
@@ -822,9 +821,9 @@
"license": "MIT"
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001718",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz",
- "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==",
+ "version": "1.0.30001741",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz",
+ "integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==",
"dev": true,
"funding": [
{
@@ -976,16 +975,16 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.155",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz",
- "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==",
+ "version": "1.5.215",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.215.tgz",
+ "integrity": "sha512-TIvGp57UpeNetj/wV/xpFNpWGb0b/ROw372lHPx5Aafx02gjTBtWnEEcaSX3W2dLM3OSdGGyHX/cHl01JQsLaQ==",
"dev": true,
"license": "ISC"
},
"node_modules/enhanced-resolve": {
- "version": "5.18.1",
- "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
- "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
+ "version": "5.18.3",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
+ "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1108,9 +1107,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
- "version": "3.0.6",
- "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
- "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+ "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
"dev": true,
"funding": [
{
@@ -1242,9 +1241,9 @@
}
},
"node_modules/immutable": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
- "integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==",
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz",
+ "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==",
"dev": true,
"license": "MIT"
},
@@ -1529,9 +1528,9 @@
"optional": true
},
"node_modules/node-releases": {
- "version": "2.0.19",
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
- "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
+ "version": "2.0.20",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.20.tgz",
+ "integrity": "sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA==",
"dev": true,
"license": "MIT"
},
@@ -1636,9 +1635,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.3",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
- "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
@@ -1656,7 +1655,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.8",
+ "nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -1861,9 +1860,9 @@
"license": "MIT"
},
"node_modules/sass": {
- "version": "1.89.2",
- "resolved": "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz",
- "integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==",
+ "version": "1.91.0",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.91.0.tgz",
+ "integrity": "sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2062,24 +2061,28 @@
}
},
"node_modules/tapable": {
- "version": "2.2.2",
- "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
- "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz",
+ "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
}
},
"node_modules/terser": {
- "version": "5.39.2",
- "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz",
- "integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==",
+ "version": "5.44.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
+ "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
- "acorn": "^8.14.0",
+ "acorn": "^8.15.0",
"commander": "^2.20.0",
"source-map-support": "~0.5.20"
},
@@ -2148,9 +2151,9 @@
}
},
"node_modules/undici-types": {
- "version": "6.21.0",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
- "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "version": "7.10.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
+ "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
"dev": true,
"license": "MIT"
},
@@ -2207,22 +2210,23 @@
}
},
"node_modules/webpack": {
- "version": "5.99.8",
- "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz",
- "integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==",
+ "version": "5.101.3",
+ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz",
+ "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint-scope": "^3.7.7",
- "@types/estree": "^1.0.6",
+ "@types/estree": "^1.0.8",
"@types/json-schema": "^7.0.15",
"@webassemblyjs/ast": "^1.14.1",
"@webassemblyjs/wasm-edit": "^1.14.1",
"@webassemblyjs/wasm-parser": "^1.14.1",
- "acorn": "^8.14.0",
+ "acorn": "^8.15.0",
+ "acorn-import-phases": "^1.0.3",
"browserslist": "^4.24.0",
"chrome-trace-event": "^1.0.2",
- "enhanced-resolve": "^5.17.1",
+ "enhanced-resolve": "^5.17.3",
"es-module-lexer": "^1.2.1",
"eslint-scope": "5.1.1",
"events": "^3.2.0",
@@ -2236,7 +2240,7 @@
"tapable": "^2.1.1",
"terser-webpack-plugin": "^5.3.11",
"watchpack": "^2.4.1",
- "webpack-sources": "^3.2.3"
+ "webpack-sources": "^3.3.3"
},
"bin": {
"webpack": "bin/webpack.js"
@@ -2326,9 +2330,9 @@
}
},
"node_modules/webpack-sources": {
- "version": "3.2.3",
- "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
- "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
+ "version": "3.3.3",
+ "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
+ "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
"dev": true,
"license": "MIT",
"engines": {
diff --git a/src/Admin/package.json b/src/Admin/package.json
index 9076a46239..89ee1c5358 100644
--- a/src/Admin/package.json
+++ b/src/Admin/package.json
@@ -17,9 +17,9 @@
"css-loader": "7.1.2",
"expose-loader": "5.0.1",
"mini-css-extract-plugin": "2.9.2",
- "sass": "1.89.2",
+ "sass": "1.91.0",
"sass-loader": "16.0.5",
- "webpack": "5.99.8",
+ "webpack": "5.101.3",
"webpack-cli": "5.1.4"
}
}
diff --git a/src/Api/AdminConsole/Authorization/AuthorizationHandlerCollectionExtensions.cs b/src/Api/AdminConsole/Authorization/AuthorizationHandlerCollectionExtensions.cs
new file mode 100644
index 0000000000..ed628105e0
--- /dev/null
+++ b/src/Api/AdminConsole/Authorization/AuthorizationHandlerCollectionExtensions.cs
@@ -0,0 +1,21 @@
+using Bit.Api.Vault.AuthorizationHandlers.Collections;
+using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+
+namespace Bit.Api.AdminConsole.Authorization;
+
+public static class AuthorizationHandlerCollectionExtensions
+{
+ public static void AddAdminConsoleAuthorizationHandlers(this IServiceCollection services)
+ {
+ services.TryAddScoped
();
+
+ services.TryAddEnumerable([
+ ServiceDescriptor.Scoped(),
+ ServiceDescriptor.Scoped(),
+ ServiceDescriptor.Scoped(),
+ ServiceDescriptor.Scoped(),
+ ]);
+ }
+}
diff --git a/src/Api/AdminConsole/Authorization/HttpContextExtensions.cs b/src/Api/AdminConsole/Authorization/HttpContextExtensions.cs
index accb9539fa..5cb261b41d 100644
--- a/src/Api/AdminConsole/Authorization/HttpContextExtensions.cs
+++ b/src/Api/AdminConsole/Authorization/HttpContextExtensions.cs
@@ -1,6 +1,4 @@
-#nullable enable
-
-using Bit.Core.AdminConsole.Enums.Provider;
+using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.AdminConsole.Repositories;
diff --git a/src/Api/AdminConsole/Authorization/OrganizationClaimsExtensions.cs b/src/Api/AdminConsole/Authorization/OrganizationClaimsExtensions.cs
index e21d153bab..9ea01bd21b 100644
--- a/src/Api/AdminConsole/Authorization/OrganizationClaimsExtensions.cs
+++ b/src/Api/AdminConsole/Authorization/OrganizationClaimsExtensions.cs
@@ -1,9 +1,7 @@
-#nullable enable
-
-using System.Security.Claims;
+using System.Security.Claims;
+using Bit.Core.Auth.Identity;
using Bit.Core.Context;
using Bit.Core.Enums;
-using Bit.Core.Identity;
using Bit.Core.Models.Data;
namespace Bit.Api.AdminConsole.Authorization;
diff --git a/src/Api/AdminConsole/Authorization/OrganizationContext.cs b/src/Api/AdminConsole/Authorization/OrganizationContext.cs
new file mode 100644
index 0000000000..7b06e33dfd
--- /dev/null
+++ b/src/Api/AdminConsole/Authorization/OrganizationContext.cs
@@ -0,0 +1,84 @@
+using System.Security.Claims;
+using Bit.Core.AdminConsole.Enums.Provider;
+using Bit.Core.AdminConsole.Models.Data.Provider;
+using Bit.Core.AdminConsole.Repositories;
+using Bit.Core.Context;
+using Bit.Core.Services;
+
+// Note: do not move this into Core! See remarks below.
+namespace Bit.Api.AdminConsole.Authorization;
+
+///
+/// Provides information about a user's membership or provider relationship with an organization.
+/// Used for authorization decisions in the API layer, usually called by a controller or authorization handler or attribute.
+///
+///
+/// This is intended to deprecate organization-related methods in .
+/// It should remain in the API layer (not Core) because it is closely tied to user claims and authentication.
+///
+public interface IOrganizationContext
+{
+ ///
+ /// Parses the provided for claims relating to the specified organization.
+ /// A user will have organization claims if they are a confirmed member of the organization.
+ ///
+ /// The claims for the user.
+ /// The organization to extract claims for.
+ ///
+ /// A representing the user's claims for the organization,
+ /// or null if the user has no claims.
+ ///
+ public CurrentContextOrganization? GetOrganizationClaims(ClaimsPrincipal user, Guid organizationId);
+ ///
+ /// Used to determine whether the user is a ProviderUser for the specified organization.
+ ///
+ /// The claims for the user.
+ /// The organization to check the provider relationship for.
+ /// True if the user is a ProviderUser for the specified organization, otherwise false.
+ ///
+ /// This requires a database call, but the results are cached for the lifetime of the service instance.
+ /// Try to check purely claims-based sources of authorization first (such as organization membership with
+ /// ) to avoid unnecessary database calls.
+ ///
+ public Task IsProviderUserForOrganization(ClaimsPrincipal user, Guid organizationId);
+}
+
+public class OrganizationContext(
+ IUserService userService,
+ IProviderUserRepository providerUserRepository) : IOrganizationContext
+{
+ public const string NoUserIdError = "This method should only be called on the private api with a logged in user.";
+
+ ///
+ /// Caches provider relationships by UserId.
+ /// In practice this should only have 1 entry (for the current user), but this approach ensures that a mix-up
+ /// between users cannot occur if is called with a different
+ /// ClaimsPrincipal for any reason.
+ ///
+ private readonly Dictionary> _providerUserOrganizationsCache = new();
+
+ public CurrentContextOrganization? GetOrganizationClaims(ClaimsPrincipal user, Guid organizationId)
+ {
+ return user.GetCurrentContextOrganization(organizationId);
+ }
+
+ public async Task IsProviderUserForOrganization(ClaimsPrincipal user, Guid organizationId)
+ {
+ var userId = userService.GetProperUserId(user);
+ if (!userId.HasValue)
+ {
+ throw new InvalidOperationException(NoUserIdError);
+ }
+
+ if (!_providerUserOrganizationsCache.TryGetValue(userId.Value, out var providerUserOrganizations))
+ {
+ providerUserOrganizations =
+ await providerUserRepository.GetManyOrganizationDetailsByUserAsync(userId.Value,
+ ProviderUserStatusType.Confirmed);
+ providerUserOrganizations = providerUserOrganizations.ToList();
+ _providerUserOrganizationsCache[userId.Value] = providerUserOrganizations;
+ }
+
+ return providerUserOrganizations.Any(o => o.OrganizationId == organizationId);
+ }
+}
diff --git a/src/Api/AdminConsole/Controllers/EventsController.cs b/src/Api/AdminConsole/Controllers/EventsController.cs
index 18199ad8f2..f868f0b3b6 100644
--- a/src/Api/AdminConsole/Controllers/EventsController.cs
+++ b/src/Api/AdminConsole/Controllers/EventsController.cs
@@ -30,6 +30,8 @@ public class EventsController : Controller
private readonly ICurrentContext _currentContext;
private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository;
+ private readonly IServiceAccountRepository _serviceAccountRepository;
+
public EventsController(
IUserService userService,
@@ -39,7 +41,8 @@ public class EventsController : Controller
IEventRepository eventRepository,
ICurrentContext currentContext,
ISecretRepository secretRepository,
- IProjectRepository projectRepository)
+ IProjectRepository projectRepository,
+ IServiceAccountRepository serviceAccountRepository)
{
_userService = userService;
_cipherRepository = cipherRepository;
@@ -49,6 +52,7 @@ public class EventsController : Controller
_currentContext = currentContext;
_secretRepository = secretRepository;
_projectRepository = projectRepository;
+ _serviceAccountRepository = serviceAccountRepository;
}
[HttpGet("")]
@@ -184,6 +188,57 @@ public class EventsController : Controller
return new ListResponseModel(responses, result.ContinuationToken);
}
+ [HttpGet("~/organization/{orgId}/service-account/{id}/events")]
+ public async Task> GetServiceAccounts(
+ Guid orgId,
+ Guid id,
+ [FromQuery] DateTime? start = null,
+ [FromQuery] DateTime? end = null,
+ [FromQuery] string continuationToken = null)
+ {
+ if (id == Guid.Empty || orgId == Guid.Empty)
+ {
+ throw new NotFoundException();
+ }
+
+ var serviceAccount = await GetServiceAccount(id, orgId);
+ var org = _currentContext.GetOrganization(orgId);
+
+ if (org == null || !await _currentContext.AccessEventLogs(org.Id))
+ {
+ throw new NotFoundException();
+ }
+
+ var (fromDate, toDate) = ApiHelpers.GetDateRange(start, end);
+ var result = await _eventRepository.GetManyByOrganizationServiceAccountAsync(
+ serviceAccount.OrganizationId,
+ serviceAccount.Id,
+ fromDate,
+ toDate,
+ new PageOptions { ContinuationToken = continuationToken });
+
+ var responses = result.Data.Select(e => new EventResponseModel(e));
+ return new ListResponseModel(responses, result.ContinuationToken);
+ }
+
+ [ApiExplorerSettings(IgnoreApi = true)]
+ private async Task GetServiceAccount(Guid serviceAccountId, Guid orgId)
+ {
+ var serviceAccount = await _serviceAccountRepository.GetByIdAsync(serviceAccountId);
+ if (serviceAccount != null)
+ {
+ return serviceAccount;
+ }
+
+ var fallbackServiceAccount = new ServiceAccount
+ {
+ Id = serviceAccountId,
+ OrganizationId = orgId
+ };
+
+ return fallbackServiceAccount;
+ }
+
[HttpGet("~/organizations/{orgId}/users/{id}/events")]
public async Task> GetOrganizationUser(string orgId, string id,
[FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null, [FromQuery] string continuationToken = null)
diff --git a/src/Api/AdminConsole/Controllers/GroupsController.cs b/src/Api/AdminConsole/Controllers/GroupsController.cs
index f8e97881cb..4587e54aee 100644
--- a/src/Api/AdminConsole/Controllers/GroupsController.cs
+++ b/src/Api/AdminConsole/Controllers/GroupsController.cs
@@ -163,7 +163,6 @@ public class GroupsController : Controller
}
[HttpPut("{id}")]
- [HttpPost("{id}")]
public async Task Put(Guid orgId, Guid id, [FromBody] GroupRequestModel model)
{
if (!await _currentContext.ManageGroups(orgId))
@@ -237,8 +236,14 @@ public class GroupsController : Controller
return new GroupResponseModel(group);
}
+ [HttpPost("{id}")]
+ [Obsolete("This endpoint is deprecated. Use PUT method instead")]
+ public async Task PostPut(Guid orgId, Guid id, [FromBody] GroupRequestModel model)
+ {
+ return await Put(orgId, id, model);
+ }
+
[HttpDelete("{id}")]
- [HttpPost("{id}/delete")]
public async Task Delete(string orgId, string id)
{
var group = await _groupRepository.GetByIdAsync(new Guid(id));
@@ -250,8 +255,14 @@ public class GroupsController : Controller
await _deleteGroupCommand.DeleteAsync(group);
}
+ [HttpPost("{id}/delete")]
+ [Obsolete("This endpoint is deprecated. Use DELETE method instead")]
+ public async Task PostDelete(string orgId, string id)
+ {
+ await Delete(orgId, id);
+ }
+
[HttpDelete("")]
- [HttpPost("delete")]
public async Task BulkDelete([FromBody] GroupBulkRequestModel model)
{
var groups = await _groupRepository.GetManyByManyIds(model.Ids);
@@ -267,9 +278,15 @@ public class GroupsController : Controller
await _deleteGroupCommand.DeleteManyAsync(groups);
}
+ [HttpPost("delete")]
+ [Obsolete("This endpoint is deprecated. Use DELETE method instead")]
+ public async Task PostBulkDelete([FromBody] GroupBulkRequestModel model)
+ {
+ await BulkDelete(model);
+ }
+
[HttpDelete("{id}/user/{orgUserId}")]
- [HttpPost("{id}/delete-user/{orgUserId}")]
- public async Task Delete(string orgId, string id, string orgUserId)
+ public async Task DeleteUser(string orgId, string id, string orgUserId)
{
var group = await _groupRepository.GetByIdAsync(new Guid(id));
if (group == null || !await _currentContext.ManageGroups(group.OrganizationId))
@@ -279,4 +296,11 @@ public class GroupsController : Controller
await _groupService.DeleteUserAsync(group, new Guid(orgUserId));
}
+
+ [HttpPost("{id}/delete-user/{orgUserId}")]
+ [Obsolete("This endpoint is deprecated. Use DELETE method instead")]
+ public async Task PostDeleteUser(string orgId, string id, string orgUserId)
+ {
+ await DeleteUser(orgId, id, orgUserId);
+ }
}
diff --git a/src/Api/AdminConsole/Controllers/OrganizationConnectionsController.cs b/src/Api/AdminConsole/Controllers/OrganizationConnectionsController.cs
index 79ed2ceabe..776e28d2a3 100644
--- a/src/Api/AdminConsole/Controllers/OrganizationConnectionsController.cs
+++ b/src/Api/AdminConsole/Controllers/OrganizationConnectionsController.cs
@@ -140,7 +140,6 @@ public class OrganizationConnectionsController : Controller
}
[HttpDelete("{organizationConnectionId}")]
- [HttpPost("{organizationConnectionId}/delete")]
public async Task DeleteConnection(Guid organizationConnectionId)
{
var connection = await _organizationConnectionRepository.GetByIdAsync(organizationConnectionId);
@@ -158,6 +157,13 @@ public class OrganizationConnectionsController : Controller
await _deleteOrganizationConnectionCommand.DeleteAsync(connection);
}
+ [HttpPost("{organizationConnectionId}/delete")]
+ [Obsolete("This endpoint is deprecated. Use DELETE method instead")]
+ public async Task PostDeleteConnection(Guid organizationConnectionId)
+ {
+ await DeleteConnection(organizationConnectionId);
+ }
+
private async Task> GetConnectionsAsync(Guid organizationId, OrganizationConnectionType type) =>
await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organizationId, type);
diff --git a/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs b/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs
index a8882dfaf3..15cfafe240 100644
--- a/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs
+++ b/src/Api/AdminConsole/Controllers/OrganizationDomainController.cs
@@ -46,7 +46,7 @@ public class OrganizationDomainController : Controller
}
[HttpGet("{orgId}/domain")]
- public async Task> Get(Guid orgId)
+ public async Task> GetAll(Guid orgId)
{
await ValidateOrganizationAccessAsync(orgId);
@@ -105,7 +105,6 @@ public class OrganizationDomainController : Controller
}
[HttpDelete("{orgId}/domain/{id}")]
- [HttpPost("{orgId}/domain/{id}/remove")]
public async Task RemoveDomain(Guid orgId, Guid id)
{
await ValidateOrganizationAccessAsync(orgId);
@@ -119,6 +118,13 @@ public class OrganizationDomainController : Controller
await _deleteOrganizationDomainCommand.DeleteAsync(domain);
}
+ [HttpPost("{orgId}/domain/{id}/remove")]
+ [Obsolete("This endpoint is deprecated. Use DELETE method instead")]
+ public async Task PostRemoveDomain(Guid orgId, Guid id)
+ {
+ await RemoveDomain(orgId, id);
+ }
+
[AllowAnonymous]
[HttpPost("domain/sso/details")] // must be post to accept email cleanly
public async Task GetOrgDomainSsoDetails(
diff --git a/src/Api/AdminConsole/Controllers/OrganizationIntegrationConfigurationController.cs b/src/Api/AdminConsole/Controllers/OrganizationIntegrationConfigurationController.cs
index 319fbbe707..ae0f91d355 100644
--- a/src/Api/AdminConsole/Controllers/OrganizationIntegrationConfigurationController.cs
+++ b/src/Api/AdminConsole/Controllers/OrganizationIntegrationConfigurationController.cs
@@ -98,7 +98,6 @@ public class OrganizationIntegrationConfigurationController(
}
[HttpDelete("{configurationId:guid}")]
- [HttpPost("{configurationId:guid}/delete")]
public async Task DeleteAsync(Guid organizationId, Guid integrationId, Guid configurationId)
{
if (!await HasPermission(organizationId))
@@ -120,6 +119,13 @@ public class OrganizationIntegrationConfigurationController(
await integrationConfigurationRepository.DeleteAsync(configuration);
}
+ [HttpPost("{configurationId:guid}/delete")]
+ [Obsolete("This endpoint is deprecated. Use DELETE method instead")]
+ public async Task PostDeleteAsync(Guid organizationId, Guid integrationId, Guid configurationId)
+ {
+ await DeleteAsync(organizationId, integrationId, configurationId);
+ }
+
private async Task HasPermission(Guid organizationId)
{
return await currentContext.OrganizationOwner(organizationId);
diff --git a/src/Api/AdminConsole/Controllers/OrganizationIntegrationController.cs b/src/Api/AdminConsole/Controllers/OrganizationIntegrationController.cs
index 7052350c9a..a12492949d 100644
--- a/src/Api/AdminConsole/Controllers/OrganizationIntegrationController.cs
+++ b/src/Api/AdminConsole/Controllers/OrganizationIntegrationController.cs
@@ -64,7 +64,6 @@ public class OrganizationIntegrationController(
}
[HttpDelete("{integrationId:guid}")]
- [HttpPost("{integrationId:guid}/delete")]
public async Task DeleteAsync(Guid organizationId, Guid integrationId)
{
if (!await HasPermission(organizationId))
@@ -81,6 +80,13 @@ public class OrganizationIntegrationController(
await integrationRepository.DeleteAsync(integration);
}
+ [HttpPost("{integrationId:guid}/delete")]
+ [Obsolete("This endpoint is deprecated. Use DELETE method instead")]
+ public async Task PostDeleteAsync(Guid organizationId, Guid integrationId)
+ {
+ await DeleteAsync(organizationId, integrationId);
+ }
+
private async Task HasPermission(Guid organizationId)
{
return await currentContext.OrganizationOwner(organizationId);
diff --git a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs
index 2b464c24e2..74ac9b1255 100644
--- a/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs
+++ b/src/Api/AdminConsole/Controllers/OrganizationUsersController.cs
@@ -11,6 +11,7 @@ using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Core;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
+using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccount;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
@@ -23,6 +24,7 @@ using Bit.Core.Billing.Pricing;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
+using Bit.Core.Models.Api;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
@@ -167,7 +169,7 @@ public class OrganizationUsersController : Controller
}
[HttpGet("")]
- public async Task> Get(Guid orgId, bool includeGroups = false, bool includeCollections = false)
+ public async Task> GetAll(Guid orgId, bool includeGroups = false, bool includeCollections = false)
{
var request = new OrganizationUserUserDetailsQueryRequest
{
@@ -360,7 +362,6 @@ public class OrganizationUsersController : Controller
}
[HttpPut("{id}")]
- [HttpPost("{id}")]
[Authorize]
public async Task Put(Guid orgId, Guid id, [FromBody] OrganizationUserUpdateRequestModel model)
{
@@ -436,6 +437,14 @@ public class OrganizationUsersController : Controller
collectionsToSave, groupsToSave);
}
+ [HttpPost("{id}")]
+ [Obsolete("This endpoint is deprecated. Use PUT method instead")]
+ [Authorize]
+ public async Task PostPut(Guid orgId, Guid id, [FromBody] OrganizationUserUpdateRequestModel model)
+ {
+ await Put(orgId, id, model);
+ }
+
[HttpPut("{userId}/reset-password-enrollment")]
public async Task PutResetPasswordEnrollment(Guid orgId, Guid userId, [FromBody] OrganizationUserResetPasswordEnrollmentRequestModel model)
{
@@ -492,7 +501,6 @@ public class OrganizationUsersController : Controller
}
[HttpDelete("{id}")]
- [HttpPost("{id}/remove")]
[Authorize]
public async Task Remove(Guid orgId, Guid id)
{
@@ -500,8 +508,15 @@ public class OrganizationUsersController : Controller
await _removeOrganizationUserCommand.RemoveUserAsync(orgId, id, userId.Value);
}
+ [HttpPost("{id}/remove")]
+ [Obsolete("This endpoint is deprecated. Use DELETE method instead")]
+ [Authorize]
+ public async Task PostRemove(Guid orgId, Guid id)
+ {
+ await Remove(orgId, id);
+ }
+
[HttpDelete("")]
- [HttpPost("remove")]
[Authorize]
public async Task> BulkRemove(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
{
@@ -511,38 +526,70 @@ public class OrganizationUsersController : Controller
new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage)));
}
- [HttpDelete("{id}/delete-account")]
- [HttpPost("{id}/delete-account")]
+ [HttpPost("remove")]
+ [Obsolete("This endpoint is deprecated. Use DELETE method instead")]
[Authorize]
- public async Task DeleteAccount(Guid orgId, Guid id)
+ public async Task> PostBulkRemove(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
{
- var currentUser = await _userService.GetUserByPrincipalAsync(User);
- if (currentUser == null)
+ return await BulkRemove(orgId, model);
+ }
+
+ [HttpDelete("{id}/delete-account")]
+ [Authorize]
+ public async Task DeleteAccount(Guid orgId, Guid id)
+ {
+ var currentUserId = _userService.GetProperUserId(User);
+ if (currentUserId == null)
{
- throw new UnauthorizedAccessException();
+ return TypedResults.Unauthorized();
}
- await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
+ var commandResult = await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUserId.Value);
+
+ return commandResult.Result.Match(
+ error => error is NotFoundError
+ ? TypedResults.NotFound(new ErrorResponseModel(error.Message))
+ : TypedResults.BadRequest(new ErrorResponseModel(error.Message)),
+ TypedResults.Ok
+ );
+ }
+
+ [HttpPost("{id}/delete-account")]
+ [Obsolete("This endpoint is deprecated. Use DELETE method instead")]
+ [Authorize]
+ public async Task PostDeleteAccount(Guid orgId, Guid id)
+ {
+ await DeleteAccount(orgId, id);
}
[HttpDelete("delete-account")]
- [HttpPost("delete-account")]
[Authorize]
public async Task> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
{
- var currentUser = await _userService.GetUserByPrincipalAsync(User);
- if (currentUser == null)
+ var currentUserId = _userService.GetProperUserId(User);
+ if (currentUserId == null)
{
throw new UnauthorizedAccessException();
}
- var results = await _deleteClaimedOrganizationUserAccountCommand.DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id);
+ var result = await _deleteClaimedOrganizationUserAccountCommand.DeleteManyUsersAsync(orgId, model.Ids, currentUserId.Value);
- return new ListResponseModel(results.Select(r =>
- new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage)));
+ var responses = result.Select(r => r.Result.Match(
+ error => new OrganizationUserBulkResponseModel(r.Id, error.Message),
+ _ => new OrganizationUserBulkResponseModel(r.Id, string.Empty)
+ ));
+
+ return new ListResponseModel(responses);
+ }
+
+ [HttpPost("delete-account")]
+ [Obsolete("This endpoint is deprecated. Use DELETE method instead")]
+ [Authorize]
+ public async Task> PostBulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
+ {
+ return await BulkDeleteAccount(orgId, model);
}
- [HttpPatch("{id}/revoke")]
[HttpPut("{id}/revoke")]
[Authorize]
public async Task RevokeAsync(Guid orgId, Guid id)
@@ -550,7 +597,14 @@ public class OrganizationUsersController : Controller
await RestoreOrRevokeUserAsync(orgId, id, _revokeOrganizationUserCommand.RevokeUserAsync);
}
- [HttpPatch("revoke")]
+ [HttpPatch("{id}/revoke")]
+ [Obsolete("This endpoint is deprecated. Use PUT method instead")]
+ [Authorize]
+ public async Task PatchRevokeAsync(Guid orgId, Guid id)
+ {
+ await RevokeAsync(orgId, id);
+ }
+
[HttpPut("revoke")]
[Authorize]
public async Task> BulkRevokeAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
@@ -558,7 +612,14 @@ public class OrganizationUsersController : Controller
return await RestoreOrRevokeUsersAsync(orgId, model, _revokeOrganizationUserCommand.RevokeUsersAsync);
}
- [HttpPatch("{id}/restore")]
+ [HttpPatch("revoke")]
+ [Obsolete("This endpoint is deprecated. Use PUT method instead")]
+ [Authorize]
+ public async Task> PatchBulkRevokeAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
+ {
+ return await BulkRevokeAsync(orgId, model);
+ }
+
[HttpPut("{id}/restore")]
[Authorize]
public async Task RestoreAsync(Guid orgId, Guid id)
@@ -566,7 +627,14 @@ public class OrganizationUsersController : Controller
await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _restoreOrganizationUserCommand.RestoreUserAsync(orgUser, userId));
}
- [HttpPatch("restore")]
+ [HttpPatch("{id}/restore")]
+ [Obsolete("This endpoint is deprecated. Use PUT method instead")]
+ [Authorize]
+ public async Task PatchRestoreAsync(Guid orgId, Guid id)
+ {
+ await RestoreAsync(orgId, id);
+ }
+
[HttpPut("restore")]
[Authorize]
public async Task> BulkRestoreAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
@@ -574,7 +642,14 @@ public class OrganizationUsersController : Controller
return await RestoreOrRevokeUsersAsync(orgId, model, (orgId, orgUserIds, restoringUserId) => _restoreOrganizationUserCommand.RestoreUsersAsync(orgId, orgUserIds, restoringUserId, _userService));
}
- [HttpPatch("enable-secrets-manager")]
+ [HttpPatch("restore")]
+ [Obsolete("This endpoint is deprecated. Use PUT method instead")]
+ [Authorize]
+ public async Task> PatchBulkRestoreAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
+ {
+ return await BulkRestoreAsync(orgId, model);
+ }
+
[HttpPut("enable-secrets-manager")]
[Authorize]
public async Task BulkEnableSecretsManagerAsync(Guid orgId,
@@ -607,6 +682,15 @@ public class OrganizationUsersController : Controller
await _organizationUserRepository.ReplaceManyAsync(orgUsers);
}
+ [HttpPatch("enable-secrets-manager")]
+ [Obsolete("This endpoint is deprecated. Use PUT method instead")]
+ [Authorize]
+ public async Task PatchBulkEnableSecretsManagerAsync(Guid orgId,
+ [FromBody] OrganizationUserBulkRequestModel model)
+ {
+ await BulkEnableSecretsManagerAsync(orgId, model);
+ }
+
private async Task RestoreOrRevokeUserAsync(
Guid orgId,
Guid id,
diff --git a/src/Api/AdminConsole/Controllers/OrganizationsController.cs b/src/Api/AdminConsole/Controllers/OrganizationsController.cs
index 8b1a6243c3..590895665d 100644
--- a/src/Api/AdminConsole/Controllers/OrganizationsController.cs
+++ b/src/Api/AdminConsole/Controllers/OrganizationsController.cs
@@ -225,7 +225,6 @@ public class OrganizationsController : Controller
}
[HttpPut("{id}")]
- [HttpPost("{id}")]
public async Task Put(string id, [FromBody] OrganizationUpdateRequestModel model)
{
var orgIdGuid = new Guid(id);
@@ -252,6 +251,13 @@ public class OrganizationsController : Controller
return new OrganizationResponseModel(organization, plan);
}
+ [HttpPost("{id}")]
+ [Obsolete("This endpoint is deprecated. Use PUT method instead")]
+ public async Task PostPut(string id, [FromBody] OrganizationUpdateRequestModel model)
+ {
+ return await Put(id, model);
+ }
+
[HttpPost("{id}/storage")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task PostStorage(string id, [FromBody] StorageRequestModel model)
@@ -291,7 +297,6 @@ public class OrganizationsController : Controller
}
[HttpDelete("{id}")]
- [HttpPost("{id}/delete")]
public async Task Delete(string id, [FromBody] SecretVerificationRequestModel model)
{
var orgIdGuid = new Guid(id);
@@ -334,6 +339,13 @@ public class OrganizationsController : Controller
await _organizationDeleteCommand.DeleteAsync(organization);
}
+ [HttpPost("{id}/delete")]
+ [Obsolete("This endpoint is deprecated. Use DELETE method instead")]
+ public async Task PostDelete(string id, [FromBody] SecretVerificationRequestModel model)
+ {
+ await Delete(id, model);
+ }
+
[HttpPost("{id}/delete-recover-token")]
[AllowAnonymous]
public async Task PostDeleteRecoverToken(Guid id, [FromBody] OrganizationVerifyDeleteRecoverRequestModel model)
@@ -554,18 +566,12 @@ public class OrganizationsController : Controller
[HttpPut("{id}/collection-management")]
public async Task PutCollectionManagement(Guid id, [FromBody] OrganizationCollectionManagementUpdateRequestModel model)
{
- var organization = await _organizationRepository.GetByIdAsync(id);
- if (organization == null)
- {
- throw new NotFoundException();
- }
-
if (!await _currentContext.OrganizationOwner(id))
{
throw new NotFoundException();
}
- await _organizationService.UpdateAsync(model.ToOrganization(organization, _featureService), eventType: EventType.Organization_CollectionManagement_Updated);
+ var organization = await _organizationService.UpdateCollectionManagementSettingsAsync(id, model.ToSettings());
var plan = await _pricingClient.GetPlan(organization.PlanType);
return new OrganizationResponseModel(organization, plan);
}
diff --git a/src/Api/AdminConsole/Controllers/PoliciesController.cs b/src/Api/AdminConsole/Controllers/PoliciesController.cs
index a80546e2f5..ce92321833 100644
--- a/src/Api/AdminConsole/Controllers/PoliciesController.cs
+++ b/src/Api/AdminConsole/Controllers/PoliciesController.cs
@@ -1,10 +1,13 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
+using Bit.Api.AdminConsole.Authorization;
+using Bit.Api.AdminConsole.Authorization.Requirements;
using Bit.Api.AdminConsole.Models.Request;
using Bit.Api.AdminConsole.Models.Response.Helpers;
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.Models.Response;
+using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
@@ -30,7 +33,6 @@ namespace Bit.Api.AdminConsole.Controllers;
public class PoliciesController : Controller
{
private readonly ICurrentContext _currentContext;
- private readonly IFeatureService _featureService;
private readonly GlobalSettings _globalSettings;
private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;
private readonly IOrganizationRepository _organizationRepository;
@@ -49,7 +51,6 @@ public class PoliciesController : Controller
GlobalSettings globalSettings,
IDataProtectionProvider dataProtectionProvider,
IDataProtectorTokenFactory orgUserInviteTokenDataFactory,
- IFeatureService featureService,
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery,
IOrganizationRepository organizationRepository,
ISavePolicyCommand savePolicyCommand)
@@ -63,7 +64,6 @@ public class PoliciesController : Controller
"OrganizationServiceDataProtector");
_organizationRepository = organizationRepository;
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
- _featureService = featureService;
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
_savePolicyCommand = savePolicyCommand;
}
@@ -90,7 +90,7 @@ public class PoliciesController : Controller
}
[HttpGet("")]
- public async Task> Get(string orgId)
+ public async Task> GetAll(string orgId)
{
var orgIdGuid = new Guid(orgId);
if (!await _currentContext.ManagePolicies(orgIdGuid))
@@ -212,4 +212,18 @@ public class PoliciesController : Controller
var policy = await _savePolicyCommand.SaveAsync(policyUpdate);
return new PolicyResponseModel(policy);
}
+
+
+ [HttpPut("{type}/vnext")]
+ [RequireFeatureAttribute(FeatureFlagKeys.CreateDefaultLocation)]
+ [Authorize]
+ public async Task PutVNext(Guid orgId, [FromBody] SavePolicyRequest model)
+ {
+ var savePolicyRequest = await model.ToSavePolicyModelAsync(orgId, _currentContext);
+
+ var policy = await _savePolicyCommand.VNextSaveAsync(savePolicyRequest);
+
+ return new PolicyResponseModel(policy);
+ }
+
}
diff --git a/src/Api/AdminConsole/Controllers/ProviderOrganizationsController.cs b/src/Api/AdminConsole/Controllers/ProviderOrganizationsController.cs
index f68b036be4..11d302ff86 100644
--- a/src/Api/AdminConsole/Controllers/ProviderOrganizationsController.cs
+++ b/src/Api/AdminConsole/Controllers/ProviderOrganizationsController.cs
@@ -93,7 +93,6 @@ public class ProviderOrganizationsController : Controller
}
[HttpDelete("{id:guid}")]
- [HttpPost("{id:guid}/delete")]
public async Task Delete(Guid providerId, Guid id)
{
if (!_currentContext.ManageProviderOrganizations(providerId))
@@ -112,4 +111,11 @@ public class ProviderOrganizationsController : Controller
providerOrganization,
organization);
}
+
+ [HttpPost("{id:guid}/delete")]
+ [Obsolete("This endpoint is deprecated. Use DELETE method instead")]
+ public async Task PostDelete(Guid providerId, Guid id)
+ {
+ await Delete(providerId, id);
+ }
}
diff --git a/src/Api/AdminConsole/Controllers/ProviderUsersController.cs b/src/Api/AdminConsole/Controllers/ProviderUsersController.cs
index b89f553325..dcf9492605 100644
--- a/src/Api/AdminConsole/Controllers/ProviderUsersController.cs
+++ b/src/Api/AdminConsole/Controllers/ProviderUsersController.cs
@@ -49,7 +49,7 @@ public class ProviderUsersController : Controller
}
[HttpGet("")]
- public async Task> Get(Guid providerId)
+ public async Task> GetAll(Guid providerId)
{
if (!_currentContext.ProviderManageUsers(providerId))
{
@@ -155,7 +155,6 @@ public class ProviderUsersController : Controller
}
[HttpPut("{id:guid}")]
- [HttpPost("{id:guid}")]
public async Task Put(Guid providerId, Guid id, [FromBody] ProviderUserUpdateRequestModel model)
{
if (!_currentContext.ProviderManageUsers(providerId))
@@ -173,8 +172,14 @@ public class ProviderUsersController : Controller
await _providerService.SaveUserAsync(model.ToProviderUser(providerUser), userId.Value);
}
+ [HttpPost("{id:guid}")]
+ [Obsolete("This endpoint is deprecated. Use PUT method instead")]
+ public async Task PostPut(Guid providerId, Guid id, [FromBody] ProviderUserUpdateRequestModel model)
+ {
+ await Put(providerId, id, model);
+ }
+
[HttpDelete("{id:guid}")]
- [HttpPost("{id:guid}/delete")]
public async Task Delete(Guid providerId, Guid id)
{
if (!_currentContext.ProviderManageUsers(providerId))
@@ -186,8 +191,14 @@ public class ProviderUsersController : Controller
await _providerService.DeleteUsersAsync(providerId, new[] { id }, userId.Value);
}
+ [HttpPost("{id:guid}/delete")]
+ [Obsolete("This endpoint is deprecated. Use DELETE method instead")]
+ public async Task PostDelete(Guid providerId, Guid id)
+ {
+ await Delete(providerId, id);
+ }
+
[HttpDelete("")]
- [HttpPost("delete")]
public async Task> BulkDelete(Guid providerId, [FromBody] ProviderUserBulkRequestModel model)
{
if (!_currentContext.ProviderManageUsers(providerId))
@@ -200,4 +211,11 @@ public class ProviderUsersController : Controller
return new ListResponseModel(result.Select(r =>
new ProviderUserBulkResponseModel(r.Item1.Id, r.Item2)));
}
+
+ [HttpPost("delete")]
+ [Obsolete("This endpoint is deprecated. Use DELETE method instead")]
+ public async Task> PostBulkDelete(Guid providerId, [FromBody] ProviderUserBulkRequestModel model)
+ {
+ return await BulkDelete(providerId, model);
+ }
}
diff --git a/src/Api/AdminConsole/Controllers/ProvidersController.cs b/src/Api/AdminConsole/Controllers/ProvidersController.cs
index d8bda2ca18..aa87bf9c74 100644
--- a/src/Api/AdminConsole/Controllers/ProvidersController.cs
+++ b/src/Api/AdminConsole/Controllers/ProvidersController.cs
@@ -7,7 +7,6 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Context;
using Bit.Core.Exceptions;
-using Bit.Core.Models.Business;
using Bit.Core.Services;
using Bit.Core.Settings;
using Microsoft.AspNetCore.Authorization;
@@ -53,7 +52,6 @@ public class ProvidersController : Controller
}
[HttpPut("{id:guid}")]
- [HttpPost("{id:guid}")]
public async Task Put(Guid id, [FromBody] ProviderUpdateRequestModel model)
{
if (!_currentContext.ProviderProviderAdmin(id))
@@ -71,6 +69,13 @@ public class ProvidersController : Controller
return new ProviderResponseModel(provider);
}
+ [HttpPost("{id:guid}")]
+ [Obsolete("This endpoint is deprecated. Use PUT method instead")]
+ public async Task PostPut(Guid id, [FromBody] ProviderUpdateRequestModel model)
+ {
+ return await Put(id, model);
+ }
+
[HttpPost("{id:guid}/setup")]
public async Task Setup(Guid id, [FromBody] ProviderSetupRequestModel model)
{
@@ -87,22 +92,12 @@ public class ProvidersController : Controller
var userId = _userService.GetProperUserId(User).Value;
- var taxInfo = new TaxInfo
- {
- BillingAddressCountry = model.TaxInfo.Country,
- BillingAddressPostalCode = model.TaxInfo.PostalCode,
- TaxIdNumber = model.TaxInfo.TaxId,
- BillingAddressLine1 = model.TaxInfo.Line1,
- BillingAddressLine2 = model.TaxInfo.Line2,
- BillingAddressCity = model.TaxInfo.City,
- BillingAddressState = model.TaxInfo.State
- };
-
- var tokenizedPaymentSource = model.PaymentSource?.ToDomain();
+ var paymentMethod = model.PaymentMethod.ToDomain();
+ var billingAddress = model.BillingAddress.ToDomain();
var response =
await _providerService.CompleteSetupAsync(model.ToProvider(provider), userId, model.Token, model.Key,
- taxInfo, tokenizedPaymentSource);
+ paymentMethod, billingAddress);
return new ProviderResponseModel(response);
}
@@ -120,7 +115,6 @@ public class ProvidersController : Controller
}
[HttpDelete("{id}")]
- [HttpPost("{id}/delete")]
public async Task Delete(Guid id)
{
if (!_currentContext.ProviderProviderAdmin(id))
@@ -142,4 +136,11 @@ public class ProvidersController : Controller
await _providerService.DeleteAsync(provider);
}
+
+ [HttpPost("{id}/delete")]
+ [Obsolete("This endpoint is deprecated. Use DELETE method instead")]
+ public async Task PostDelete(Guid id)
+ {
+ await Delete(id);
+ }
}
diff --git a/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs b/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs
index 6e3751c6f6..c8ff4f9f7c 100644
--- a/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs
+++ b/src/Api/AdminConsole/Controllers/SlackIntegrationController.cs
@@ -1,7 +1,4 @@
-// FIXME: Update this file to be null safe and then delete the line below
-#nullable disable
-
-using System.Text.Json;
+using System.Text.Json;
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
@@ -18,25 +15,58 @@ using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.AdminConsole.Controllers;
[RequireFeature(FeatureFlagKeys.EventBasedOrganizationIntegrations)]
-[Route("organizations/{organizationId:guid}/integrations/slack")]
+[Route("organizations")]
[Authorize("Application")]
public class SlackIntegrationController(
ICurrentContext currentContext,
IOrganizationIntegrationRepository integrationRepository,
- ISlackService slackService) : Controller
+ ISlackService slackService,
+ TimeProvider timeProvider) : Controller
{
- [HttpGet("redirect")]
+ [HttpGet("{organizationId:guid}/integrations/slack/redirect")]
public async Task RedirectAsync(Guid organizationId)
{
if (!await currentContext.OrganizationOwner(organizationId))
{
throw new NotFoundException();
}
- string callbackUrl = Url.RouteUrl(
- nameof(CreateAsync),
- new { organizationId },
- currentContext.HttpContext.Request.Scheme);
- var redirectUrl = slackService.GetRedirectUrl(callbackUrl);
+
+ string? callbackUrl = Url.RouteUrl(
+ routeName: nameof(CreateAsync),
+ values: null,
+ protocol: currentContext.HttpContext.Request.Scheme,
+ host: currentContext.HttpContext.Request.Host.ToUriComponent()
+ );
+ if (string.IsNullOrEmpty(callbackUrl))
+ {
+ throw new BadRequestException("Unable to build callback Url");
+ }
+
+ var integrations = await integrationRepository.GetManyByOrganizationAsync(organizationId);
+ var integration = integrations.FirstOrDefault(i => i.Type == IntegrationType.Slack);
+
+ if (integration is null)
+ {
+ // No slack integration exists, create Initiated version
+ integration = await integrationRepository.CreateAsync(new OrganizationIntegration
+ {
+ OrganizationId = organizationId,
+ Type = IntegrationType.Slack,
+ Configuration = null,
+ });
+ }
+ else if (integration.Configuration is not null)
+ {
+ // A Completed (fully configured) Slack integration already exists, throw to prevent overriding
+ throw new BadRequestException("There already exists a Slack integration for this organization");
+
+ } // An Initiated slack integration exits, re-use it and kick off a new OAuth flow
+
+ var state = IntegrationOAuthState.FromIntegration(integration, timeProvider);
+ var redirectUrl = slackService.GetRedirectUrl(
+ callbackUrl: callbackUrl,
+ state: state.ToString()
+ );
if (string.IsNullOrEmpty(redirectUrl))
{
@@ -46,23 +76,42 @@ public class SlackIntegrationController(
return Redirect(redirectUrl);
}
- [HttpGet("create", Name = nameof(CreateAsync))]
- public async Task CreateAsync(Guid organizationId, [FromQuery] string code)
+ [HttpGet("integrations/slack/create", Name = nameof(CreateAsync))]
+ [AllowAnonymous]
+ public async Task CreateAsync([FromQuery] string code, [FromQuery] string state)
{
- if (!await currentContext.OrganizationOwner(organizationId))
+ var oAuthState = IntegrationOAuthState.FromString(state: state, timeProvider: timeProvider);
+ if (oAuthState is null)
{
throw new NotFoundException();
}
- if (string.IsNullOrEmpty(code))
+ // Fetch existing Initiated record
+ var integration = await integrationRepository.GetByIdAsync(oAuthState.IntegrationId);
+ if (integration is null ||
+ integration.Type != IntegrationType.Slack ||
+ integration.Configuration is not null)
{
- throw new BadRequestException("Missing code from Slack.");
+ throw new NotFoundException();
}
- string callbackUrl = Url.RouteUrl(
- nameof(CreateAsync),
- new { organizationId },
- currentContext.HttpContext.Request.Scheme);
+ // Verify Organization matches hash
+ if (!oAuthState.ValidateOrg(integration.OrganizationId))
+ {
+ throw new NotFoundException();
+ }
+
+ // Fetch token from Slack and store to DB
+ string? callbackUrl = Url.RouteUrl(
+ routeName: nameof(CreateAsync),
+ values: null,
+ protocol: currentContext.HttpContext.Request.Scheme,
+ host: currentContext.HttpContext.Request.Host.ToUriComponent()
+ );
+ if (string.IsNullOrEmpty(callbackUrl))
+ {
+ throw new BadRequestException("Unable to build callback Url");
+ }
var token = await slackService.ObtainTokenViaOAuth(code, callbackUrl);
if (string.IsNullOrEmpty(token))
@@ -70,14 +119,10 @@ public class SlackIntegrationController(
throw new BadRequestException("Invalid response from Slack.");
}
- var integration = await integrationRepository.CreateAsync(new OrganizationIntegration
- {
- OrganizationId = organizationId,
- Type = IntegrationType.Slack,
- Configuration = JsonSerializer.Serialize(new SlackIntegration(token)),
- });
- var location = $"/organizations/{organizationId}/integrations/{integration.Id}";
+ integration.Configuration = JsonSerializer.Serialize(new SlackIntegration(token));
+ await integrationRepository.UpsertAsync(integration);
+ var location = $"/organizations/{integration.OrganizationId}/integrations/{integration.Id}";
return Created(location, new OrganizationIntegrationResponseModel(integration));
}
}
diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs
index 10f938adfe..7754c44c8c 100644
--- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs
+++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationCreateRequestModel.cs
@@ -3,6 +3,7 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
+using Bit.Core;
using Bit.Core.Billing.Enums;
using Bit.Core.Entities;
using Bit.Core.Enums;
@@ -139,7 +140,7 @@ public class OrganizationCreateRequestModel : IValidatableObject
new string[] { nameof(BillingAddressCountry) });
}
- if (PlanType != PlanType.Free && BillingAddressCountry == "US" &&
+ if (PlanType != PlanType.Free && BillingAddressCountry == Constants.CountryAbbreviations.UnitedStates &&
string.IsNullOrWhiteSpace(BillingAddressPostalCode))
{
yield return new ValidationResult("Zip / postal code is required.",
diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs
index 17e116b8d1..7d1efe2315 100644
--- a/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs
+++ b/src/Api/AdminConsole/Models/Request/Organizations/OrganizationIntegrationConfigurationRequestModel.cs
@@ -1,6 +1,4 @@
-#nullable enable
-
-using System.Text.Json;
+using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Enums;
@@ -36,6 +34,10 @@ public class OrganizationIntegrationConfigurationRequestModel
return !string.IsNullOrWhiteSpace(Template) &&
Configuration is null &&
IsFiltersValid();
+ case IntegrationType.Datadog:
+ return !string.IsNullOrWhiteSpace(Template) &&
+ Configuration is null &&
+ IsFiltersValid();
default:
return false;
diff --git a/src/Api/AdminConsole/Models/Request/Organizations/OrgnizationIntegrationRequestModel.cs b/src/Api/AdminConsole/Models/Request/Organizations/OrgnizationIntegrationRequestModel.cs
index edae0719e3..92d65ab8fe 100644
--- a/src/Api/AdminConsole/Models/Request/Organizations/OrgnizationIntegrationRequestModel.cs
+++ b/src/Api/AdminConsole/Models/Request/Organizations/OrgnizationIntegrationRequestModel.cs
@@ -4,15 +4,13 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
using Bit.Core.Enums;
-#nullable enable
-
namespace Bit.Api.AdminConsole.Models.Request.Organizations;
public class OrganizationIntegrationRequestModel : IValidatableObject
{
- public string? Configuration { get; set; }
+ public string? Configuration { get; init; }
- public IntegrationType Type { get; set; }
+ public IntegrationType Type { get; init; }
public OrganizationIntegration ToOrganizationIntegration(Guid organizationId)
{
@@ -35,54 +33,55 @@ public class OrganizationIntegrationRequestModel : IValidatableObject
switch (Type)
{
case IntegrationType.CloudBillingSync or IntegrationType.Scim:
- yield return new ValidationResult($"{nameof(Type)} integrations are not yet supported.", new[] { nameof(Type) });
+ yield return new ValidationResult($"{nameof(Type)} integrations are not yet supported.", [nameof(Type)]);
break;
case IntegrationType.Slack:
- yield return new ValidationResult($"{nameof(Type)} integrations cannot be created directly.", new[] { nameof(Type) });
+ yield return new ValidationResult($"{nameof(Type)} integrations cannot be created directly.", [nameof(Type)]);
break;
case IntegrationType.Webhook:
- if (string.IsNullOrWhiteSpace(Configuration))
- {
- break;
- }
- if (!IsIntegrationValid())
- {
- yield return new ValidationResult(
- "Webhook integrations must include valid configuration.",
- new[] { nameof(Configuration) });
- }
+ foreach (var r in ValidateConfiguration(allowNullOrEmpty: true))
+ yield return r;
break;
case IntegrationType.Hec:
- if (!IsIntegrationValid())
- {
- yield return new ValidationResult(
- "HEC integrations must include valid configuration.",
- new[] { nameof(Configuration) });
- }
+ foreach (var r in ValidateConfiguration(allowNullOrEmpty: false))
+ yield return r;
+ break;
+ case IntegrationType.Datadog:
+ foreach (var r in ValidateConfiguration(allowNullOrEmpty: false))
+ yield return r;
break;
default:
yield return new ValidationResult(
$"Integration type '{Type}' is not recognized.",
- new[] { nameof(Type) });
+ [nameof(Type)]);
break;
}
}
- private bool IsIntegrationValid()
+ private List ValidateConfiguration(bool allowNullOrEmpty)
{
+ var results = new List();
+
if (string.IsNullOrWhiteSpace(Configuration))
{
- return false;
+ if (!allowNullOrEmpty)
+ results.Add(InvalidConfig());
+ return results;
}
try
{
- var config = JsonSerializer.Deserialize(Configuration);
- return config is not null;
+ if (JsonSerializer.Deserialize(Configuration) is null)
+ results.Add(InvalidConfig());
}
catch
{
- return false;
+ results.Add(InvalidConfig());
}
+
+ return results;
}
+
+ private static ValidationResult InvalidConfig() =>
+ new(errorMessage: $"Must include valid {typeof(T).Name} configuration.", memberNames: [nameof(Configuration)]);
}
diff --git a/src/Api/AdminConsole/Models/Request/Providers/ProviderSetupRequestModel.cs b/src/Api/AdminConsole/Models/Request/Providers/ProviderSetupRequestModel.cs
index 1f50c384a3..41cebe8b9b 100644
--- a/src/Api/AdminConsole/Models/Request/Providers/ProviderSetupRequestModel.cs
+++ b/src/Api/AdminConsole/Models/Request/Providers/ProviderSetupRequestModel.cs
@@ -3,8 +3,7 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
-using Bit.Api.Billing.Models.Requests;
-using Bit.Api.Models.Request;
+using Bit.Api.Billing.Models.Requests.Payment;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Utilities;
@@ -28,8 +27,9 @@ public class ProviderSetupRequestModel
[Required]
public string Key { get; set; }
[Required]
- public ExpandedTaxInfoUpdateRequestModel TaxInfo { get; set; }
- public TokenizedPaymentSourceRequestBody PaymentSource { get; set; }
+ public MinimalTokenizedPaymentMethodRequest PaymentMethod { get; set; }
+ [Required]
+ public BillingAddressRequest BillingAddress { get; set; }
public virtual Provider ToProvider(Provider provider)
{
diff --git a/src/Api/AdminConsole/Models/Request/SavePolicyRequest.cs b/src/Api/AdminConsole/Models/Request/SavePolicyRequest.cs
new file mode 100644
index 0000000000..fcdc49882b
--- /dev/null
+++ b/src/Api/AdminConsole/Models/Request/SavePolicyRequest.cs
@@ -0,0 +1,61 @@
+using System.ComponentModel.DataAnnotations;
+using System.Text.Json;
+using Bit.Core.AdminConsole.Enums;
+using Bit.Core.AdminConsole.Models.Data;
+using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
+using Bit.Core.Context;
+using Bit.Core.Utilities;
+
+namespace Bit.Api.AdminConsole.Models.Request;
+
+public class SavePolicyRequest
+{
+ [Required]
+ public PolicyRequestModel Policy { get; set; } = null!;
+
+ public Dictionary? Metadata { get; set; }
+
+ public async Task ToSavePolicyModelAsync(Guid organizationId, ICurrentContext currentContext)
+ {
+ var performedBy = new StandardUser(currentContext.UserId!.Value, await currentContext.OrganizationOwner(organizationId));
+
+ var updatedPolicy = new PolicyUpdate()
+ {
+ Type = Policy.Type!.Value,
+ OrganizationId = organizationId,
+ Data = Policy.Data != null ? JsonSerializer.Serialize(Policy.Data) : null,
+ Enabled = Policy.Enabled.GetValueOrDefault(),
+ };
+
+ var metadata = MapToPolicyMetadata();
+
+ return new SavePolicyModel(updatedPolicy, performedBy, metadata);
+ }
+
+ private IPolicyMetadataModel MapToPolicyMetadata()
+ {
+ if (Metadata == null)
+ {
+ return new EmptyMetadataModel();
+ }
+
+ return Policy?.Type switch
+ {
+ PolicyType.OrganizationDataOwnership => MapToPolicyMetadata(),
+ _ => new EmptyMetadataModel()
+ };
+ }
+
+ private IPolicyMetadataModel MapToPolicyMetadata() where T : IPolicyMetadataModel, new()
+ {
+ try
+ {
+ var json = JsonSerializer.Serialize(Metadata);
+ return CoreHelpers.LoadClassFromJsonData(json);
+ }
+ catch
+ {
+ return new EmptyMetadataModel();
+ }
+ }
+}
diff --git a/src/Api/AdminConsole/Models/Response/EventResponseModel.cs b/src/Api/AdminConsole/Models/Response/EventResponseModel.cs
index bf02d8b00f..c259bc3bc4 100644
--- a/src/Api/AdminConsole/Models/Response/EventResponseModel.cs
+++ b/src/Api/AdminConsole/Models/Response/EventResponseModel.cs
@@ -35,6 +35,7 @@ public class EventResponseModel : ResponseModel
SecretId = ev.SecretId;
ProjectId = ev.ProjectId;
ServiceAccountId = ev.ServiceAccountId;
+ GrantedServiceAccountId = ev.GrantedServiceAccountId;
}
public EventType Type { get; set; }
@@ -58,4 +59,5 @@ public class EventResponseModel : ResponseModel
public Guid? SecretId { get; set; }
public Guid? ProjectId { get; set; }
public Guid? ServiceAccountId { get; set; }
+ public Guid? GrantedServiceAccountId { get; set; }
}
diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModel.cs
index f062ff46a2..5368f78e39 100644
--- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModel.cs
+++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationIntegrationResponseModel.cs
@@ -2,8 +2,6 @@
using Bit.Core.Enums;
using Bit.Core.Models.Api;
-#nullable enable
-
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
public class OrganizationIntegrationResponseModel : ResponseModel
@@ -21,4 +19,29 @@ public class OrganizationIntegrationResponseModel : ResponseModel
public Guid Id { get; set; }
public IntegrationType Type { get; set; }
public string? Configuration { get; set; }
+
+ public OrganizationIntegrationStatus Status => Type switch
+ {
+ // Not yet implemented, shouldn't be present, NotApplicable
+ IntegrationType.CloudBillingSync => OrganizationIntegrationStatus.NotApplicable,
+ IntegrationType.Scim => OrganizationIntegrationStatus.NotApplicable,
+
+ // Webhook is allowed to be null. If it's present, it's Completed
+ IntegrationType.Webhook => OrganizationIntegrationStatus.Completed,
+
+ // If present and the configuration is null, OAuth has been initiated, and we are
+ // waiting on the return call
+ IntegrationType.Slack => string.IsNullOrWhiteSpace(Configuration)
+ ? OrganizationIntegrationStatus.Initiated
+ : OrganizationIntegrationStatus.Completed,
+
+ // HEC and Datadog should only be allowed to be created non-null.
+ // If they are null, they are Invalid
+ IntegrationType.Hec => string.IsNullOrWhiteSpace(Configuration)
+ ? OrganizationIntegrationStatus.Invalid
+ : OrganizationIntegrationStatus.Completed,
+ IntegrationType.Datadog => string.IsNullOrWhiteSpace(Configuration)
+ ? OrganizationIntegrationStatus.Invalid
+ : OrganizationIntegrationStatus.Completed,
+ };
}
diff --git a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs
index 7c31c2ae81..eb810599f3 100644
--- a/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs
+++ b/src/Api/AdminConsole/Models/Response/Organizations/OrganizationUserResponseModel.cs
@@ -236,8 +236,8 @@ public class OrganizationUserPublicKeyResponseModel : ResponseModel
public class OrganizationUserBulkResponseModel : ResponseModel
{
- public OrganizationUserBulkResponseModel(Guid id, string error,
- string obj = "OrganizationBulkConfirmResponseModel") : base(obj)
+ public OrganizationUserBulkResponseModel(Guid id, string error)
+ : base("OrganizationBulkConfirmResponseModel")
{
Id = id;
Error = error;
diff --git a/src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs b/src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs
index 9feafce70c..81ca801308 100644
--- a/src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs
+++ b/src/Api/AdminConsole/Models/Response/Organizations/PolicyResponseModel.cs
@@ -10,6 +10,10 @@ namespace Bit.Api.AdminConsole.Models.Response.Organizations;
public class PolicyResponseModel : ResponseModel
{
+ public PolicyResponseModel() : base("policy")
+ {
+ }
+
public PolicyResponseModel(Policy policy, string obj = "policy")
: base(obj)
{
diff --git a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs
index e421c3247e..fd2bfe06dc 100644
--- a/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs
+++ b/src/Api/AdminConsole/Models/Response/ProfileOrganizationResponseModel.cs
@@ -78,12 +78,14 @@ public class ProfileOrganizationResponseModel : ResponseModel
UseRiskInsights = organization.UseRiskInsights;
UseOrganizationDomains = organization.UseOrganizationDomains;
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
+ SsoEnabled = organization.SsoEnabled ?? false;
if (organization.SsoConfig != null)
{
var ssoConfigData = SsoConfigurationData.Deserialize(organization.SsoConfig);
KeyConnectorEnabled = ssoConfigData.MemberDecryptionType == MemberDecryptionType.KeyConnector && !string.IsNullOrEmpty(ssoConfigData.KeyConnectorUrl);
KeyConnectorUrl = ssoConfigData.KeyConnectorUrl;
+ SsoMemberDecryptionType = ssoConfigData.MemberDecryptionType;
}
}
@@ -160,4 +162,6 @@ public class ProfileOrganizationResponseModel : ResponseModel
public bool UseOrganizationDomains { get; set; }
public bool UseAdminSponsoredFamilies { get; set; }
public bool IsAdminInitiated { get; set; }
+ public bool SsoEnabled { get; set; }
+ public MemberDecryptionType? SsoMemberDecryptionType { get; set; }
}
diff --git a/src/Api/AdminConsole/Public/Models/Request/MemberCreateRequestModel.cs b/src/Api/AdminConsole/Public/Models/Request/MemberCreateRequestModel.cs
index 6813610325..b3182601b5 100644
--- a/src/Api/AdminConsole/Public/Models/Request/MemberCreateRequestModel.cs
+++ b/src/Api/AdminConsole/Public/Models/Request/MemberCreateRequestModel.cs
@@ -31,7 +31,7 @@ public class MemberCreateRequestModel : MemberUpdateRequestModel
{
Emails = new[] { Email },
Type = Type.Value,
- Collections = Collections?.Select(c => c.ToCollectionAccessSelection()).ToList(),
+ Collections = Collections?.Select(c => c.ToCollectionAccessSelection())?.ToList() ?? [],
Groups = Groups
};
diff --git a/src/Api/Api.csproj b/src/Api/Api.csproj
index d48f49626f..138549e92d 100644
--- a/src/Api/Api.csproj
+++ b/src/Api/Api.csproj
@@ -34,7 +34,7 @@
-
+
diff --git a/src/Api/Auth/Controllers/AccountsController.cs b/src/Api/Auth/Controllers/AccountsController.cs
index f197f1270b..19165a5a1c 100644
--- a/src/Api/Auth/Controllers/AccountsController.cs
+++ b/src/Api/Auth/Controllers/AccountsController.cs
@@ -9,6 +9,7 @@ using Bit.Core;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
+using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
@@ -16,6 +17,7 @@ using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
+using Bit.Core.KeyManagement.Kdf;
using Bit.Core.Models.Api.Response;
using Bit.Core.Repositories;
using Bit.Core.Services;
@@ -26,7 +28,7 @@ using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Auth.Controllers;
[Route("accounts")]
-[Authorize("Application")]
+[Authorize(Policies.Application)]
public class AccountsController : Controller
{
private readonly IOrganizationService _organizationService;
@@ -39,7 +41,7 @@ public class AccountsController : Controller
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IFeatureService _featureService;
private readonly ITwoFactorEmailService _twoFactorEmailService;
-
+ private readonly IChangeKdfCommand _changeKdfCommand;
public AccountsController(
IOrganizationService organizationService,
@@ -51,7 +53,8 @@ public class AccountsController : Controller
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IFeatureService featureService,
- ITwoFactorEmailService twoFactorEmailService
+ ITwoFactorEmailService twoFactorEmailService,
+ IChangeKdfCommand changeKdfCommand
)
{
_organizationService = organizationService;
@@ -64,7 +67,7 @@ public class AccountsController : Controller
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_featureService = featureService;
_twoFactorEmailService = twoFactorEmailService;
-
+ _changeKdfCommand = changeKdfCommand;
}
@@ -256,7 +259,7 @@ public class AccountsController : Controller
}
[HttpPost("kdf")]
- public async Task PostKdf([FromBody] KdfRequestModel model)
+ public async Task PostKdf([FromBody] PasswordRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
@@ -264,8 +267,12 @@ public class AccountsController : Controller
throw new UnauthorizedAccessException();
}
- var result = await _userService.ChangeKdfAsync(user, model.MasterPasswordHash,
- model.NewMasterPasswordHash, model.Key, model.Kdf.Value, model.KdfIterations.Value, model.KdfMemory, model.KdfParallelism);
+ if (model.AuthenticationData == null || model.UnlockData == null)
+ {
+ throw new BadRequestException("AuthenticationData and UnlockData must be provided.");
+ }
+
+ var result = await _changeKdfCommand.ChangeKdfAsync(user, model.MasterPasswordHash, model.AuthenticationData.ToData(), model.UnlockData.ToData());
if (result.Succeeded)
{
return;
@@ -344,7 +351,6 @@ public class AccountsController : Controller
}
[HttpPut("profile")]
- [HttpPost("profile")]
public async Task PutProfile([FromBody] UpdateProfileRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
@@ -363,8 +369,14 @@ public class AccountsController : Controller
return response;
}
+ [HttpPost("profile")]
+ [Obsolete("This endpoint is deprecated. Use PUT /profile instead.")]
+ public async Task PostProfile([FromBody] UpdateProfileRequestModel model)
+ {
+ return await PutProfile(model);
+ }
+
[HttpPut("avatar")]
- [HttpPost("avatar")]
public async Task PutAvatar([FromBody] UpdateAvatarRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
@@ -382,6 +394,13 @@ public class AccountsController : Controller
return response;
}
+ [HttpPost("avatar")]
+ [Obsolete("This endpoint is deprecated. Use PUT /avatar instead.")]
+ public async Task PostAvatar([FromBody] UpdateAvatarRequestModel model)
+ {
+ return await PutAvatar(model);
+ }
+
[HttpGet("revision-date")]
public async Task GetAccountRevisionDate()
{
@@ -430,7 +449,6 @@ public class AccountsController : Controller
}
[HttpDelete]
- [HttpPost("delete")]
public async Task Delete([FromBody] SecretVerificationRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
@@ -467,6 +485,13 @@ public class AccountsController : Controller
throw new BadRequestException(ModelState);
}
+ [HttpPost("delete")]
+ [Obsolete("This endpoint is deprecated. Use DELETE / instead.")]
+ public async Task PostDelete([FromBody] SecretVerificationRequestModel model)
+ {
+ await Delete(model);
+ }
+
[AllowAnonymous]
[HttpPost("delete-recover")]
public async Task PostDeleteRecover([FromBody] DeleteRecoverRequestModel model)
@@ -638,7 +663,6 @@ public class AccountsController : Controller
await _twoFactorEmailService.SendNewDeviceVerificationEmailAsync(user);
}
- [HttpPost("verify-devices")]
[HttpPut("verify-devices")]
public async Task SetUserVerifyDevicesAsync([FromBody] SetVerifyDevicesRequestModel request)
{
@@ -654,6 +678,13 @@ public class AccountsController : Controller
await _userService.SaveUserAsync(user);
}
+ [HttpPost("verify-devices")]
+ [Obsolete("This endpoint is deprecated. Use PUT /verify-devices instead.")]
+ public async Task PostSetUserVerifyDevicesAsync([FromBody] SetVerifyDevicesRequestModel request)
+ {
+ await SetUserVerifyDevicesAsync(request);
+ }
+
private async Task> GetOrganizationIdsClaimingUserAsync(Guid userId)
{
var organizationsClaimingUser = await _userService.GetOrganizationsClaimingUserAsync(userId);
diff --git a/src/Api/Auth/Controllers/AuthRequestsController.cs b/src/Api/Auth/Controllers/AuthRequestsController.cs
index 3f91bd6eea..e9dfe17c94 100644
--- a/src/Api/Auth/Controllers/AuthRequestsController.cs
+++ b/src/Api/Auth/Controllers/AuthRequestsController.cs
@@ -3,22 +3,21 @@
using Bit.Api.Auth.Models.Response;
using Bit.Api.Models.Response;
-using Bit.Core;
using Bit.Core.Auth.Enums;
+using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Models.Api.Request.AuthRequest;
using Bit.Core.Auth.Services;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
-using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Auth.Controllers;
[Route("auth-requests")]
-[Authorize("Application")]
+[Authorize(Policies.Application)]
public class AuthRequestsController(
IUserService userService,
IAuthRequestRepository authRequestRepository,
@@ -31,7 +30,7 @@ public class AuthRequestsController(
private readonly IAuthRequestService _authRequestService = authRequestService;
[HttpGet("")]
- public async Task> Get()
+ public async Task> GetAll()
{
var userId = _userService.GetProperUserId(User).Value;
var authRequests = await _authRequestRepository.GetManyByUserIdAsync(userId);
@@ -54,7 +53,6 @@ public class AuthRequestsController(
}
[HttpGet("pending")]
- [RequireFeature(FeatureFlagKeys.BrowserExtensionLoginApproval)]
public async Task> GetPendingAuthRequestsAsync()
{
var userId = _userService.GetProperUserId(User).Value;
@@ -102,7 +100,37 @@ public class AuthRequestsController(
public async Task Put(Guid id, [FromBody] AuthRequestUpdateRequestModel model)
{
var userId = _userService.GetProperUserId(User).Value;
+
+ // If the Approving Device is attempting to approve a request, validate the approval
+ if (model.RequestApproved == true)
+ {
+ await ValidateApprovalOfMostRecentAuthRequest(id, userId);
+ }
+
var authRequest = await _authRequestService.UpdateAuthRequestAsync(id, userId, model);
return new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);
}
+
+ private async Task ValidateApprovalOfMostRecentAuthRequest(Guid id, Guid userId)
+ {
+ // Get the current auth request to find the device identifier
+ var currentAuthRequest = await _authRequestService.GetAuthRequestAsync(id, userId);
+ if (currentAuthRequest == null)
+ {
+ throw new NotFoundException();
+ }
+
+ // Get all pending auth requests for this user (returns most recent per device)
+ var pendingRequests = await _authRequestRepository.GetManyPendingAuthRequestByUserId(userId);
+
+ // Find the most recent request for the same device
+ var mostRecentForDevice = pendingRequests
+ .FirstOrDefault(pendingRequest => pendingRequest.RequestDeviceIdentifier == currentAuthRequest.RequestDeviceIdentifier);
+
+ var isMostRecentRequestForDevice = mostRecentForDevice?.Id == id;
+ if (!isMostRecentRequestForDevice)
+ {
+ throw new BadRequestException("This request is no longer valid. Make sure to approve the most recent request.");
+ }
+ }
}
diff --git a/src/Api/Auth/Controllers/EmergencyAccessController.cs b/src/Api/Auth/Controllers/EmergencyAccessController.cs
index 53b57fe685..016cd82fe2 100644
--- a/src/Api/Auth/Controllers/EmergencyAccessController.cs
+++ b/src/Api/Auth/Controllers/EmergencyAccessController.cs
@@ -18,7 +18,7 @@ using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Auth.Controllers;
[Route("emergency-access")]
-[Authorize("Application")]
+[Authorize(Core.Auth.Identity.Policies.Application)]
public class EmergencyAccessController : Controller
{
private readonly IUserService _userService;
@@ -79,7 +79,6 @@ public class EmergencyAccessController : Controller
}
[HttpPut("{id}")]
- [HttpPost("{id}")]
public async Task Put(Guid id, [FromBody] EmergencyAccessUpdateRequestModel model)
{
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
@@ -92,14 +91,27 @@ public class EmergencyAccessController : Controller
await _emergencyAccessService.SaveAsync(model.ToEmergencyAccess(emergencyAccess), user);
}
+ [HttpPost("{id}")]
+ [Obsolete("This endpoint is deprecated. Use PUT /{id} instead.")]
+ public async Task Post(Guid id, [FromBody] EmergencyAccessUpdateRequestModel model)
+ {
+ await Put(id, model);
+ }
+
[HttpDelete("{id}")]
- [HttpPost("{id}/delete")]
public async Task Delete(Guid id)
{
var userId = _userService.GetProperUserId(User);
await _emergencyAccessService.DeleteAsync(id, userId.Value);
}
+ [HttpPost("{id}/delete")]
+ [Obsolete("This endpoint is deprecated. Use DELETE /{id} instead.")]
+ public async Task PostDelete(Guid id)
+ {
+ await Delete(id);
+ }
+
[HttpPost("invite")]
public async Task Invite([FromBody] EmergencyAccessInviteRequestModel model)
{
@@ -136,7 +148,7 @@ public class EmergencyAccessController : Controller
}
[HttpPost("{id}/approve")]
- public async Task Accept(Guid id)
+ public async Task Approve(Guid id)
{
var user = await _userService.GetUserByPrincipalAsync(User);
await _emergencyAccessService.ApproveAsync(id, user);
diff --git a/src/Api/Auth/Controllers/TwoFactorController.cs b/src/Api/Auth/Controllers/TwoFactorController.cs
index 96b64f16fc..0af46fb57c 100644
--- a/src/Api/Auth/Controllers/TwoFactorController.cs
+++ b/src/Api/Auth/Controllers/TwoFactorController.cs
@@ -7,6 +7,7 @@ using Bit.Api.Auth.Models.Response.TwoFactor;
using Bit.Api.Models.Request;
using Bit.Api.Models.Response;
using Bit.Core.Auth.Enums;
+using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces;
using Bit.Core.Auth.Models.Business.Tokenables;
@@ -26,7 +27,7 @@ using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Auth.Controllers;
[Route("two-factor")]
-[Authorize("Web")]
+[Authorize(Policies.Web)]
public class TwoFactorController : Controller
{
private readonly IUserService _userService;
@@ -110,7 +111,6 @@ public class TwoFactorController : Controller
}
[HttpPut("authenticator")]
- [HttpPost("authenticator")]
public async Task PutAuthenticator(
[FromBody] UpdateTwoFactorAuthenticatorRequestModel model)
{
@@ -133,6 +133,14 @@ public class TwoFactorController : Controller
return response;
}
+ [HttpPost("authenticator")]
+ [Obsolete("This endpoint is deprecated. Use PUT /authenticator instead.")]
+ public async Task PostAuthenticator(
+ [FromBody] UpdateTwoFactorAuthenticatorRequestModel model)
+ {
+ return await PutAuthenticator(model);
+ }
+
[HttpDelete("authenticator")]
public async Task DisableAuthenticator(
[FromBody] TwoFactorAuthenticatorDisableRequestModel model)
@@ -157,7 +165,6 @@ public class TwoFactorController : Controller
}
[HttpPut("yubikey")]
- [HttpPost("yubikey")]
public async Task PutYubiKey([FromBody] UpdateTwoFactorYubicoOtpRequestModel model)
{
var user = await CheckAsync(model, true);
@@ -174,6 +181,13 @@ public class TwoFactorController : Controller
return response;
}
+ [HttpPost("yubikey")]
+ [Obsolete("This endpoint is deprecated. Use PUT /yubikey instead.")]
+ public async Task PostYubiKey([FromBody] UpdateTwoFactorYubicoOtpRequestModel model)
+ {
+ return await PutYubiKey(model);
+ }
+
[HttpPost("get-duo")]
public async Task GetDuo([FromBody] SecretVerificationRequestModel model)
{
@@ -183,7 +197,6 @@ public class TwoFactorController : Controller
}
[HttpPut("duo")]
- [HttpPost("duo")]
public async Task PutDuo([FromBody] UpdateTwoFactorDuoRequestModel model)
{
var user = await CheckAsync(model, true);
@@ -199,6 +212,13 @@ public class TwoFactorController : Controller
return response;
}
+ [HttpPost("duo")]
+ [Obsolete("This endpoint is deprecated. Use PUT /duo instead.")]
+ public async Task PostDuo([FromBody] UpdateTwoFactorDuoRequestModel model)
+ {
+ return await PutDuo(model);
+ }
+
[HttpPost("~/organizations/{id}/two-factor/get-duo")]
public async Task GetOrganizationDuo(string id,
[FromBody] SecretVerificationRequestModel model)
@@ -217,7 +237,6 @@ public class TwoFactorController : Controller
}
[HttpPut("~/organizations/{id}/two-factor/duo")]
- [HttpPost("~/organizations/{id}/two-factor/duo")]
public async Task PutOrganizationDuo(string id,
[FromBody] UpdateTwoFactorDuoRequestModel model)
{
@@ -243,6 +262,14 @@ public class TwoFactorController : Controller
return response;
}
+ [HttpPost("~/organizations/{id}/two-factor/duo")]
+ [Obsolete("This endpoint is deprecated. Use PUT /organizations/{id}/two-factor/duo instead.")]
+ public async Task PostOrganizationDuo(string id,
+ [FromBody] UpdateTwoFactorDuoRequestModel model)
+ {
+ return await PutOrganizationDuo(id, model);
+ }
+
[HttpPost("get-webauthn")]
public async Task GetWebAuthn([FromBody] SecretVerificationRequestModel model)
{
@@ -261,7 +288,6 @@ public class TwoFactorController : Controller
}
[HttpPut("webauthn")]
- [HttpPost("webauthn")]
public async Task PutWebAuthn([FromBody] TwoFactorWebAuthnRequestModel model)
{
var user = await CheckAsync(model, false);
@@ -277,6 +303,13 @@ public class TwoFactorController : Controller
return response;
}
+ [HttpPost("webauthn")]
+ [Obsolete("This endpoint is deprecated. Use PUT /webauthn instead.")]
+ public async Task PostWebAuthn([FromBody] TwoFactorWebAuthnRequestModel model)
+ {
+ return await PutWebAuthn(model);
+ }
+
[HttpDelete("webauthn")]
public async Task DeleteWebAuthn(
[FromBody] TwoFactorWebAuthnDeleteRequestModel model)
@@ -349,7 +382,6 @@ public class TwoFactorController : Controller
}
[HttpPut("email")]
- [HttpPost("email")]
public async Task PutEmail([FromBody] UpdateTwoFactorEmailRequestModel model)
{
var user = await CheckAsync(model, false);
@@ -367,8 +399,14 @@ public class TwoFactorController : Controller
return response;
}
+ [HttpPost("email")]
+ [Obsolete("This endpoint is deprecated. Use PUT /email instead.")]
+ public async Task PostEmail([FromBody] UpdateTwoFactorEmailRequestModel model)
+ {
+ return await PutEmail(model);
+ }
+
[HttpPut("disable")]
- [HttpPost("disable")]
public async Task PutDisable([FromBody] TwoFactorProviderRequestModel model)
{
var user = await CheckAsync(model, false);
@@ -377,8 +415,14 @@ public class TwoFactorController : Controller
return response;
}
+ [HttpPost("disable")]
+ [Obsolete("This endpoint is deprecated. Use PUT /disable instead.")]
+ public async Task PostDisable([FromBody] TwoFactorProviderRequestModel model)
+ {
+ return await PutDisable(model);
+ }
+
[HttpPut("~/organizations/{id}/two-factor/disable")]
- [HttpPost("~/organizations/{id}/two-factor/disable")]
public async Task PutOrganizationDisable(string id,
[FromBody] TwoFactorProviderRequestModel model)
{
@@ -401,6 +445,14 @@ public class TwoFactorController : Controller
return response;
}
+ [HttpPost("~/organizations/{id}/two-factor/disable")]
+ [Obsolete("This endpoint is deprecated. Use PUT /organizations/{id}/two-factor/disable instead.")]
+ public async Task PostOrganizationDisable(string id,
+ [FromBody] TwoFactorProviderRequestModel model)
+ {
+ return await PutOrganizationDisable(id, model);
+ }
+
[HttpPost("get-recover")]
public async Task GetRecover([FromBody] SecretVerificationRequestModel model)
{
@@ -409,21 +461,6 @@ public class TwoFactorController : Controller
return response;
}
- ///
- /// To be removed when the feature flag pm-17128-recovery-code-login is removed PM-18175.
- ///
- [Obsolete("Two Factor recovery is handled in the TwoFactorAuthenticationValidator.")]
- [HttpPost("recover")]
- [AllowAnonymous]
- public async Task PostRecover([FromBody] TwoFactorRecoveryRequestModel model)
- {
- if (!await _userService.RecoverTwoFactorAsync(model.Email, model.MasterPasswordHash, model.RecoveryCode))
- {
- await Task.Delay(2000);
- throw new BadRequestException(string.Empty, "Invalid information. Try again.");
- }
- }
-
[Obsolete("Leaving this for backwards compatibility on clients")]
[HttpGet("get-device-verification-settings")]
public Task GetDeviceVerificationSettings()
diff --git a/src/Api/Auth/Controllers/WebAuthnController.cs b/src/Api/Auth/Controllers/WebAuthnController.cs
index bb17607954..60b8621c5e 100644
--- a/src/Api/Auth/Controllers/WebAuthnController.cs
+++ b/src/Api/Auth/Controllers/WebAuthnController.cs
@@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums;
+using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Models.Api.Response.Accounts;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories;
@@ -20,7 +21,7 @@ using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Auth.Controllers;
[Route("webauthn")]
-[Authorize("Web")]
+[Authorize(Policies.Web)]
public class WebAuthnController : Controller
{
private readonly IUserService _userService;
diff --git a/src/Api/Auth/Models/Request/Accounts/KdfRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/KdfRequestModel.cs
deleted file mode 100644
index fc62f22bab..0000000000
--- a/src/Api/Auth/Models/Request/Accounts/KdfRequestModel.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using System.ComponentModel.DataAnnotations;
-using Bit.Core.Enums;
-using Bit.Core.Utilities;
-
-namespace Bit.Api.Auth.Models.Request.Accounts;
-
-public class KdfRequestModel : PasswordRequestModel, IValidatableObject
-{
- [Required]
- public KdfType? Kdf { get; set; }
- [Required]
- public int? KdfIterations { get; set; }
- public int? KdfMemory { get; set; }
- public int? KdfParallelism { get; set; }
-
- public override IEnumerable Validate(ValidationContext validationContext)
- {
- if (Kdf.HasValue && KdfIterations.HasValue)
- {
- return KdfSettingsValidator.Validate(Kdf.Value, KdfIterations.Value, KdfMemory, KdfParallelism);
- }
-
- return Enumerable.Empty();
- }
-}
diff --git a/src/Api/Auth/Models/Request/Accounts/MasterPasswordUnlockDataModel.cs b/src/Api/Auth/Models/Request/Accounts/MasterPasswordUnlockDataAndAuthenticationModel.cs
similarity index 90%
rename from src/Api/Auth/Models/Request/Accounts/MasterPasswordUnlockDataModel.cs
rename to src/Api/Auth/Models/Request/Accounts/MasterPasswordUnlockDataAndAuthenticationModel.cs
index ba57788cec..da361e5a0c 100644
--- a/src/Api/Auth/Models/Request/Accounts/MasterPasswordUnlockDataModel.cs
+++ b/src/Api/Auth/Models/Request/Accounts/MasterPasswordUnlockDataAndAuthenticationModel.cs
@@ -7,7 +7,7 @@ using Bit.Core.Utilities;
namespace Bit.Api.Auth.Models.Request.Accounts;
-public class MasterPasswordUnlockDataModel : IValidatableObject
+public class MasterPasswordUnlockAndAuthenticationDataModel : IValidatableObject
{
public required KdfType KdfType { get; set; }
public required int KdfIterations { get; set; }
@@ -45,9 +45,9 @@ public class MasterPasswordUnlockDataModel : IValidatableObject
}
}
- public MasterPasswordUnlockData ToUnlockData()
+ public MasterPasswordUnlockAndAuthenticationData ToUnlockData()
{
- var data = new MasterPasswordUnlockData
+ var data = new MasterPasswordUnlockAndAuthenticationData
{
KdfType = KdfType,
KdfIterations = KdfIterations,
diff --git a/src/Api/Auth/Models/Request/Accounts/PasswordRequestModel.cs b/src/Api/Auth/Models/Request/Accounts/PasswordRequestModel.cs
index 01da1f0f9f..8fa51e9f34 100644
--- a/src/Api/Auth/Models/Request/Accounts/PasswordRequestModel.cs
+++ b/src/Api/Auth/Models/Request/Accounts/PasswordRequestModel.cs
@@ -1,7 +1,7 @@
-// FIXME: Update this file to be null safe and then delete the line below
-#nullable disable
+#nullable enable
using System.ComponentModel.DataAnnotations;
+using Bit.Api.KeyManagement.Models.Requests;
namespace Bit.Api.Auth.Models.Request.Accounts;
@@ -9,9 +9,13 @@ public class PasswordRequestModel : SecretVerificationRequestModel
{
[Required]
[StringLength(300)]
- public string NewMasterPasswordHash { get; set; }
+ public required string NewMasterPasswordHash { get; set; }
[StringLength(50)]
- public string MasterPasswordHint { get; set; }
+ public string? MasterPasswordHint { get; set; }
[Required]
- public string Key { get; set; }
+ public required string Key { get; set; }
+
+ // Note: These will eventually become required, but not all consumers are moved over yet.
+ public MasterPasswordAuthenticationDataRequestModel? AuthenticationData { get; set; }
+ public MasterPasswordUnlockDataRequestModel? UnlockData { get; set; }
}
diff --git a/src/Api/Auth/Models/Request/OrganizationSsoRequestModel.cs b/src/Api/Auth/Models/Request/OrganizationSsoRequestModel.cs
index fcf386d7ee..349bdebb88 100644
--- a/src/Api/Auth/Models/Request/OrganizationSsoRequestModel.cs
+++ b/src/Api/Auth/Models/Request/OrganizationSsoRequestModel.cs
@@ -121,7 +121,7 @@ public class SsoConfigurationDataRequest : IValidatableObject
new[] { nameof(IdpEntityId) });
}
- if (!Uri.IsWellFormedUriString(IdpEntityId, UriKind.Absolute) && string.IsNullOrWhiteSpace(IdpSingleSignOnServiceUrl))
+ if (string.IsNullOrWhiteSpace(IdpSingleSignOnServiceUrl))
{
yield return new ValidationResult(i18nService.GetLocalizedHtmlString("IdpSingleSignOnServiceUrlValidationError"),
new[] { nameof(IdpSingleSignOnServiceUrl) });
@@ -139,6 +139,7 @@ public class SsoConfigurationDataRequest : IValidatableObject
new[] { nameof(IdpSingleLogoutServiceUrl) });
}
+ // TODO: On server, make public certificate required for SAML2 SSO: https://bitwarden.atlassian.net/browse/PM-26028
if (!string.IsNullOrWhiteSpace(IdpX509PublicCert))
{
// Validate the certificate is in a valid format
diff --git a/src/Api/Billing/Attributes/PaymentMethodTypeValidationAttribute.cs b/src/Api/Billing/Attributes/PaymentMethodTypeValidationAttribute.cs
new file mode 100644
index 0000000000..227b454f9f
--- /dev/null
+++ b/src/Api/Billing/Attributes/PaymentMethodTypeValidationAttribute.cs
@@ -0,0 +1,13 @@
+using Bit.Api.Utilities;
+
+namespace Bit.Api.Billing.Attributes;
+
+public class PaymentMethodTypeValidationAttribute : StringMatchesAttribute
+{
+ private static readonly string[] _acceptedValues = ["bankAccount", "card", "payPal"];
+
+ public PaymentMethodTypeValidationAttribute() : base(_acceptedValues)
+ {
+ ErrorMessage = $"Payment method type must be one of: {string.Join(", ", _acceptedValues)}";
+ }
+}
diff --git a/src/Api/Billing/Controllers/OrganizationBillingController.cs b/src/Api/Billing/Controllers/OrganizationBillingController.cs
index 762b06db96..1d6bf51661 100644
--- a/src/Api/Billing/Controllers/OrganizationBillingController.cs
+++ b/src/Api/Billing/Controllers/OrganizationBillingController.cs
@@ -1,16 +1,8 @@
-#nullable enable
-using System.Diagnostics;
-using Bit.Api.AdminConsole.Models.Request.Organizations;
-using Bit.Api.Billing.Models.Requests;
+using Bit.Api.Billing.Models.Requests;
using Bit.Api.Billing.Models.Responses;
-using Bit.Core.Billing.Enums;
-using Bit.Core.Billing.Models;
-using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Billing.Organizations.Services;
-using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Billing.Services;
-using Bit.Core.Billing.Tax.Models;
using Bit.Core.Context;
using Bit.Core.Repositories;
using Bit.Core.Services;
@@ -28,10 +20,8 @@ public class OrganizationBillingController(
IOrganizationBillingService organizationBillingService,
IOrganizationRepository organizationRepository,
IPaymentService paymentService,
- IPricingClient pricingClient,
ISubscriberService subscriberService,
- IPaymentHistoryService paymentHistoryService,
- IUserService userService) : BaseBillingController
+ IPaymentHistoryService paymentHistoryService) : BaseBillingController
{
[HttpGet("metadata")]
public async Task GetMetadataAsync([FromRoute] Guid organizationId)
@@ -264,71 +254,6 @@ public class OrganizationBillingController(
return TypedResults.Ok();
}
- [HttpPost("restart-subscription")]
- public async Task RestartSubscriptionAsync([FromRoute] Guid organizationId,
- [FromBody] OrganizationCreateRequestModel model)
- {
- var user = await userService.GetUserByPrincipalAsync(User);
- if (user == null)
- {
- throw new UnauthorizedAccessException();
- }
-
- if (!await currentContext.EditPaymentMethods(organizationId))
- {
- return Error.Unauthorized();
- }
-
- var organization = await organizationRepository.GetByIdAsync(organizationId);
- if (organization == null)
- {
- return Error.NotFound();
- }
- var existingPlan = organization.PlanType;
- var organizationSignup = model.ToOrganizationSignup(user);
- var sale = OrganizationSale.From(organization, organizationSignup);
- var plan = await pricingClient.GetPlanOrThrow(model.PlanType);
- sale.Organization.PlanType = plan.Type;
- sale.Organization.Plan = plan.Name;
- sale.SubscriptionSetup.SkipTrial = true;
- if (existingPlan == PlanType.Free && organization.GatewaySubscriptionId is not null)
- {
- sale.Organization.UseTotp = plan.HasTotp;
- sale.Organization.UseGroups = plan.HasGroups;
- sale.Organization.UseDirectory = plan.HasDirectory;
- sale.Organization.SelfHost = plan.HasSelfHost;
- sale.Organization.UsersGetPremium = plan.UsersGetPremium;
- sale.Organization.UseEvents = plan.HasEvents;
- sale.Organization.Use2fa = plan.Has2fa;
- sale.Organization.UseApi = plan.HasApi;
- sale.Organization.UsePolicies = plan.HasPolicies;
- sale.Organization.UseSso = plan.HasSso;
- sale.Organization.UseResetPassword = plan.HasResetPassword;
- sale.Organization.UseKeyConnector = plan.HasKeyConnector;
- sale.Organization.UseScim = plan.HasScim;
- sale.Organization.UseCustomPermissions = plan.HasCustomPermissions;
- sale.Organization.UseOrganizationDomains = plan.HasOrganizationDomains;
- sale.Organization.MaxCollections = plan.PasswordManager.MaxCollections;
- }
-
- if (organizationSignup.PaymentMethodType == null || string.IsNullOrEmpty(organizationSignup.PaymentToken))
- {
- return Error.BadRequest("A payment method is required to restart the subscription.");
- }
- var org = await organizationRepository.GetByIdAsync(organizationId);
- Debug.Assert(org is not null, "This organization has already been found via this same ID, this should be fine.");
- var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken);
- var taxInformation = TaxInformation.From(organizationSignup.TaxInfo);
- await organizationBillingService.Finalize(sale);
- var updatedOrg = await organizationRepository.GetByIdAsync(organizationId);
- if (updatedOrg != null)
- {
- await organizationBillingService.UpdatePaymentMethod(updatedOrg, paymentSource, taxInformation);
- }
-
- return TypedResults.Ok();
- }
-
[HttpPost("setup-business-unit")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task SetupBusinessUnitAsync(
diff --git a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs
index 2d05595b2d..8c202752de 100644
--- a/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs
+++ b/src/Api/Billing/Controllers/OrganizationSponsorshipsController.cs
@@ -208,7 +208,6 @@ public class OrganizationSponsorshipsController : Controller
[Authorize("Application")]
[HttpDelete("{sponsoringOrganizationId}")]
- [HttpPost("{sponsoringOrganizationId}/delete")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task RevokeSponsorship(Guid sponsoringOrganizationId)
{
@@ -225,6 +224,15 @@ public class OrganizationSponsorshipsController : Controller
await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);
}
+ [Authorize("Application")]
+ [HttpPost("{sponsoringOrganizationId}/delete")]
+ [Obsolete("This endpoint is deprecated. Use DELETE /{sponsoringOrganizationId} instead.")]
+ [SelfHosted(NotSelfHostedOnly = true)]
+ public async Task PostRevokeSponsorship(Guid sponsoringOrganizationId)
+ {
+ await RevokeSponsorship(sponsoringOrganizationId);
+ }
+
[Authorize("Application")]
[HttpDelete("{sponsoringOrgId}/{sponsoredFriendlyName}/revoke")]
[SelfHosted(NotSelfHostedOnly = true)]
@@ -241,7 +249,6 @@ public class OrganizationSponsorshipsController : Controller
[Authorize("Application")]
[HttpDelete("sponsored/{sponsoredOrgId}")]
- [HttpPost("sponsored/{sponsoredOrgId}/remove")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task RemoveSponsorship(Guid sponsoredOrgId)
{
@@ -257,6 +264,15 @@ public class OrganizationSponsorshipsController : Controller
await _removeSponsorshipCommand.RemoveSponsorshipAsync(existingOrgSponsorship);
}
+ [Authorize("Application")]
+ [HttpPost("sponsored/{sponsoredOrgId}/remove")]
+ [Obsolete("This endpoint is deprecated. Use DELETE /sponsored/{sponsoredOrgId} instead.")]
+ [SelfHosted(NotSelfHostedOnly = true)]
+ public async Task PostRemoveSponsorship(Guid sponsoredOrgId)
+ {
+ await RemoveSponsorship(sponsoredOrgId);
+ }
+
[HttpGet("{sponsoringOrgId}/sync-status")]
public async Task