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:
commit
80f8780109
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -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
|
||||
|
34
.github/workflows/build.yml
vendored
34
.github/workflows/build.yml
vendored
@ -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
|
||||
|
2
.github/workflows/code-references.yml
vendored
2
.github/workflows/code-references.yml
vendored
@ -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
|
||||
|
6
.github/workflows/repository-management.yml
vendored
6
.github/workflows/repository-management.yml
vendored
@ -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 }}
|
||||
|
8
.github/workflows/scan.yml
vendored
8
.github/workflows/scan.yml
vendored
@ -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
|
||||
|
12
.github/workflows/test-database.yml
vendored
12
.github/workflows/test-database.yml
vendored
@ -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"
|
||||
|
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@ -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 }}
|
||||
|
@ -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>
|
@ -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);
|
||||
|
120
bitwarden_license/src/Sso/package-lock.json
generated
120
bitwarden_license/src/Sso/package-lock.json
generated
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -92,7 +92,7 @@
|
||||
@if (canPromoteAdmin)
|
||||
{
|
||||
<a class="dropdown-item" asp-controller="Tools" asp-action="PromoteAdmin">
|
||||
Promote Admin
|
||||
Promote Organization Admin
|
||||
</a>
|
||||
}
|
||||
@if (canPromoteProviderServiceUser)
|
||||
|
120
src/Admin/package-lock.json
generated
120
src/Admin/package-lock.json
generated
@ -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",
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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; }
|
||||
}
|
@ -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]
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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("")]
|
||||
|
@ -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
|
@ -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
|
||||
{
|
@ -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
|
||||
{
|
@ -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)]
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
}
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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; }
|
||||
}
|
||||
}
|
81
src/Core/Auth/Models/Data/DeviceAuthDetails.cs
Normal file
81
src/Core/Auth/Models/Data/DeviceAuthDetails.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Entities;
|
||||
|
||||
namespace Bit.Core.Auth.Models.Data;
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ public record OrganizationMetadata(
|
||||
bool IsSubscriptionUnpaid,
|
||||
bool HasSubscription,
|
||||
bool HasOpenInvoice,
|
||||
bool IsSubscriptionCanceled,
|
||||
DateTime? InvoiceDueDate,
|
||||
DateTime? InvoiceCreatedDate,
|
||||
DateTime? SubPeriodEndDate);
|
||||
|
@ -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; }
|
||||
|
12
src/Core/Billing/Pricing/IPricingClient.cs
Normal file
12
src/Core/Billing/Pricing/IPricingClient.cs
Normal 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();
|
||||
}
|
232
src/Core/Billing/Pricing/PlanAdapter.cs
Normal file
232
src/Core/Billing/Pricing/PlanAdapter.cs
Normal 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
|
||||
}
|
92
src/Core/Billing/Pricing/PricingClient.cs
Normal file
92
src/Core/Billing/Pricing/PricingClient.cs
Normal 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
|
||||
};
|
||||
}
|
92
src/Core/Billing/Pricing/Protos/password-manager.proto
Normal file
92
src/Core/Billing/Pricing/Protos/password-manager.proto
Normal 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;
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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\" />
|
||||
|
@ -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()
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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()
|
||||
{
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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>
|
||||
{
|
||||
}
|
19
src/Core/Platform/PlatformServiceCollectionExtensions.cs
Normal file
19
src/Core/Platform/PlatformServiceCollectionExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
@ -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
|
||||
{
|
@ -1,6 +1,6 @@
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
namespace Bit.Core.Platform.Push;
|
||||
|
||||
public interface IPushRegistrationService
|
||||
{
|
@ -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
|
||||
{
|
@ -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
|
||||
{
|
@ -1,6 +1,6 @@
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
namespace Bit.Core.Platform.Push.Internal;
|
||||
|
||||
public class NoopPushRegistrationService : IPushRegistrationService
|
||||
{
|
@ -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
|
||||
{
|
@ -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
|
||||
{
|
@ -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,
|
@ -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);
|
||||
}
|
||||
|
@ -1,9 +0,0 @@
|
||||
using Bit.Core.Entities;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Core.Repositories;
|
||||
|
||||
public interface IInstallationRepository : IRepository<Installation, Guid>
|
||||
{
|
||||
}
|
@ -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;
|
||||
|
@ -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:
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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)
|
||||
{
|
||||
|
@ -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; }
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -44,8 +44,7 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
|
||||
IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> assertionOptionsDataProtector,
|
||||
IFeatureService featureService,
|
||||
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
|
||||
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand
|
||||
)
|
||||
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand)
|
||||
: base(
|
||||
userManager,
|
||||
userService,
|
||||
|
@ -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;
|
||||
|
@ -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)
|
@ -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))
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
|
@ -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
Loading…
Reference in New Issue
Block a user