1
0
mirror of https://github.com/bitwarden/server.git synced 2025-01-31 23:21:22 +01:00

Merge branch 'main' into km/userkey-rotation-v2

This commit is contained in:
Bernd Schoolmann 2025-01-09 15:50:47 +01:00 committed by GitHub
commit 80f8780109
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
167 changed files with 11479 additions and 472 deletions

1
.github/CODEOWNERS vendored
View File

@ -71,6 +71,7 @@ src/Admin/Views/Tools @bitwarden/team-billing-dev
.github/workflows/repository-management.yml @bitwarden/team-platform-dev
.github/workflows/test-database.yml @bitwarden/team-platform-dev
.github/workflows/test.yml @bitwarden/team-platform-dev
**/*Platform* @bitwarden/team-platform-dev
# Multiple owners - DO NOT REMOVE (BRE)
**/packages.lock.json

View File

@ -30,7 +30,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up .NET
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
- name: Verify format
run: dotnet format --verify-no-changes
@ -81,7 +81,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up .NET
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
- name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
@ -120,7 +120,7 @@ jobs:
ls -atlh ../../../
- name: Upload project artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: ${{ matrix.project_name }}.zip
path: ${{ matrix.base_path }}/${{ matrix.project_name }}/${{ matrix.project_name }}.zip
@ -278,7 +278,7 @@ jobs:
- name: Build Docker image
id: build-docker
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
uses: docker/build-push-action@b32b51a8eda65d6793cd0494a773d4f6bcef32dc # v6.11.0
with:
context: ${{ matrix.base_path }}/${{ matrix.project_name }}
file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile
@ -307,14 +307,14 @@ jobs:
- name: Scan Docker image
id: container-scan
uses: anchore/scan-action@5ed195cc06065322983cae4bb31e2a751feb86fd # v5.2.0
uses: anchore/scan-action@abae793926ec39a78ab18002bc7fc45bbbd94342 # v6.0.0
with:
image: ${{ steps.image-tags.outputs.primary_tag }}
fail-build: false
output-format: sarif
- name: Upload Grype results to GitHub
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
with:
sarif_file: ${{ steps.container-scan.outputs.sarif }}
@ -329,7 +329,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up .NET
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
- name: Log in to Azure - production subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
@ -393,7 +393,7 @@ jobs:
if: |
github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: docker-stub-US.zip
path: docker-stub-US.zip
@ -403,7 +403,7 @@ jobs:
if: |
github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: docker-stub-EU.zip
path: docker-stub-EU.zip
@ -413,7 +413,7 @@ jobs:
if: |
github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: docker-stub-US-sha256.txt
path: docker-stub-US-sha256.txt
@ -423,7 +423,7 @@ jobs:
if: |
github.event_name != 'pull_request_target'
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: docker-stub-EU-sha256.txt
path: docker-stub-EU-sha256.txt
@ -447,7 +447,7 @@ jobs:
GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder"
- name: Upload Public API Swagger artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: swagger.json
path: swagger.json
@ -481,14 +481,14 @@ jobs:
GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder"
- name: Upload Internal API Swagger artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: internal.json
path: internal.json
if-no-files-found: error
- name: Upload Identity Swagger artifact
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: identity.json
path: identity.json
@ -517,7 +517,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up .NET
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
- name: Print environment
run: |
@ -533,7 +533,7 @@ jobs:
- name: Upload project artifact for Windows
if: ${{ contains(matrix.target, 'win') == true }}
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: MsSqlMigratorUtility-${{ matrix.target }}
path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility.exe
@ -541,7 +541,7 @@ jobs:
- name: Upload project artifact
if: ${{ contains(matrix.target, 'win') == false }}
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: MsSqlMigratorUtility-${{ matrix.target }}
path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility

View File

@ -37,7 +37,7 @@ jobs:
- name: Collect
id: collect
uses: launchdarkly/find-code-references-in-pull-request@d008aa4f321d8cd35314d9cb095388dcfde84439 # v2.0.0
uses: launchdarkly/find-code-references-in-pull-request@b2d44bb453e13c11fd1a6ada7b1e5f9fb0ace629 # v2.0.1
with:
project-key: default
environment-key: dev

View File

@ -52,7 +52,7 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}
@ -98,7 +98,7 @@ jobs:
version: ${{ inputs.version_number_override }}
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}
@ -197,7 +197,7 @@ jobs:
- setup
steps:
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
uses: actions/create-github-app-token@c1a285145b9d317df6ced56c09f525b5c2b6f755 # v1.11.1
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}

View File

@ -31,7 +31,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with Checkmarx
uses: checkmarx/ast-github-action@f0869bd1a37fddc06499a096101e6c900e815d81 # 2.0.36
uses: checkmarx/ast-github-action@184bf2f64f55d1c93fd6636d539edf274703e434 # 2.0.41
env:
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
with:
@ -46,7 +46,7 @@ jobs:
--output-path . ${{ env.INCREMENTAL }}
- name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
uses: github/codeql-action/upload-sarif@48ab28a6f5dbc2a99bf1e0131198dd8f1df78169 # v3.28.0
with:
sarif_file: cx_result.sarif
@ -60,7 +60,7 @@ jobs:
steps:
- name: Set up JDK 17
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
uses: actions/setup-java@7a6d8a8234af8eb26422e24e3006232cccaa061b # v4.6.0
with:
java-version: 17
distribution: "zulu"
@ -72,7 +72,7 @@ jobs:
ref: ${{ github.event.pull_request.head.sha }}
- name: Set up .NET
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
- name: Install SonarCloud scanner
run: dotnet tool install dotnet-sonarscanner -g

View File

@ -57,7 +57,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up .NET
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
- name: Restore tools
run: dotnet tool restore
@ -107,7 +107,7 @@ jobs:
run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:MySql:ConnectionString="$CONN_STR"'
env:
CONN_STR: "server=localhost;uid=root;pwd=SET_A_PASSWORD_HERE_123;database=vault_dev;Allow User Variables=true"
- name: Migrate MariaDB
working-directory: "util/MySqlMigrations"
run: 'dotnet ef database update --connection "$CONN_STR" -- --GlobalSettings:MySql:ConnectionString="$CONN_STR"'
@ -186,7 +186,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up .NET
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
- name: Print environment
run: |
@ -200,7 +200,7 @@ jobs:
shell: pwsh
- name: Upload DACPAC
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: sql.dacpac
path: Sql.dacpac
@ -226,7 +226,7 @@ jobs:
shell: pwsh
- name: Report validation results
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
uses: actions/upload-artifact@6f51ac03b9356f520e9adb1b1b7802705f340c2b # v4.5.0
with:
name: report.xml
path: |
@ -237,7 +237,7 @@ jobs:
run: |
if grep -q "<Operations>" "report.xml"; then
echo
echo "Migrations are out of sync with sqlproj!"
echo "Migration files are not in sync with the files in the Sql project. Review to make sure that any stored procedures / other db changes match with the stored procedures in the Sql project."
exit 1
else
echo "Report looks good"

View File

@ -49,7 +49,7 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set up .NET
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
uses: actions/setup-dotnet@87b7050bc53ea08284295505d98d2aa94301e852 # v4.2.0
- name: Print environment
run: |
@ -77,7 +77,7 @@ jobs:
fail-on-error: true
- name: Upload to codecov.io
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
uses: codecov/codecov-action@1e68e06f1dbfde0e4cefc87efeba9e4643565303 # v5.1.2
if: ${{ needs.check-test-secrets.outputs.available == 'true' }}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>2025.1.0</Version>
<Version>2025.1.1</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
@ -64,4 +64,4 @@
</ItemGroup>
</Target>
</Project>
</Project>

View File

@ -32,7 +32,8 @@ public class ProviderBillingService(
IProviderOrganizationRepository providerOrganizationRepository,
IProviderPlanRepository providerPlanRepository,
IStripeAdapter stripeAdapter,
ISubscriberService subscriberService) : IProviderBillingService
ISubscriberService subscriberService,
ITaxService taxService) : IProviderBillingService
{
public async Task ChangePlan(ChangeProviderPlanCommand command)
{
@ -335,14 +336,30 @@ public class ProviderBillingService(
Metadata = new Dictionary<string, string>
{
{ "region", globalSettings.BaseServiceUri.CloudRegion }
},
TaxIdData = taxInfo.HasTaxId ?
[
new CustomerTaxIdDataOptions { Type = taxInfo.TaxIdType, Value = taxInfo.TaxIdNumber }
]
: null
}
};
if (!string.IsNullOrEmpty(taxInfo.TaxIdNumber))
{
var taxIdType = taxService.GetStripeTaxCode(taxInfo.BillingAddressCountry,
taxInfo.TaxIdNumber);
if (taxIdType == null)
{
logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.",
taxInfo.BillingAddressCountry,
taxInfo.TaxIdNumber);
throw new BadRequestException("billingTaxIdTypeInferenceError");
}
customerCreateOptions.TaxIdData = taxInfo.HasTaxId
?
[
new CustomerTaxIdDataOptions { Type = taxIdType, Value = taxInfo.TaxIdNumber }
]
: null;
}
try
{
return await stripeAdapter.CustomerCreateAsync(customerCreateOptions);

View File

@ -779,9 +779,9 @@
}
},
"node_modules/browserslist": {
"version": "4.24.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz",
"integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==",
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
"integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
"dev": true,
"funding": [
{
@ -799,9 +799,9 @@
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001669",
"electron-to-chromium": "^1.5.41",
"node-releases": "^2.0.18",
"caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73",
"node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.1"
},
"bin": {
@ -819,9 +819,9 @@
"license": "MIT"
},
"node_modules/caniuse-lite": {
"version": "1.0.30001688",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001688.tgz",
"integrity": "sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA==",
"version": "1.0.30001690",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz",
"integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==",
"dev": true,
"funding": [
{
@ -840,9 +840,9 @@
"license": "CC-BY-4.0"
},
"node_modules/chokidar": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -972,16 +972,16 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.73",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz",
"integrity": "sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg==",
"version": "1.5.75",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz",
"integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==",
"dev": true,
"license": "ISC"
},
"node_modules/enhanced-resolve": {
"version": "5.17.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
"integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz",
"integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1271,9 +1271,9 @@
}
},
"node_modules/is-core-module": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1792,19 +1792,22 @@
}
},
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.13.0",
"is-core-module": "^2.16.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@ -2082,17 +2085,17 @@
}
},
"node_modules/terser-webpack-plugin": {
"version": "5.3.10",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz",
"integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==",
"version": "5.3.11",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz",
"integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.20",
"@jridgewell/trace-mapping": "^0.3.25",
"jest-worker": "^27.4.5",
"schema-utils": "^3.1.1",
"serialize-javascript": "^6.0.1",
"terser": "^5.26.0"
"schema-utils": "^4.3.0",
"serialize-javascript": "^6.0.2",
"terser": "^5.31.1"
},
"engines": {
"node": ">= 10.13.0"
@ -2116,59 +2119,6 @@
}
}
},
"node_modules/terser-webpack-plugin/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/terser-webpack-plugin/node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"ajv": "^6.9.1"
}
},
"node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/terser-webpack-plugin/node_modules/schema-utils": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
"integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.8",
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
},
"engines": {
"node": ">= 10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",

View File

@ -746,6 +746,12 @@ public class ProviderBillingServiceTests
{
provider.Name = "MSP";
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns(taxInfo.TaxIdType);
taxInfo.BillingAddressCountry = "AD";
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
@ -777,6 +783,29 @@ public class ProviderBillingServiceTests
Assert.Equivalent(expected, actual);
}
[Theory, BitAutoData]
public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
provider.Name = "MSP";
taxInfo.BillingAddressCountry = "AD";
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns((string)null);
var actual = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.SetupCustomer(provider, taxInfo));
Assert.IsType<BadRequestException>(actual);
Assert.Equal("billingTaxIdTypeInferenceError", actual.Message);
}
#endregion
#region SetupSubscription

View File

@ -3,7 +3,6 @@ using Bit.Admin.AdminConsole.Models;
using Bit.Admin.Enums;
using Bit.Admin.Services;
using Bit.Admin.Utilities;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Providers.Interfaces;
@ -476,14 +475,6 @@ public class OrganizationsController : Controller
Organization organization,
OrganizationEditModel update)
{
var scaleMSPOnClientOrganizationUpdate =
_featureService.IsEnabled(FeatureFlagKeys.PM14401_ScaleMSPOnClientOrganizationUpdate);
if (!scaleMSPOnClientOrganizationUpdate)
{
return;
}
var provider = await _providerRepository.GetByOrganizationIdAsync(organization.Id);
// No scaling required

View File

@ -10,4 +10,6 @@ public class OrganizationsModel : PagedModel<Organization>
public bool? Paid { get; set; }
public string Action { get; set; }
public bool SelfHosted { get; set; }
public double StorageGB(Organization org) => org.Storage.HasValue ? Math.Round(org.Storage.Value / 1073741824D, 2) : 0;
}

View File

@ -81,16 +81,7 @@
<i class="fa fa-smile-o fa-lg fa-fw text-body-secondary" title="Freeloader"></i>
}
}
@if(org.MaxStorageGb.HasValue && org.MaxStorageGb > 1)
{
<i class="fa fa-plus-square fa-lg fa-fw"
title="Additional Storage, @(org.MaxStorageGb - 1) GB"></i>
}
else
{
<i class="fa fa-plus-square-o fa-lg fa-fw text-body-secondary"
title="No Additional Storage"></i>
}
<i class="fa fa-hdd-o fa-lg fa-fw" title="Used Storage, @Model.StorageGB(org) GB"></i>
@if(org.Enabled)
{
<i class="fa fa-check-circle fa-lg fa-fw"

View File

@ -9,6 +9,7 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Entities;
using Bit.Core.Models.BitStripe;
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
using Bit.Core.Platform.Installations;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;

View File

@ -92,7 +92,7 @@
@if (canPromoteAdmin)
{
<a class="dropdown-item" asp-controller="Tools" asp-action="PromoteAdmin">
Promote Admin
Promote Organization Admin
</a>
}
@if (canPromoteProviderServiceUser)

View File

@ -780,9 +780,9 @@
}
},
"node_modules/browserslist": {
"version": "4.24.2",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz",
"integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==",
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
"integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
"dev": true,
"funding": [
{
@ -800,9 +800,9 @@
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001669",
"electron-to-chromium": "^1.5.41",
"node-releases": "^2.0.18",
"caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73",
"node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.1"
},
"bin": {
@ -820,9 +820,9 @@
"license": "MIT"
},
"node_modules/caniuse-lite": {
"version": "1.0.30001688",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001688.tgz",
"integrity": "sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA==",
"version": "1.0.30001690",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz",
"integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==",
"dev": true,
"funding": [
{
@ -841,9 +841,9 @@
"license": "CC-BY-4.0"
},
"node_modules/chokidar": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -973,16 +973,16 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.73",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz",
"integrity": "sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg==",
"version": "1.5.75",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz",
"integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==",
"dev": true,
"license": "ISC"
},
"node_modules/enhanced-resolve": {
"version": "5.17.1",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz",
"integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==",
"version": "5.18.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz",
"integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1272,9 +1272,9 @@
}
},
"node_modules/is-core-module": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
"integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1793,19 +1793,22 @@
}
},
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.13.0",
"is-core-module": "^2.16.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
@ -2083,17 +2086,17 @@
}
},
"node_modules/terser-webpack-plugin": {
"version": "5.3.10",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz",
"integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==",
"version": "5.3.11",
"resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz",
"integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.20",
"@jridgewell/trace-mapping": "^0.3.25",
"jest-worker": "^27.4.5",
"schema-utils": "^3.1.1",
"serialize-javascript": "^6.0.1",
"terser": "^5.26.0"
"schema-utils": "^4.3.0",
"serialize-javascript": "^6.0.2",
"terser": "^5.31.1"
},
"engines": {
"node": ">= 10.13.0"
@ -2117,59 +2120,6 @@
}
}
},
"node_modules/terser-webpack-plugin/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/terser-webpack-plugin/node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"ajv": "^6.9.1"
}
},
"node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/terser-webpack-plugin/node_modules/schema-utils": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
"integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.8",
"ajv": "^6.12.5",
"ajv-keywords": "^3.5.2"
},
"engines": {
"node": ">= 10.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/to-regex-range": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",

View File

@ -2,7 +2,6 @@
using Bit.Api.AdminConsole.Models.Response;
using Bit.Api.Models.Response;
using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
@ -90,7 +89,7 @@ public class GroupsController : Controller
}
[HttpGet("")]
public async Task<ListResponseModel<GroupDetailsResponseModel>> GetOrganizationGroups(Guid orgId)
public async Task<ListResponseModel<GroupResponseModel>> GetOrganizationGroups(Guid orgId)
{
var authResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAll);
if (!authResult.Succeeded)
@ -98,24 +97,15 @@ public class GroupsController : Controller
throw new NotFoundException();
}
if (_featureService.IsEnabled(FeatureFlagKeys.SecureOrgGroupDetails))
{
var groups = await _groupRepository.GetManyByOrganizationIdAsync(orgId);
var responses = groups.Select(g => new GroupDetailsResponseModel(g, []));
return new ListResponseModel<GroupDetailsResponseModel>(responses);
}
var groupDetails = await _groupRepository.GetManyWithCollectionsByOrganizationIdAsync(orgId);
var detailResponses = groupDetails.Select(g => new GroupDetailsResponseModel(g.Item1, g.Item2));
return new ListResponseModel<GroupDetailsResponseModel>(detailResponses);
var groups = await _groupRepository.GetManyByOrganizationIdAsync(orgId);
var responses = groups.Select(g => new GroupResponseModel(g));
return new ListResponseModel<GroupResponseModel>(responses);
}
[HttpGet("details")]
public async Task<ListResponseModel<GroupDetailsResponseModel>> GetOrganizationGroupDetails(Guid orgId)
{
var authResult = _featureService.IsEnabled(FeatureFlagKeys.SecureOrgGroupDetails)
? await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAllDetails)
: await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAll);
var authResult = await _authorizationService.AuthorizeAsync(User, new OrganizationScope(orgId), GroupOperations.ReadAllDetails);
if (!authResult.Succeeded)
{

View File

@ -43,6 +43,7 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
UserId = organization.UserId;
ProviderId = organization.ProviderId;
ProviderName = organization.ProviderName;
ProviderType = organization.ProviderType;
ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier;
LimitCollectionCreation = organization.LimitCollectionCreation;
LimitCollectionDeletion = organization.LimitCollectionDeletion;

View File

@ -1023,11 +1023,28 @@ public class AccountsController : Controller
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
[AllowAnonymous]
[HttpPost("resend-new-device-otp")]
public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificatioRequestModel request)
public async Task ResendNewDeviceOtpAsync([FromBody] UnauthenticatedSecretVerificationRequestModel request)
{
await _userService.ResendNewDeviceVerificationEmail(request.Email, request.Secret);
}
[RequireFeature(FeatureFlagKeys.NewDeviceVerification)]
[HttpPost("verify-devices")]
[HttpPut("verify-devices")]
public async Task SetUserVerifyDevicesAsync([FromBody] SetVerifyDevicesRequestModel request)
{
var user = await _userService.GetUserByPrincipalAsync(User) ?? throw new UnauthorizedAccessException();
if (!await _userService.VerifySecretAsync(user, request.Secret))
{
await Task.Delay(2000);
throw new BadRequestException(string.Empty, "User verification failed.");
}
user.VerifyDevices = request.VerifyDevices;
await _userService.SaveUserAsync(user);
}
private async Task<IEnumerable<Guid>> GetOrganizationIdsManagingUserAsync(Guid userId)
{
var organizationManagingUser = await _userService.GetOrganizationsManagingUserAsync(userId);

View File

@ -0,0 +1,9 @@
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Auth.Models.Request.Accounts;
public class SetVerifyDevicesRequestModel : SecretVerificationRequestModel
{
[Required]
public bool VerifyDevices { get; set; }
}

View File

@ -3,7 +3,7 @@ using Bit.Core.Utilities;
namespace Bit.Api.Auth.Models.Request.Accounts;
public class UnauthenticatedSecretVerificatioRequestModel : SecretVerificationRequestModel
public class UnauthenticatedSecretVerificationRequestModel : SecretVerificationRequestModel
{
[Required]
[StrictEmailAddress]

View File

@ -1,7 +1,9 @@
#nullable enable
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Billing.Models.Requests;
using Bit.Api.Billing.Models.Responses;
using Bit.Core;
using Bit.Core.Billing.Models.Sales;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
using Bit.Core.Repositories;
@ -21,7 +23,8 @@ public class OrganizationBillingController(
IOrganizationRepository organizationRepository,
IPaymentService paymentService,
ISubscriberService subscriberService,
IPaymentHistoryService paymentHistoryService) : BaseBillingController
IPaymentHistoryService paymentHistoryService,
IUserService userService) : BaseBillingController
{
[HttpGet("metadata")]
public async Task<IResult> GetMetadataAsync([FromRoute] Guid organizationId)
@ -278,4 +281,37 @@ public class OrganizationBillingController(
return TypedResults.Ok();
}
[HttpPost("restart-subscription")]
public async Task<IResult> RestartSubscriptionAsync([FromRoute] Guid organizationId,
[FromBody] OrganizationCreateRequestModel model)
{
var user = await userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
if (!featureService.IsEnabled(FeatureFlagKeys.AC2476_DeprecateStripeSourcesAPI))
{
return Error.NotFound();
}
if (!await currentContext.EditPaymentMethods(organizationId))
{
return Error.Unauthorized();
}
var organization = await organizationRepository.GetByIdAsync(organizationId);
if (organization == null)
{
return Error.NotFound();
}
var organizationSignup = model.ToOrganizationSignup(user);
var sale = OrganizationSale.From(organization, organizationSignup);
await organizationBillingService.Finalize(sale);
return TypedResults.Ok();
}
}

View File

@ -9,6 +9,7 @@ public record OrganizationMetadataResponse(
bool IsSubscriptionUnpaid,
bool HasSubscription,
bool HasOpenInvoice,
bool IsSubscriptionCanceled,
DateTime? InvoiceDueDate,
DateTime? InvoiceCreatedDate,
DateTime? SubPeriodEndDate)
@ -21,6 +22,7 @@ public record OrganizationMetadataResponse(
metadata.IsSubscriptionUnpaid,
metadata.HasSubscription,
metadata.HasOpenInvoice,
metadata.IsSubscriptionCanceled,
metadata.InvoiceDueDate,
metadata.InvoiceCreatedDate,
metadata.SubPeriodEndDate);

View File

@ -6,7 +6,6 @@ using Bit.Api.Models.Response;
using Bit.Core.Auth.Models.Api.Request;
using Bit.Core.Auth.Models.Api.Response;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -70,11 +69,17 @@ public class DevicesController : Controller
}
[HttpGet("")]
public async Task<ListResponseModel<DeviceResponseModel>> Get()
public async Task<ListResponseModel<DeviceAuthRequestResponseModel>> Get()
{
ICollection<Device> devices = await _deviceRepository.GetManyByUserIdAsync(_userService.GetProperUserId(User).Value);
var responses = devices.Select(d => new DeviceResponseModel(d));
return new ListResponseModel<DeviceResponseModel>(responses);
var devicesWithPendingAuthData = await _deviceRepository.GetManyByUserIdWithDeviceAuth(_userService.GetProperUserId(User).Value);
// Convert from DeviceAuthDetails to DeviceAuthRequestResponseModel
var deviceAuthRequestResponseList = devicesWithPendingAuthData
.Select(DeviceAuthRequestResponseModel.From)
.ToList();
var response = new ListResponseModel<DeviceAuthRequestResponseModel>(deviceAuthRequestResponseList);
return response;
}
[HttpPost("")]

View File

@ -1,13 +1,20 @@
using Bit.Api.Models.Request;
using Bit.Api.Models.Response;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Exceptions;
using Bit.Core.Platform.Installations;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Controllers;
namespace Bit.Api.Platform.Installations;
/// <summary>
/// Routes used to manipulate `Installation` objects: a type used to manage
/// a record of a self hosted installation.
/// </summary>
/// <remarks>
/// This controller is not called from any clients. It's primarily referenced
/// in the `Setup` project for creating a new self hosted installation.
/// </remarks>
/// <seealso>Bit.Setup.Program</seealso>
[Route("installations")]
[SelfHosted(NotSelfHostedOnly = true)]
public class InstallationsController : Controller

View File

@ -1,8 +1,8 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Entities;
using Bit.Core.Platform.Installations;
using Bit.Core.Utilities;
namespace Bit.Api.Models.Request;
namespace Bit.Api.Platform.Installations;
public class InstallationRequestModel
{

View File

@ -1,7 +1,7 @@
using Bit.Core.Entities;
using Bit.Core.Models.Api;
using Bit.Core.Models.Api;
using Bit.Core.Platform.Installations;
namespace Bit.Api.Models.Response;
namespace Bit.Api.Platform.Installations;
public class InstallationResponseModel : ResponseModel
{

View File

@ -1,14 +1,18 @@
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Models.Api;
using Bit.Core.Services;
using Bit.Core.Platform.Push;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Controllers;
namespace Bit.Api.Platform.Push;
/// <summary>
/// Routes for push relay: functionality that facilitates communication
/// between self hosted organizations and Bitwarden cloud.
/// </summary>
[Route("push")]
[Authorize("Push")]
[SelfHosted(NotSelfHostedOnly = true)]

View File

@ -34,6 +34,9 @@ public static class ServiceCollectionExtensions
Url = new Uri("https://github.com/bitwarden/server/blob/master/LICENSE.txt")
}
});
config.CustomSchemaIds(type => type.FullName);
config.SwaggerDoc("internal", new OpenApiInfo { Title = "Bitwarden Internal API", Version = "latest" });
config.AddSecurityDefinition("oauth2-client-credentials", new OpenApiSecurityScheme

View File

@ -2,6 +2,7 @@
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Context;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.Enums;

View File

@ -1,5 +1,6 @@
using Bit.Billing.Constants;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Utilities;

View File

@ -4,10 +4,10 @@ namespace Bit.Core.AdminConsole.Enums.Provider;
public enum ProviderType : byte
{
[Display(ShortName = "MSP", Name = "Managed Service Provider", Description = "Access to clients organization", Order = 0)]
[Display(ShortName = "MSP", Name = "Managed Service Provider", Description = "Creates provider portal for client organization management", Order = 0)]
Msp = 0,
[Display(ShortName = "Reseller", Name = "Reseller", Description = "Access to clients billing", Order = 1000)]
[Display(ShortName = "Reseller", Name = "Reseller", Description = "Creates Bitwarden Portal page for client organization billing management", Order = 1000)]
Reseller = 1,
[Display(ShortName = "MOE", Name = "Multi-organization Enterprise", Description = "Access to multiple organizations", Order = 1)]
[Display(ShortName = "MOE", Name = "Multi-organization Enterprises", Description = "Creates provider portal for multi-organization management", Order = 1)]
MultiOrganizationEnterprise = 2,
}

View File

@ -44,4 +44,5 @@ public class ProviderUserOrganizationDetails
public bool LimitCollectionDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; }
public bool UseRiskInsights { get; set; }
public ProviderType ProviderType { get; set; }
}

View File

@ -7,6 +7,7 @@ using Bit.Core.Auth.Models.Api.Request.AuthRequest;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Services;
using Bit.Core.Enums;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;

View File

@ -4,6 +4,7 @@ using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.Enums;

View File

@ -3,6 +3,7 @@ using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;

View File

@ -11,6 +11,7 @@ using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data;
using Bit.Core.Models.StaticStore;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.Enums;

View File

@ -27,6 +27,7 @@ using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Models.Mail;
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Bit.Core.Tokens;

View File

@ -1,5 +1,12 @@
namespace Bit.Core.Auth.Enums;
/**
* The type of auth request.
*
* Note:
* Used by the Device_ReadActiveWithPendingAuthRequestsByUserId.sql stored procedure.
* If the enum changes be aware of this reference.
*/
public enum AuthRequestType : byte
{
AuthenticateAndUnlock = 0,

View File

@ -0,0 +1,51 @@
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Utilities;
using Bit.Core.Enums;
using Bit.Core.Models.Api;
namespace Bit.Core.Auth.Models.Api.Response;
public class DeviceAuthRequestResponseModel : ResponseModel
{
public DeviceAuthRequestResponseModel()
: base("device") { }
public static DeviceAuthRequestResponseModel From(DeviceAuthDetails deviceAuthDetails)
{
var converted = new DeviceAuthRequestResponseModel
{
Id = deviceAuthDetails.Id,
Name = deviceAuthDetails.Name,
Type = deviceAuthDetails.Type,
Identifier = deviceAuthDetails.Identifier,
CreationDate = deviceAuthDetails.CreationDate,
IsTrusted = deviceAuthDetails.IsTrusted()
};
if (deviceAuthDetails.AuthRequestId != null && deviceAuthDetails.AuthRequestCreatedAt != null)
{
converted.DevicePendingAuthRequest = new PendingAuthRequest
{
Id = (Guid)deviceAuthDetails.AuthRequestId,
CreationDate = (DateTime)deviceAuthDetails.AuthRequestCreatedAt
};
}
return converted;
}
public Guid Id { get; set; }
public string Name { get; set; }
public DeviceType Type { get; set; }
public string Identifier { get; set; }
public DateTime CreationDate { get; set; }
public bool IsTrusted { get; set; }
public PendingAuthRequest DevicePendingAuthRequest { get; set; }
public class PendingAuthRequest
{
public Guid Id { get; set; }
public DateTime CreationDate { get; set; }
}
}

View File

@ -0,0 +1,81 @@
using Bit.Core.Auth.Utilities;
using Bit.Core.Entities;
using Bit.Core.Enums;
namespace Bit.Core.Auth.Models.Data;
public class DeviceAuthDetails : Device
{
public bool IsTrusted { get; set; }
public Guid? AuthRequestId { get; set; }
public DateTime? AuthRequestCreatedAt { get; set; }
/**
* Constructor for EF response.
*/
public DeviceAuthDetails(
Device device,
Guid? authRequestId,
DateTime? authRequestCreationDate)
{
if (device == null)
{
throw new ArgumentNullException(nameof(device));
}
Id = device.Id;
Name = device.Name;
Type = device.Type;
Identifier = device.Identifier;
CreationDate = device.CreationDate;
IsTrusted = device.IsTrusted();
AuthRequestId = authRequestId;
AuthRequestCreatedAt = authRequestCreationDate;
}
/**
* Constructor for dapper response.
* Note: if the authRequestId or authRequestCreationDate is null it comes back as
* an empty guid and a min value for datetime. That could change if the stored
* procedure runs on a different kind of db.
*/
public DeviceAuthDetails(
Guid id,
Guid userId,
string name,
short type,
string identifier,
string pushToken,
DateTime creationDate,
DateTime revisionDate,
string encryptedUserKey,
string encryptedPublicKey,
string encryptedPrivateKey,
bool active,
Guid authRequestId,
DateTime authRequestCreationDate)
{
Id = id;
Name = name;
Type = (DeviceType)type;
Identifier = identifier;
CreationDate = creationDate;
IsTrusted = new Device
{
Id = id,
UserId = userId,
Name = name,
Type = (DeviceType)type,
Identifier = identifier,
PushToken = pushToken,
RevisionDate = revisionDate,
EncryptedUserKey = encryptedUserKey,
EncryptedPublicKey = encryptedPublicKey,
EncryptedPrivateKey = encryptedPrivateKey,
Active = active
}.IsTrusted();
AuthRequestId = authRequestId != Guid.Empty ? authRequestId : null;
AuthRequestCreatedAt =
authRequestCreationDate != DateTime.MinValue ? authRequestCreationDate : null;
}
}

View File

@ -1,5 +1,4 @@

using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Entities;
namespace Bit.Core.Auth.Models.Data;

View File

@ -7,6 +7,7 @@ using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;

View File

@ -23,7 +23,6 @@ namespace Bit.Core.Auth.UserFeatures.Registration.Implementations;
public class RegisterUserCommand : IRegisterUserCommand
{
private readonly IGlobalSettings _globalSettings;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IPolicyRepository _policyRepository;

View File

@ -3,6 +3,7 @@ using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Identity;

View File

@ -17,6 +17,7 @@ public static class ServiceCollectionExtensions
services.AddTransient<IPremiumUserBillingService, PremiumUserBillingService>();
services.AddTransient<ISetupIntentCache, SetupIntentDistributedCache>();
services.AddTransient<ISubscriberService, SubscriberService>();
// services.AddSingleton<IPricingClient, PricingClient>();
services.AddLicenseServices();
}
}

View File

@ -12,26 +12,42 @@ public class UserLicenseClaimsFactory : ILicenseClaimsFactory<User>
{
var subscriptionInfo = licenseContext.SubscriptionInfo;
var expires = subscriptionInfo.UpcomingInvoice?.Date?.AddDays(7) ?? entity.PremiumExpirationDate?.AddDays(7);
var refresh = subscriptionInfo.UpcomingInvoice?.Date ?? entity.PremiumExpirationDate;
var trial = (subscriptionInfo.Subscription?.TrialEndDate.HasValue ?? false) &&
var expires = subscriptionInfo?.UpcomingInvoice?.Date?.AddDays(7) ?? entity.PremiumExpirationDate?.AddDays(7);
var refresh = subscriptionInfo?.UpcomingInvoice?.Date ?? entity.PremiumExpirationDate;
var trial = (subscriptionInfo?.Subscription?.TrialEndDate.HasValue ?? false) &&
subscriptionInfo.Subscription.TrialEndDate.Value > DateTime.UtcNow;
var claims = new List<Claim>
{
new(nameof(UserLicenseConstants.LicenseType), LicenseType.User.ToString()),
new(nameof(UserLicenseConstants.LicenseKey), entity.LicenseKey),
new(nameof(UserLicenseConstants.Id), entity.Id.ToString()),
new(nameof(UserLicenseConstants.Name), entity.Name),
new(nameof(UserLicenseConstants.Email), entity.Email),
new(nameof(UserLicenseConstants.Premium), entity.Premium.ToString()),
new(nameof(UserLicenseConstants.MaxStorageGb), entity.MaxStorageGb.ToString()),
new(nameof(UserLicenseConstants.Issued), DateTime.UtcNow.ToString(CultureInfo.InvariantCulture)),
new(nameof(UserLicenseConstants.Expires), expires.ToString()),
new(nameof(UserLicenseConstants.Refresh), refresh.ToString()),
new(nameof(UserLicenseConstants.Trial), trial.ToString()),
};
if (entity.LicenseKey is not null)
{
claims.Add(new(nameof(UserLicenseConstants.LicenseKey), entity.LicenseKey));
}
if (entity.MaxStorageGb is not null)
{
claims.Add(new(nameof(UserLicenseConstants.MaxStorageGb), entity.MaxStorageGb.ToString()));
}
if (expires is not null)
{
claims.Add(new(nameof(UserLicenseConstants.Expires), expires.ToString()));
}
if (refresh is not null)
{
claims.Add(new(nameof(UserLicenseConstants.Refresh), refresh.ToString()));
}
return Task.FromResult(claims);
}
}

View File

@ -7,6 +7,7 @@ public record OrganizationMetadata(
bool IsSubscriptionUnpaid,
bool HasSubscription,
bool HasOpenInvoice,
bool IsSubscriptionCanceled,
DateTime? InvoiceDueDate,
DateTime? InvoiceCreatedDate,
DateTime? SubPeriodEndDate);

View File

@ -8,8 +8,11 @@ public abstract record Plan
public ProductTierType ProductTier { get; protected init; }
public string Name { get; protected init; }
public bool IsAnnual { get; protected init; }
// TODO: Move to the client
public string NameLocalizationKey { get; protected init; }
// TODO: Move to the client
public string DescriptionLocalizationKey { get; protected init; }
// TODO: Remove
public bool CanBeUsedByBusiness { get; protected init; }
public int? TrialPeriodDays { get; protected init; }
public bool HasSelfHost { get; protected init; }
@ -27,7 +30,9 @@ public abstract record Plan
public bool UsersGetPremium { get; protected init; }
public bool HasCustomPermissions { get; protected init; }
public int UpgradeSortOrder { get; protected init; }
// TODO: Move to the client
public int DisplaySortOrder { get; protected init; }
// TODO: Remove
public int? LegacyYear { get; protected init; }
public bool Disabled { get; protected init; }
public PasswordManagerPlanFeatures PasswordManager { get; protected init; }
@ -45,15 +50,19 @@ public abstract record Plan
public string StripeServiceAccountPlanId { get; init; }
public decimal? AdditionalPricePerServiceAccount { get; init; }
public short BaseServiceAccount { get; init; }
// TODO: Unused, remove
public short? MaxAdditionalServiceAccount { get; init; }
public bool HasAdditionalServiceAccountOption { get; init; }
// Seats
public string StripeSeatPlanId { get; init; }
public bool HasAdditionalSeatsOption { get; init; }
// TODO: Remove, SM is never packaged
public decimal BasePrice { get; init; }
public decimal SeatPrice { get; init; }
// TODO: Remove, SM is never packaged
public int BaseSeats { get; init; }
public short? MaxSeats { get; init; }
// TODO: Unused, remove
public int? MaxAdditionalSeats { get; init; }
public bool AllowSeatAutoscale { get; init; }
@ -72,8 +81,10 @@ public abstract record Plan
public decimal ProviderPortalSeatPrice { get; init; }
public bool AllowSeatAutoscale { get; init; }
public bool HasAdditionalSeatsOption { get; init; }
// TODO: Remove, never set.
public int? MaxAdditionalSeats { get; init; }
public int BaseSeats { get; init; }
// TODO: Remove premium access as it's deprecated
public bool HasPremiumAccessOption { get; init; }
public string StripePremiumAccessPlanId { get; init; }
public decimal PremiumAccessOptionPrice { get; init; }
@ -83,6 +94,7 @@ public abstract record Plan
public bool HasAdditionalStorageOption { get; init; }
public decimal AdditionalStoragePricePerGb { get; init; }
public string StripeStoragePlanId { get; init; }
// TODO: Remove
public short? MaxAdditionalStorage { get; init; }
// Feature
public short? MaxCollections { get; init; }

View File

@ -0,0 +1,12 @@
using Bit.Core.Billing.Enums;
using Bit.Core.Models.StaticStore;
#nullable enable
namespace Bit.Core.Billing.Pricing;
public interface IPricingClient
{
Task<Plan?> GetPlan(PlanType planType);
Task<List<Plan>> ListPlans();
}

View File

@ -0,0 +1,232 @@
using Bit.Core.Billing.Enums;
using Bit.Core.Models.StaticStore;
using Proto.Billing.Pricing;
#nullable enable
namespace Bit.Core.Billing.Pricing;
public record PlanAdapter : Plan
{
public PlanAdapter(PlanResponse planResponse)
{
Type = ToPlanType(planResponse.LookupKey);
ProductTier = ToProductTierType(Type);
Name = planResponse.Name;
IsAnnual = !string.IsNullOrEmpty(planResponse.Cadence) && planResponse.Cadence == "annually";
NameLocalizationKey = planResponse.AdditionalData?["nameLocalizationKey"];
DescriptionLocalizationKey = planResponse.AdditionalData?["descriptionLocalizationKey"];
TrialPeriodDays = planResponse.TrialPeriodDays;
HasSelfHost = HasFeature("selfHost");
HasPolicies = HasFeature("policies");
HasGroups = HasFeature("groups");
HasDirectory = HasFeature("directory");
HasEvents = HasFeature("events");
HasTotp = HasFeature("totp");
Has2fa = HasFeature("2fa");
HasApi = HasFeature("api");
HasSso = HasFeature("sso");
HasKeyConnector = HasFeature("keyConnector");
HasScim = HasFeature("scim");
HasResetPassword = HasFeature("resetPassword");
UsersGetPremium = HasFeature("usersGetPremium");
UpgradeSortOrder = planResponse.AdditionalData != null
? int.Parse(planResponse.AdditionalData["upgradeSortOrder"])
: 0;
DisplaySortOrder = planResponse.AdditionalData != null
? int.Parse(planResponse.AdditionalData["displaySortOrder"])
: 0;
HasCustomPermissions = HasFeature("customPermissions");
Disabled = !planResponse.Available;
PasswordManager = ToPasswordManagerPlanFeatures(planResponse);
SecretsManager = planResponse.SecretsManager != null ? ToSecretsManagerPlanFeatures(planResponse) : null;
return;
bool HasFeature(string lookupKey) => planResponse.Features.Any(feature => feature.LookupKey == lookupKey);
}
#region Mappings
private static PlanType ToPlanType(string lookupKey)
=> lookupKey switch
{
"enterprise-annually" => PlanType.EnterpriseAnnually,
"enterprise-annually-2019" => PlanType.EnterpriseAnnually2019,
"enterprise-annually-2020" => PlanType.EnterpriseAnnually2020,
"enterprise-annually-2023" => PlanType.EnterpriseAnnually2023,
"enterprise-monthly" => PlanType.EnterpriseMonthly,
"enterprise-monthly-2019" => PlanType.EnterpriseMonthly2019,
"enterprise-monthly-2020" => PlanType.EnterpriseMonthly2020,
"enterprise-monthly-2023" => PlanType.EnterpriseMonthly2023,
"families" => PlanType.FamiliesAnnually,
"families-2019" => PlanType.FamiliesAnnually2019,
"free" => PlanType.Free,
"teams-annually" => PlanType.TeamsAnnually,
"teams-annually-2019" => PlanType.TeamsAnnually2019,
"teams-annually-2020" => PlanType.TeamsAnnually2020,
"teams-annually-2023" => PlanType.TeamsAnnually2023,
"teams-monthly" => PlanType.TeamsMonthly,
"teams-monthly-2019" => PlanType.TeamsMonthly2019,
"teams-monthly-2020" => PlanType.TeamsMonthly2020,
"teams-monthly-2023" => PlanType.TeamsMonthly2023,
"teams-starter" => PlanType.TeamsStarter,
"teams-starter-2023" => PlanType.TeamsStarter2023,
_ => throw new BillingException() // TODO: Flesh out
};
private static ProductTierType ToProductTierType(PlanType planType)
=> planType switch
{
PlanType.Free => ProductTierType.Free,
PlanType.FamiliesAnnually or PlanType.FamiliesAnnually2019 => ProductTierType.Families,
PlanType.TeamsStarter or PlanType.TeamsStarter2023 => ProductTierType.TeamsStarter,
_ when planType.ToString().Contains("Teams") => ProductTierType.Teams,
_ when planType.ToString().Contains("Enterprise") => ProductTierType.Enterprise,
_ => throw new BillingException() // TODO: Flesh out
};
private static PasswordManagerPlanFeatures ToPasswordManagerPlanFeatures(PlanResponse planResponse)
{
var stripePlanId = GetStripePlanId(planResponse.Seats);
var stripeSeatPlanId = GetStripeSeatPlanId(planResponse.Seats);
var stripeProviderPortalSeatPlanId = planResponse.ManagedSeats?.StripePriceId;
var basePrice = GetBasePrice(planResponse.Seats);
var seatPrice = GetSeatPrice(planResponse.Seats);
var providerPortalSeatPrice =
planResponse.ManagedSeats != null ? decimal.Parse(planResponse.ManagedSeats.Price) : 0;
var scales = planResponse.Seats.KindCase switch
{
PurchasableDTO.KindOneofCase.Scalable => true,
PurchasableDTO.KindOneofCase.Packaged => planResponse.Seats.Packaged.Additional != null,
_ => false
};
var baseSeats = GetBaseSeats(planResponse.Seats);
var maxSeats = GetMaxSeats(planResponse.Seats);
var baseStorageGb = (short?)planResponse.Storage?.Provided;
var hasAdditionalStorageOption = planResponse.Storage != null;
var stripeStoragePlanId = planResponse.Storage?.StripePriceId;
short? maxCollections =
planResponse.AdditionalData != null &&
planResponse.AdditionalData.TryGetValue("passwordManager.maxCollections", out var value) ? short.Parse(value) : null;
return new PasswordManagerPlanFeatures
{
StripePlanId = stripePlanId,
StripeSeatPlanId = stripeSeatPlanId,
StripeProviderPortalSeatPlanId = stripeProviderPortalSeatPlanId,
BasePrice = basePrice,
SeatPrice = seatPrice,
ProviderPortalSeatPrice = providerPortalSeatPrice,
AllowSeatAutoscale = scales,
HasAdditionalSeatsOption = scales,
BaseSeats = baseSeats,
MaxSeats = maxSeats,
BaseStorageGb = baseStorageGb,
HasAdditionalStorageOption = hasAdditionalStorageOption,
StripeStoragePlanId = stripeStoragePlanId,
MaxCollections = maxCollections
};
}
private static SecretsManagerPlanFeatures ToSecretsManagerPlanFeatures(PlanResponse planResponse)
{
var seats = planResponse.SecretsManager.Seats;
var serviceAccounts = planResponse.SecretsManager.ServiceAccounts;
var maxServiceAccounts = GetMaxServiceAccounts(serviceAccounts);
var allowServiceAccountsAutoscale = serviceAccounts.KindCase == FreeOrScalableDTO.KindOneofCase.Scalable;
var stripeServiceAccountPlanId = GetStripeServiceAccountPlanId(serviceAccounts);
var additionalPricePerServiceAccount = GetAdditionalPricePerServiceAccount(serviceAccounts);
var baseServiceAccount = GetBaseServiceAccount(serviceAccounts);
var hasAdditionalServiceAccountOption = serviceAccounts.KindCase == FreeOrScalableDTO.KindOneofCase.Scalable;
var stripeSeatPlanId = GetStripeSeatPlanId(seats);
var hasAdditionalSeatsOption = seats.KindCase == FreeOrScalableDTO.KindOneofCase.Scalable;
var seatPrice = GetSeatPrice(seats);
var maxSeats = GetMaxSeats(seats);
var allowSeatAutoscale = seats.KindCase == FreeOrScalableDTO.KindOneofCase.Scalable;
var maxProjects =
planResponse.AdditionalData != null &&
planResponse.AdditionalData.TryGetValue("secretsManager.maxProjects", out var value) ? short.Parse(value) : 0;
return new SecretsManagerPlanFeatures
{
MaxServiceAccounts = maxServiceAccounts,
AllowServiceAccountsAutoscale = allowServiceAccountsAutoscale,
StripeServiceAccountPlanId = stripeServiceAccountPlanId,
AdditionalPricePerServiceAccount = additionalPricePerServiceAccount,
BaseServiceAccount = baseServiceAccount,
HasAdditionalServiceAccountOption = hasAdditionalServiceAccountOption,
StripeSeatPlanId = stripeSeatPlanId,
HasAdditionalSeatsOption = hasAdditionalSeatsOption,
SeatPrice = seatPrice,
MaxSeats = maxSeats,
AllowSeatAutoscale = allowSeatAutoscale,
MaxProjects = maxProjects
};
}
private static decimal? GetAdditionalPricePerServiceAccount(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Scalable
? null
: decimal.Parse(freeOrScalable.Scalable.Price);
private static decimal GetBasePrice(PurchasableDTO purchasable)
=> purchasable.KindCase != PurchasableDTO.KindOneofCase.Packaged ? 0 : decimal.Parse(purchasable.Packaged.Price);
private static int GetBaseSeats(PurchasableDTO purchasable)
=> purchasable.KindCase != PurchasableDTO.KindOneofCase.Packaged ? 0 : purchasable.Packaged.Quantity;
private static short GetBaseServiceAccount(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.KindCase switch
{
FreeOrScalableDTO.KindOneofCase.Free => (short)freeOrScalable.Free.Quantity,
FreeOrScalableDTO.KindOneofCase.Scalable => (short)freeOrScalable.Scalable.Provided,
_ => 0
};
private static short? GetMaxSeats(PurchasableDTO purchasable)
=> purchasable.KindCase != PurchasableDTO.KindOneofCase.Free ? null : (short)purchasable.Free.Quantity;
private static short? GetMaxSeats(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Free ? null : (short)freeOrScalable.Free.Quantity;
private static short? GetMaxServiceAccounts(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Free ? null : (short)freeOrScalable.Free.Quantity;
private static decimal GetSeatPrice(PurchasableDTO purchasable)
=> purchasable.KindCase switch
{
PurchasableDTO.KindOneofCase.Packaged => purchasable.Packaged.Additional != null ? decimal.Parse(purchasable.Packaged.Additional.Price) : 0,
PurchasableDTO.KindOneofCase.Scalable => decimal.Parse(purchasable.Scalable.Price),
_ => 0
};
private static decimal GetSeatPrice(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Scalable
? 0
: decimal.Parse(freeOrScalable.Scalable.Price);
private static string? GetStripePlanId(PurchasableDTO purchasable)
=> purchasable.KindCase != PurchasableDTO.KindOneofCase.Packaged ? null : purchasable.Packaged.StripePriceId;
private static string? GetStripeSeatPlanId(PurchasableDTO purchasable)
=> purchasable.KindCase switch
{
PurchasableDTO.KindOneofCase.Packaged => purchasable.Packaged.Additional?.StripePriceId,
PurchasableDTO.KindOneofCase.Scalable => purchasable.Scalable.StripePriceId,
_ => null
};
private static string? GetStripeSeatPlanId(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Scalable
? null
: freeOrScalable.Scalable.StripePriceId;
private static string? GetStripeServiceAccountPlanId(FreeOrScalableDTO freeOrScalable)
=> freeOrScalable.KindCase != FreeOrScalableDTO.KindOneofCase.Scalable
? null
: freeOrScalable.Scalable.StripePriceId;
#endregion
}

View File

@ -0,0 +1,92 @@
using Bit.Core.Billing.Enums;
using Bit.Core.Models.StaticStore;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Google.Protobuf.WellKnownTypes;
using Grpc.Core;
using Grpc.Net.Client;
using Proto.Billing.Pricing;
#nullable enable
namespace Bit.Core.Billing.Pricing;
public class PricingClient(
IFeatureService featureService,
GlobalSettings globalSettings) : IPricingClient
{
public async Task<Plan?> GetPlan(PlanType planType)
{
var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService);
if (!usePricingService)
{
return StaticStore.GetPlan(planType);
}
using var channel = GrpcChannel.ForAddress(globalSettings.PricingUri);
var client = new PasswordManager.PasswordManagerClient(channel);
var lookupKey = ToLookupKey(planType);
if (string.IsNullOrEmpty(lookupKey))
{
return null;
}
try
{
var response =
await client.GetPlanByLookupKeyAsync(new GetPlanByLookupKeyRequest { LookupKey = lookupKey });
return new PlanAdapter(response);
}
catch (RpcException rpcException) when (rpcException.StatusCode == StatusCode.NotFound)
{
return null;
}
}
public async Task<List<Plan>> ListPlans()
{
var usePricingService = featureService.IsEnabled(FeatureFlagKeys.UsePricingService);
if (!usePricingService)
{
return StaticStore.Plans.ToList();
}
using var channel = GrpcChannel.ForAddress(globalSettings.PricingUri);
var client = new PasswordManager.PasswordManagerClient(channel);
var response = await client.ListPlansAsync(new Empty());
return response.Plans.Select(Plan (plan) => new PlanAdapter(plan)).ToList();
}
private static string? ToLookupKey(PlanType planType)
=> planType switch
{
PlanType.EnterpriseAnnually => "enterprise-annually",
PlanType.EnterpriseAnnually2019 => "enterprise-annually-2019",
PlanType.EnterpriseAnnually2020 => "enterprise-annually-2020",
PlanType.EnterpriseAnnually2023 => "enterprise-annually-2023",
PlanType.EnterpriseMonthly => "enterprise-monthly",
PlanType.EnterpriseMonthly2019 => "enterprise-monthly-2019",
PlanType.EnterpriseMonthly2020 => "enterprise-monthly-2020",
PlanType.EnterpriseMonthly2023 => "enterprise-monthly-2023",
PlanType.FamiliesAnnually => "families",
PlanType.FamiliesAnnually2019 => "families-2019",
PlanType.Free => "free",
PlanType.TeamsAnnually => "teams-annually",
PlanType.TeamsAnnually2019 => "teams-annually-2019",
PlanType.TeamsAnnually2020 => "teams-annually-2020",
PlanType.TeamsAnnually2023 => "teams-annually-2023",
PlanType.TeamsMonthly => "teams-monthly",
PlanType.TeamsMonthly2019 => "teams-monthly-2019",
PlanType.TeamsMonthly2020 => "teams-monthly-2020",
PlanType.TeamsMonthly2023 => "teams-monthly-2023",
PlanType.TeamsStarter => "teams-starter",
PlanType.TeamsStarter2023 => "teams-starter-2023",
_ => null
};
}

View File

@ -0,0 +1,92 @@
syntax = "proto3";
option csharp_namespace = "Proto.Billing.Pricing";
package plans;
import "google/protobuf/empty.proto";
import "google/protobuf/struct.proto";
import "google/protobuf/wrappers.proto";
service PasswordManager {
rpc GetPlanByLookupKey (GetPlanByLookupKeyRequest) returns (PlanResponse);
rpc ListPlans (google.protobuf.Empty) returns (ListPlansResponse);
}
// Requests
message GetPlanByLookupKeyRequest {
string lookupKey = 1;
}
// Responses
message PlanResponse {
string name = 1;
string lookupKey = 2;
string tier = 4;
optional string cadence = 6;
optional google.protobuf.Int32Value legacyYear = 8;
bool available = 9;
repeated FeatureDTO features = 10;
PurchasableDTO seats = 11;
optional ScalableDTO managedSeats = 12;
optional ScalableDTO storage = 13;
optional SecretsManagerPurchasablesDTO secretsManager = 14;
optional google.protobuf.Int32Value trialPeriodDays = 15;
repeated string canUpgradeTo = 16;
map<string, string> additionalData = 17;
}
message ListPlansResponse {
repeated PlanResponse plans = 1;
}
// DTOs
message FeatureDTO {
string name = 1;
string lookupKey = 2;
}
message FreeDTO {
int32 quantity = 2;
string type = 4;
}
message PackagedDTO {
message AdditionalSeats {
string stripePriceId = 1;
string price = 2;
}
int32 quantity = 2;
string stripePriceId = 3;
string price = 4;
optional AdditionalSeats additional = 5;
string type = 6;
}
message ScalableDTO {
int32 provided = 2;
string stripePriceId = 6;
string price = 7;
string type = 9;
}
message PurchasableDTO {
oneof kind {
FreeDTO free = 1;
PackagedDTO packaged = 2;
ScalableDTO scalable = 3;
}
}
message FreeOrScalableDTO {
oneof kind {
FreeDTO free = 1;
ScalableDTO scalable = 2;
}
}
message SecretsManagerPurchasablesDTO {
FreeOrScalableDTO seats = 1;
FreeOrScalableDTO serviceAccounts = 2;
}

View File

@ -69,7 +69,7 @@ public class OrganizationBillingService(
if (string.IsNullOrWhiteSpace(organization.GatewaySubscriptionId))
{
return new OrganizationMetadata(isEligibleForSelfHost, isManaged, false,
false, false, false, null, null, null);
false, false, false, false, null, null, null);
}
var customer = await subscriberService.GetCustomer(organization,
@ -79,6 +79,7 @@ public class OrganizationBillingService(
var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription);
var isSubscriptionUnpaid = IsSubscriptionUnpaid(subscription);
var isSubscriptionCanceled = IsSubscriptionCanceled(subscription);
var hasSubscription = true;
var openInvoice = await HasOpenInvoiceAsync(subscription);
var hasOpenInvoice = openInvoice.HasOpenInvoice;
@ -87,7 +88,7 @@ public class OrganizationBillingService(
var subPeriodEndDate = subscription?.CurrentPeriodEnd;
return new OrganizationMetadata(isEligibleForSelfHost, isManaged, isOnSecretsManagerStandalone,
isSubscriptionUnpaid, hasSubscription, hasOpenInvoice, invoiceDueDate, invoiceCreatedDate, subPeriodEndDate);
isSubscriptionUnpaid, hasSubscription, hasOpenInvoice, isSubscriptionCanceled, invoiceDueDate, invoiceCreatedDate, subPeriodEndDate);
}
public async Task UpdatePaymentMethod(
@ -437,5 +438,15 @@ public class OrganizationBillingService(
? (true, invoice.Created, invoice.DueDate)
: (false, null, null);
}
private static bool IsSubscriptionCanceled(Subscription subscription)
{
if (subscription == null)
{
return false;
}
return subscription.Status == "canceled";
}
#endregion
}

View File

@ -133,6 +133,7 @@ public static class FeatureFlagKeys
public const string NativeCreateAccountFlow = "native-create-account-flow";
public const string AccountDeprovisioning = "pm-10308-account-deprovisioning";
public const string NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements";
public const string BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain";
public const string AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api";
public const string PersistPopupView = "persist-popup-view";
public const string CipherKeyEncryption = "cipher-key-encryption";
@ -140,7 +141,6 @@ public static class FeatureFlagKeys
public const string StorageReseedRefactor = "storage-reseed-refactor";
public const string TrialPayment = "PM-8163-trial-payment";
public const string RemoveServerVersionHeader = "remove-server-version-header";
public const string SecureOrgGroupDetails = "pm-3479-secure-org-group-details";
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
public const string PM12275_MultiOrganizationEnterprises = "pm-12275-multi-organization-enterprises";
public const string GeneratorToolsModernization = "generator-tools-modernization";
@ -150,7 +150,6 @@ public static class FeatureFlagKeys
public const string NewDeviceVerificationTemporaryDismiss = "new-device-temporary-dismiss";
public const string NewDeviceVerificationPermanentDismiss = "new-device-permanent-dismiss";
public const string SecurityTasks = "security-tasks";
public const string PM14401_ScaleMSPOnClientOrganizationUpdate = "PM-14401-scale-msp-on-client-organization-update";
public const string PM11360RemoveProviderExportPermission = "pm-11360-remove-provider-export-permission";
public const string DisableFreeFamiliesSponsorship = "PM-12274-disable-free-families-sponsorship";
public const string MacOsNativeCredentialSync = "macos-native-credential-sync";
@ -164,6 +163,8 @@ public static class FeatureFlagKeys
public const string AuthenticatorSyncAndroid = "enable-authenticator-sync-android";
public const string AppReviewPrompt = "app-review-prompt";
public const string ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs";
public const string UsePricingService = "use-pricing-service";
public const string RecordInstallationLastActivityDate = "installation-last-activity-date";
public static List<string> GetAllKeys()
{

View File

@ -21,10 +21,16 @@
<ItemGroup>
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.402.7" />
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.64" />
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.402.18" />
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.75" />
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
<PackageReference Include="Google.Protobuf" Version="3.29.2" />
<PackageReference Include="Grpc.Net.Client" Version="2.67.0" />
<PackageReference Include="Grpc.Tools" Version="2.68.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.18.1" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.21.2" />
@ -41,9 +47,10 @@
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.7.0" />
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.10" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="8.0.10" />
<PackageReference Include="OneOf" Version="3.0.271" />
<PackageReference Include="Quartz" Version="3.13.1" />
<PackageReference Include="SendGrid" Version="9.29.3" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
@ -62,6 +69,10 @@
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.6.0" />
</ItemGroup>
<ItemGroup>
<Protobuf Include="Billing\Pricing\Protos\password-manager.proto" GrpcServices="Client" />
</ItemGroup>
<ItemGroup>
<Folder Include="Resources\" />
<Folder Include="Properties\" />

View File

@ -72,6 +72,7 @@ public class User : ITableObject<Guid>, IStorableSubscriber, IRevisable, ITwoFac
public DateTime? LastKdfChangeDate { get; set; }
public DateTime? LastKeyRotationDate { get; set; }
public DateTime? LastEmailChangeDate { get; set; }
public bool VerifyDevices { get; set; } = true;
public void SetNewId()
{

View File

@ -8,7 +8,7 @@ using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Commands.Interfaces;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.KeyManagement.Repositories;
using Bit.Core.Services;
using Bit.Core.Platform.Push;
using Microsoft.Extensions.Logging;
namespace Bit.Core.KeyManagement.Commands;

View File

@ -1,6 +1,7 @@
using Bit.Core.Auth.Repositories;
using Bit.Core.Entities;
using Bit.Core.KeyManagement.Models.Data;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.Repositories;

View File

@ -6,8 +6,8 @@ using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.Models.Data;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
using Microsoft.AspNetCore.Http;

View File

@ -1,7 +1,7 @@
using Bit.Core.Enums;
using Bit.Core.Models.Data;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Microsoft.Azure.NotificationHubs;
using Microsoft.Extensions.Logging;

View File

@ -4,7 +4,7 @@ using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.OrganizationFeatures.OrganizationLicenses.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Platform.Installations;
using Bit.Core.Services;
namespace Bit.Core.OrganizationFeatures.OrganizationLicenses;

View File

@ -0,0 +1,14 @@
namespace Bit.Core.Platform.Installations;
/// <summary>
/// Command interface responsible for updating data on an `Installation`
/// record.
/// </summary>
/// <remarks>
/// This interface is implemented by `UpdateInstallationCommand`
/// </remarks>
/// <seealso cref="Bit.Core.Platform.Installations.UpdateInstallationCommand"/>
public interface IUpdateInstallationCommand
{
Task UpdateLastActivityDateAsync(Guid installationId);
}

View File

@ -0,0 +1,53 @@
namespace Bit.Core.Platform.Installations;
/// <summary>
/// Commands responsible for updating an installation from
/// `InstallationRepository`.
/// </summary>
/// <remarks>
/// If referencing: you probably want the interface
/// `IUpdateInstallationCommand` instead of directly calling this class.
/// </remarks>
/// <seealso cref="IUpdateInstallationCommand"/>
public class UpdateInstallationCommand : IUpdateInstallationCommand
{
private readonly IGetInstallationQuery _getInstallationQuery;
private readonly IInstallationRepository _installationRepository;
private readonly TimeProvider _timeProvider;
public UpdateInstallationCommand(
IGetInstallationQuery getInstallationQuery,
IInstallationRepository installationRepository,
TimeProvider timeProvider
)
{
_getInstallationQuery = getInstallationQuery;
_installationRepository = installationRepository;
_timeProvider = timeProvider;
}
public async Task UpdateLastActivityDateAsync(Guid installationId)
{
if (installationId == default)
{
throw new Exception
(
"Tried to update the last activity date for " +
"an installation, but an invalid installation id was " +
"provided."
);
}
var installation = await _getInstallationQuery.GetByIdAsync(installationId);
if (installation == null)
{
throw new Exception
(
"Tried to update the last activity date for " +
$"installation {installationId.ToString()}, but no " +
"installation was found for that id."
);
}
installation.LastActivityDate = _timeProvider.GetUtcNow().UtcDateTime;
await _installationRepository.UpsertAsync(installation);
}
}

View File

@ -1,10 +1,15 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Entities;
using Bit.Core.Utilities;
#nullable enable
namespace Bit.Core.Entities;
namespace Bit.Core.Platform.Installations;
/// <summary>
/// The base entity for the SQL table `dbo.Installation`. Used to store
/// information pertinent to self hosted Bitwarden installations.
/// </summary>
public class Installation : ITableObject<Guid>
{
public Guid Id { get; set; }
@ -14,6 +19,7 @@ public class Installation : ITableObject<Guid>
public string Key { get; set; } = null!;
public bool Enabled { get; set; }
public DateTime CreationDate { get; internal set; } = DateTime.UtcNow;
public DateTime? LastActivityDate { get; internal set; }
public void SetNewId()
{

View File

@ -0,0 +1,30 @@
namespace Bit.Core.Platform.Installations;
/// <summary>
/// Queries responsible for fetching an installation from
/// `InstallationRepository`.
/// </summary>
/// <remarks>
/// If referencing: you probably want the interface `IGetInstallationQuery`
/// instead of directly calling this class.
/// </remarks>
/// <seealso cref="IGetInstallationQuery"/>
public class GetInstallationQuery : IGetInstallationQuery
{
private readonly IInstallationRepository _installationRepository;
public GetInstallationQuery(IInstallationRepository installationRepository)
{
_installationRepository = installationRepository;
}
/// <inheritdoc cref="IGetInstallationQuery.GetByIdAsync"/>
public async Task<Installation> GetByIdAsync(Guid installationId)
{
if (installationId == default(Guid))
{
return null;
}
return await _installationRepository.GetByIdAsync(installationId);
}
}

View File

@ -0,0 +1,20 @@
namespace Bit.Core.Platform.Installations;
/// <summary>
/// Query interface responsible for fetching an installation from
/// `InstallationRepository`.
/// </summary>
/// <remarks>
/// This interface is implemented by `GetInstallationQuery`
/// </remarks>
/// <seealso cref="GetInstallationQuery"/>
public interface IGetInstallationQuery
{
/// <summary>
/// Retrieves an installation from the `InstallationRepository` by its id.
/// </summary>
/// <param name="installationId">The GUID id of the installation.</param>
/// <returns>A task containing an `Installation`.</returns>
/// <seealso cref="T:Bit.Core.Platform.Installations.Repositories.IInstallationRepository"/>
Task<Installation> GetByIdAsync(Guid installationId);
}

View File

@ -0,0 +1,19 @@
using Bit.Core.Repositories;
#nullable enable
namespace Bit.Core.Platform.Installations;
/// <summary>
/// The CRUD repository interface for communicating with `dbo.Installation`,
/// which is used to store information pertinent to self-hosted
/// installations.
/// </summary>
/// <remarks>
/// This interface is implemented by `InstallationRepository` in the Dapper
/// and Entity Framework projects.
/// </remarks>
/// <seealso cref="T:Bit.Infrastructure.Dapper.Platform.Installations.Repositories.InstallationRepository"/>
public interface IInstallationRepository : IRepository<Installation, Guid>
{
}

View File

@ -0,0 +1,19 @@
using Bit.Core.Platform.Installations;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.Platform;
public static class PlatformServiceCollectionExtensions
{
/// <summary>
/// Extend DI to include commands and queries exported from the Platform
/// domain.
/// </summary>
public static IServiceCollection AddPlatformServices(this IServiceCollection services)
{
services.AddScoped<IGetInstallationQuery, GetInstallationQuery>();
services.AddScoped<IUpdateInstallationCommand, UpdateInstallationCommand>();
return services;
}
}

View File

@ -11,7 +11,7 @@ using Bit.Core.Utilities;
using Bit.Core.Vault.Entities;
using Microsoft.AspNetCore.Http;
namespace Bit.Core.Services;
namespace Bit.Core.Platform.Push.Internal;
public class AzureQueuePushNotificationService : IPushNotificationService
{

View File

@ -4,7 +4,7 @@ using Bit.Core.Enums;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
namespace Bit.Core.Services;
namespace Bit.Core.Platform.Push;
public interface IPushNotificationService
{

View File

@ -1,6 +1,6 @@
using Bit.Core.Enums;
namespace Bit.Core.Services;
namespace Bit.Core.Platform.Push;
public interface IPushRegistrationService
{

View File

@ -7,7 +7,7 @@ using Bit.Core.Vault.Entities;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Services;
namespace Bit.Core.Platform.Push.Internal;
public class MultiServicePushNotificationService : IPushNotificationService
{

View File

@ -4,7 +4,7 @@ using Bit.Core.Enums;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
namespace Bit.Core.Services;
namespace Bit.Core.Platform.Push.Internal;
public class NoopPushNotificationService : IPushNotificationService
{

View File

@ -1,6 +1,6 @@
using Bit.Core.Enums;
namespace Bit.Core.Services;
namespace Bit.Core.Platform.Push.Internal;
public class NoopPushRegistrationService : IPushRegistrationService
{

View File

@ -3,13 +3,15 @@ using Bit.Core.Auth.Entities;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Models;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Services;
// This service is not in the `Internal` namespace because it has direct external references.
namespace Bit.Core.Platform.Push;
public class NotificationsApiPushNotificationService : BaseIdentityClientService, IPushNotificationService
{

View File

@ -6,13 +6,14 @@ using Bit.Core.IdentityServer;
using Bit.Core.Models;
using Bit.Core.Models.Api;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Services;
namespace Bit.Core.Platform.Push.Internal;
public class RelayPushNotificationService : BaseIdentityClientService, IPushNotificationService
{

View File

@ -1,14 +1,14 @@
using Bit.Core.Enums;
using Bit.Core.IdentityServer;
using Bit.Core.Models.Api;
using Bit.Core.Services;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging;
namespace Bit.Core.Services;
namespace Bit.Core.Platform.Push.Internal;
public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegistrationService
{
public RelayPushRegistrationService(
IHttpClientFactory httpFactory,
GlobalSettings globalSettings,

View File

@ -1,4 +1,5 @@
using Bit.Core.Entities;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Entities;
#nullable enable
@ -10,5 +11,9 @@ public interface IDeviceRepository : IRepository<Device, Guid>
Task<Device?> GetByIdentifierAsync(string identifier);
Task<Device?> GetByIdentifierAsync(string identifier, Guid userId);
Task<ICollection<Device>> GetManyByUserIdAsync(Guid userId);
// DeviceAuthDetails is passed back to decouple the response model from the
// repository in case more fields are ever added to the details response for
// other requests.
Task<ICollection<DeviceAuthDetails>> GetManyByUserIdWithDeviceAuth(Guid userId);
Task ClearPushTokenAsync(Guid id);
}

View File

@ -1,9 +0,0 @@
using Bit.Core.Entities;
#nullable enable
namespace Bit.Core.Repositories;
public interface IInstallationRepository : IRepository<Installation, Guid>
{
}

View File

@ -2,6 +2,7 @@
using Bit.Core.Auth.Utilities;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
namespace Bit.Core.Services;

View File

@ -16,6 +16,7 @@ public class LaunchDarklyFeatureService : IFeatureService
private readonly ICurrentContext _currentContext;
private const string _anonymousUser = "25a15cac-58cf-4ac0-ad0f-b17c4bd92294";
private const string _contextKindDevice = "device";
private const string _contextKindOrganization = "organization";
private const string _contextKindServiceAccount = "service-account";
@ -158,6 +159,16 @@ public class LaunchDarklyFeatureService : IFeatureService
var builder = LaunchDarkly.Sdk.Context.MultiBuilder();
if (!string.IsNullOrWhiteSpace(_currentContext.DeviceIdentifier))
{
var ldDevice = LaunchDarkly.Sdk.Context.Builder(_currentContext.DeviceIdentifier);
ldDevice.Kind(_contextKindDevice);
SetCommonContextAttributes(ldDevice);
builder.Add(ldDevice.Build());
}
switch (_currentContext.IdentityClientType)
{
case IdentityClientType.User:

View File

@ -18,6 +18,7 @@ using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Settings;
using Bit.Core.Tokens;
@ -1143,7 +1144,10 @@ public class UserService : UserManager<User>, IUserService, IDisposable
? new UserLicense(user, _licenseService)
: new UserLicense(user, subscriptionInfo, _licenseService);
userLicense.Token = await _licenseService.CreateUserTokenAsync(user, subscriptionInfo);
if (_featureService.IsEnabled(FeatureFlagKeys.SelfHostLicenseRefactor))
{
userLicense.Token = await _licenseService.CreateUserTokenAsync(user, subscriptionInfo);
}
return userLicense;
}

View File

@ -81,8 +81,8 @@ public class GlobalSettings : IGlobalSettings
public virtual IDomainVerificationSettings DomainVerification { get; set; } = new DomainVerificationSettings();
public virtual ILaunchDarklySettings LaunchDarkly { get; set; } = new LaunchDarklySettings();
public virtual string DevelopmentDirectory { get; set; }
public virtual bool EnableEmailVerification { get; set; }
public virtual string PricingUri { get; set; }
public string BuildExternalUri(string explicitValue, string name)
{

View File

@ -24,5 +24,7 @@ public interface IGlobalSettings
IPasswordlessAuthSettings PasswordlessAuth { get; set; }
IDomainVerificationSettings DomainVerification { get; set; }
ILaunchDarklySettings LaunchDarkly { get; set; }
string DatabaseProvider { get; set; }
GlobalSettings.SqlSettings SqlServer { get; set; }
string DevelopmentDirectory { get; set; }
}

View File

@ -6,6 +6,7 @@ using Bit.Core.AdminConsole.Services;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;

View File

@ -5,6 +5,7 @@ using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Platform.Push;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;

View File

@ -5,6 +5,7 @@ using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Identity;
using Bit.Core.IdentityServer;
using Bit.Core.Platform.Installations;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Models.Data;
using Bit.Core.SecretsManager.Repositories;

View File

@ -1,11 +1,13 @@
using System.Diagnostics;
using System.Security.Claims;
using Bit.Core;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Models.Api.Response;
using Bit.Core.Auth.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.IdentityServer;
using Bit.Core.Platform.Installations;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
@ -23,6 +25,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
ICustomTokenRequestValidator
{
private readonly UserManager<User> _userManager;
private readonly IUpdateInstallationCommand _updateInstallationCommand;
public CustomTokenRequestValidator(
UserManager<User> userManager,
@ -39,7 +42,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
IPolicyService policyService,
IFeatureService featureService,
ISsoConfigRepository ssoConfigRepository,
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
IUpdateInstallationCommand updateInstallationCommand
)
: base(
userManager,
@ -59,6 +63,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
userDecryptionOptionsBuilder)
{
_userManager = userManager;
_updateInstallationCommand = updateInstallationCommand;
}
public async Task ValidateAsync(CustomTokenRequestValidationContext context)
@ -76,16 +81,24 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
}
string[] allowedGrantTypes = ["authorization_code", "client_credentials"];
string clientId = context.Result.ValidatedRequest.ClientId;
if (!allowedGrantTypes.Contains(context.Result.ValidatedRequest.GrantType)
|| context.Result.ValidatedRequest.ClientId.StartsWith("organization")
|| context.Result.ValidatedRequest.ClientId.StartsWith("installation")
|| context.Result.ValidatedRequest.ClientId.StartsWith("internal")
|| clientId.StartsWith("organization")
|| clientId.StartsWith("installation")
|| clientId.StartsWith("internal")
|| context.Result.ValidatedRequest.Client.AllowedScopes.Contains(ApiScopes.ApiSecrets))
{
if (context.Result.ValidatedRequest.Client.Properties.TryGetValue("encryptedPayload", out var payload) &&
!string.IsNullOrWhiteSpace(payload))
{
context.Result.CustomResponse = new Dictionary<string, object> { { "encrypted_payload", payload } };
}
if (FeatureService.IsEnabled(FeatureFlagKeys.RecordInstallationLastActivityDate)
&& context.Result.ValidatedRequest.ClientId.StartsWith("installation"))
{
var installationIdPart = clientId.Split(".")[1];
await RecordActivityForInstallation(clientId.Split(".")[1]);
}
return;
}
@ -152,6 +165,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
context.Result.CustomResponse["KeyConnectorUrl"] = userDecryptionOptions.KeyConnectorOption.KeyConnectorUrl;
context.Result.CustomResponse["ResetMasterPassword"] = false;
}
return Task.CompletedTask;
}
@ -202,4 +216,25 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
context.Result.ErrorDescription = requestContext.ValidationErrorResult.ErrorDescription;
context.Result.CustomResponse = requestContext.CustomResponse;
}
/// <summary>
/// To help mentally separate organizations that self host from abandoned
/// organizations we hook in to the token refresh event for installations
/// to write a simple `DateTime.Now` to the database.
/// </summary>
/// <remarks>
/// This works well because installations don't phone home very often.
/// Currently self hosted installations only refresh tokens every 24
/// hours or so for the sake of hooking in to cloud's push relay service.
/// If installations ever start refreshing tokens more frequently we may need to
/// adjust this to avoid making a bunch of unnecessary database calls!
/// </remarks>
private async Task RecordActivityForInstallation(string? installationIdString)
{
if (!Guid.TryParse(installationIdString, out var installationId))
{
return;
}
await _updateInstallationCommand.UpdateLastActivityDateAsync(installationId);
}
}

View File

@ -115,7 +115,7 @@ public class DeviceValidator(
/// </summary>
/// <param name="user">user attempting to authenticate</param>
/// <param name="ValidatedRequest">The Request is used to check for the NewDeviceOtp and for the raw device data</param>
/// <returns>returns deviceValtaionResultType</returns>
/// <returns>returns deviceValidationResultType</returns>
private async Task<DeviceValidationResultType> HandleNewDeviceVerificationAsync(User user, ValidatedRequest request)
{
// currently unreachable due to backward compatibility
@ -125,6 +125,12 @@ public class DeviceValidator(
return DeviceValidationResultType.InvalidUser;
}
// Has the User opted out of new device verification
if (!user.VerifyDevices)
{
return DeviceValidationResultType.Success;
}
// CS exception flow
// Check cache for user information
var cacheKey = string.Format(AuthConstants.NewDeviceVerificationExceptionCacheKeyFormat, user.Id.ToString());
@ -146,6 +152,12 @@ public class DeviceValidator(
var otpValid = await _userService.VerifyOTPAsync(user, newDeviceOtp);
if (otpValid)
{
// In order to get here they would have to have access to their email so we verify it if it's not already
if (!user.EmailVerified)
{
user.EmailVerified = true;
await _userService.SaveUserAsync(user);
}
return DeviceValidationResultType.Success;
}
return DeviceValidationResultType.InvalidNewDeviceOtp;

View File

@ -44,8 +44,7 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,
IFeatureService featureService,
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand
)
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand)
: base(
userManager,
userService,

View File

@ -3,6 +3,7 @@ using Bit.Core.Auth.Repositories;
using Bit.Core.Billing.Repositories;
using Bit.Core.KeyManagement.Repositories;
using Bit.Core.NotificationCenter.Repositories;
using Bit.Core.Platform.Installations;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Tools.Repositories;
@ -12,6 +13,7 @@ using Bit.Infrastructure.Dapper.Auth.Repositories;
using Bit.Infrastructure.Dapper.Billing.Repositories;
using Bit.Infrastructure.Dapper.KeyManagement.Repositories;
using Bit.Infrastructure.Dapper.NotificationCenter.Repositories;
using Bit.Infrastructure.Dapper.Platform;
using Bit.Infrastructure.Dapper.Repositories;
using Bit.Infrastructure.Dapper.SecretsManager.Repositories;
using Bit.Infrastructure.Dapper.Tools.Repositories;

View File

@ -1,11 +1,19 @@
using Bit.Core.Entities;
using Bit.Core.Repositories;
using Bit.Core.Platform.Installations;
using Bit.Core.Settings;
using Bit.Infrastructure.Dapper.Repositories;
#nullable enable
namespace Bit.Infrastructure.Dapper.Repositories;
namespace Bit.Infrastructure.Dapper.Platform;
/// <summary>
/// The CRUD repository for communicating with `dbo.Installation`.
/// </summary>
/// <remarks>
/// If referencing: you probably want the interface `IInstallationRepository`
/// instead of directly calling this class.
/// </remarks>
/// <seealso cref="IInstallationRepository"/>
public class InstallationRepository : Repository<Installation, Guid>, IInstallationRepository
{
public InstallationRepository(GlobalSettings globalSettings)

View File

@ -1,4 +1,5 @@
using System.Data;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Entities;
using Bit.Core.Repositories;
using Bit.Core.Settings;
@ -11,9 +12,13 @@ namespace Bit.Infrastructure.Dapper.Repositories;
public class DeviceRepository : Repository<Device, Guid>, IDeviceRepository
{
private readonly IGlobalSettings _globalSettings;
public DeviceRepository(GlobalSettings globalSettings)
: this(globalSettings.SqlServer.ConnectionString, globalSettings.SqlServer.ReadOnlyConnectionString)
{ }
{
_globalSettings = globalSettings;
}
public DeviceRepository(string connectionString, string readOnlyConnectionString)
: base(connectionString, readOnlyConnectionString)
@ -76,6 +81,24 @@ public class DeviceRepository : Repository<Device, Guid>, IDeviceRepository
}
}
public async Task<ICollection<DeviceAuthDetails>> GetManyByUserIdWithDeviceAuth(Guid userId)
{
var expirationMinutes = _globalSettings.PasswordlessAuth.UserRequestExpiration.TotalMinutes;
using (var connection = new SqlConnection(ConnectionString))
{
var results = await connection.QueryAsync<DeviceAuthDetails>(
$"[{Schema}].[{Table}_ReadActiveWithPendingAuthRequestsByUserId]",
new
{
UserId = userId,
ExpirationMinutes = expirationMinutes
},
commandType: CommandType.StoredProcedure);
return results.ToList();
}
}
public async Task ClearPushTokenAsync(Guid id)
{
using (var connection = new SqlConnection(ConnectionString))

View File

@ -51,7 +51,7 @@ public abstract class Repository<T, TId> : BaseRepository, IRepository<T, TId>
var parameters = new DynamicParameters();
parameters.AddDynamicParams(obj);
parameters.Add("Id", obj.Id, direction: ParameterDirection.InputOutput);
var results = await connection.ExecuteAsync(
await connection.ExecuteAsync(
$"[{Schema}].[{Table}_Create]",
parameters,
commandType: CommandType.StoredProcedure);
@ -64,7 +64,7 @@ public abstract class Repository<T, TId> : BaseRepository, IRepository<T, TId>
{
using (var connection = new SqlConnection(ConnectionString))
{
var results = await connection.ExecuteAsync(
await connection.ExecuteAsync(
$"[{Schema}].[{Table}_Update]",
obj,
commandType: CommandType.StoredProcedure);

View File

@ -48,6 +48,7 @@ public class ProviderUserOrganizationDetailsViewQuery : IQuery<ProviderUserOrgan
LimitCollectionDeletion = x.o.LimitCollectionDeletion,
AllowAdminAccessToAllCollectionItems = x.o.AllowAdminAccessToAllCollectionItems,
UseRiskInsights = x.o.UseRiskInsights,
ProviderType = x.p.Type
});
}
}

View File

@ -0,0 +1,38 @@
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Infrastructure.EntityFramework.Repositories;
namespace Bit.Infrastructure.EntityFramework.Auth.Repositories.Queries;
public class DeviceWithPendingAuthByUserIdQuery
{
public IQueryable<DeviceAuthDetails> GetQuery(
DatabaseContext dbContext,
Guid userId,
int expirationMinutes)
{
var devicesWithAuthQuery = (
from device in dbContext.Devices
where device.UserId == userId && device.Active
select new
{
device,
authRequest =
(
from authRequest in dbContext.AuthRequests
where authRequest.RequestDeviceIdentifier == device.Identifier
where authRequest.Type == AuthRequestType.AuthenticateAndUnlock || authRequest.Type == AuthRequestType.Unlock
where authRequest.Approved == null
where authRequest.UserId == userId
where authRequest.CreationDate.AddMinutes(expirationMinutes) > DateTime.UtcNow
orderby authRequest.CreationDate descending
select authRequest
).First()
}).Select(deviceWithAuthRequest => new DeviceAuthDetails(
deviceWithAuthRequest.device,
deviceWithAuthRequest.authRequest.Id,
deviceWithAuthRequest.authRequest.CreationDate));
return devicesWithAuthQuery;
}
}

View File

@ -1,6 +1,6 @@
using AutoMapper;
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
using Bit.Infrastructure.EntityFramework.Models;
using Bit.Infrastructure.EntityFramework.Platform;
namespace Bit.Infrastructure.EntityFramework.Billing.Models;

View File

@ -4,6 +4,7 @@ using Bit.Core.Billing.Repositories;
using Bit.Core.Enums;
using Bit.Core.KeyManagement.Repositories;
using Bit.Core.NotificationCenter.Repositories;
using Bit.Core.Platform.Installations;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Tools.Repositories;
@ -13,6 +14,7 @@ using Bit.Infrastructure.EntityFramework.Auth.Repositories;
using Bit.Infrastructure.EntityFramework.Billing.Repositories;
using Bit.Infrastructure.EntityFramework.KeyManagement.Repositories;
using Bit.Infrastructure.EntityFramework.NotificationCenter.Repositories;
using Bit.Infrastructure.EntityFramework.Platform;
using Bit.Infrastructure.EntityFramework.Repositories;
using Bit.Infrastructure.EntityFramework.SecretsManager.Repositories;
using Bit.Infrastructure.EntityFramework.Tools.Repositories;

Some files were not shown because too many files have changed in this diff Show More