diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 7945c3e7e3..1b76bccf2c 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "swashbuckle.aspnetcore.cli": { - "version": "6.7.3", + "version": "6.8.0", "commands": ["swagger"] }, "dotnet-ef": { diff --git a/.github/workflows/_move_finalization_db_scripts.yml b/.github/workflows/_move_finalization_db_scripts.yml index fc8a7b76e2..c54e3abb2a 100644 --- a/.github/workflows/_move_finalization_db_scripts.yml +++ b/.github/workflows/_move_finalization_db_scripts.yml @@ -30,7 +30,7 @@ jobs: secrets: "github-pat-bitwarden-devops-bot-repo-scope" - name: Check out branch - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: token: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }} @@ -54,7 +54,7 @@ jobs: if: ${{ needs.setup.outputs.copy_finalization_scripts == 'true' }} steps: - name: Checkout - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: fetch-depth: 0 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a7bf370cc3..fe4063f441 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up .NET uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 @@ -68,7 +68,7 @@ jobs: node: true steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up .NET uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 @@ -173,7 +173,7 @@ jobs: dotnet: true steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Check branch to publish env: @@ -263,7 +263,7 @@ jobs: -d ${{ matrix.base_path }}/${{ matrix.project_name }}/obj/build-output/publish - name: Build Docker image - uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85 # v6.7.0 + uses: docker/build-push-action@32945a339266b759abcbdc89316275140b0fc960 # v6.8.0 with: context: ${{ matrix.base_path }}/${{ matrix.project_name }} file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile @@ -282,7 +282,7 @@ jobs: output-format: sarif - name: Upload Grype results to GitHub - uses: github/codeql-action/upload-sarif@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 + uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 with: sarif_file: ${{ steps.container-scan.outputs.sarif }} @@ -292,7 +292,7 @@ jobs: needs: build-docker steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up .NET uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 @@ -467,7 +467,7 @@ jobs: - win-x64 steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up .NET uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 diff --git a/.github/workflows/cleanup-rc-branch.yml b/.github/workflows/cleanup-rc-branch.yml index abd7c4bb41..3b3c2d55de 100644 --- a/.github/workflows/cleanup-rc-branch.yml +++ b/.github/workflows/cleanup-rc-branch.yml @@ -24,7 +24,7 @@ jobs: secrets: "github-pat-bitwarden-devops-bot-repo-scope" - name: Checkout main - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: ref: main token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }} diff --git a/.github/workflows/code-references.yml b/.github/workflows/code-references.yml index 90523ba176..101e5730d4 100644 --- a/.github/workflows/code-references.yml +++ b/.github/workflows/code-references.yml @@ -33,7 +33,7 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Collect id: collect diff --git a/.github/workflows/container-registry-purge.yml b/.github/workflows/container-registry-purge.yml deleted file mode 100644 index 1fc4c511bd..0000000000 --- a/.github/workflows/container-registry-purge.yml +++ /dev/null @@ -1,102 +0,0 @@ ---- -name: Container registry purge - -on: - schedule: - - cron: "0 0 * * SUN" - workflow_dispatch: - inputs: {} - -jobs: - purge: - name: Purge old images - runs-on: ubuntu-22.04 - steps: - - name: Log in to Azure - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - with: - creds: ${{ secrets.AZURE_PROD_KV_CREDENTIALS }} - - - name: Purge images - env: - REGISTRY: bitwardenprod - AGO_DUR_VER: "180d" - AGO_DUR: "30d" - run: | - REPO_LIST=$(az acr repository list -n $REGISTRY -o tsv) - for REPO in $REPO_LIST - do - - PURGE_LATEST="" - PURGE_VERSION="" - PURGE_ELSE="" - - TAG_LIST=$(az acr repository show-tags -n $REGISTRY --repository $REPO -o tsv) - for TAG in $TAG_LIST - do - if [ $TAG = "latest" ] || [ $TAG = "dev" ]; then - PURGE_LATEST+="--filter '$REPO:$TAG' " - elif [[ $TAG =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then - PURGE_VERSION+="--filter '$REPO:$TAG' " - else - PURGE_ELSE+="--filter '$REPO:$TAG' " - fi - done - - if [ ! -z "$PURGE_LATEST" ] - then - PURGE_LATEST_CMD="acr purge $PURGE_LATEST --ago $AGO_DUR_VER --untagged --keep 1" - az acr run --cmd "$PURGE_LATEST_CMD" --registry $REGISTRY /dev/null & - fi - - if [ ! -z "$PURGE_VERSION" ] - then - PURGE_VERSION_CMD="acr purge $PURGE_VERSION --ago $AGO_DUR_VER --untagged" - az acr run --cmd "$PURGE_VERSION_CMD" --registry $REGISTRY /dev/null & - fi - - if [ ! -z "$PURGE_ELSE" ] - then - PURGE_ELSE_CMD="acr purge $PURGE_ELSE --ago $AGO_DUR --untagged" - az acr run --cmd "$PURGE_ELSE_CMD" --registry $REGISTRY /dev/null & - fi - - wait - - done - - check-failures: - name: Check for failures - if: always() - runs-on: ubuntu-22.04 - needs: [purge] - steps: - - name: Check if any job failed - if: | - (github.ref == 'refs/heads/main' - || github.ref == 'refs/heads/rc' - || github.ref == 'refs/heads/hotfix-rc') - && contains(needs.*.result, 'failure') - run: exit 1 - - - name: Log in to Azure - CI subscription - uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0 - if: failure() - with: - creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }} - - - name: Retrieve secrets - id: retrieve-secrets - uses: bitwarden/gh-actions/get-keyvault-secrets@main - if: failure() - with: - keyvault: "bitwarden-ci" - secrets: "devops-alerts-slack-webhook-url" - - - name: Notify Slack on failure - uses: act10ns/slack@44541246747a30eb3102d87f7a4cc5471b0ffb7d # v2.1.0 - if: failure() - env: - SLACK_WEBHOOK_URL: ${{ steps.retrieve-secrets.outputs.devops-alerts-slack-webhook-url }} - with: - status: ${{ job.status }} diff --git a/.github/workflows/protect-files.yml b/.github/workflows/protect-files.yml index 9e2e03d67c..3bbc7e74f1 100644 --- a/.github/workflows/protect-files.yml +++ b/.github/workflows/protect-files.yml @@ -29,7 +29,7 @@ jobs: label: "DB-migrations-changed" steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: fetch-depth: 2 diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 331f996c02..3c45f84b75 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -99,7 +99,7 @@ jobs: echo "Github Release Option: $RELEASE_OPTION" - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up project name id: setup diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7e0e8ae263..c63302cbc5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,7 @@ jobs: fi - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Check release version id: version diff --git a/.github/workflows/scan.yml b/.github/workflows/scan.yml index b4335ee491..0f4d060ba5 100644 --- a/.github/workflows/scan.yml +++ b/.github/workflows/scan.yml @@ -26,7 +26,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: ref: ${{ github.event.pull_request.head.sha }} @@ -46,7 +46,7 @@ jobs: --output-path . ${{ env.INCREMENTAL }} - name: Upload Checkmarx results to GitHub - uses: github/codeql-action/upload-sarif@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8 + uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9 with: sarif_file: cx_result.sarif @@ -66,7 +66,7 @@ jobs: distribution: "zulu" - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: fetch-depth: 0 ref: ${{ github.event.pull_request.head.sha }} diff --git a/.github/workflows/test-database.yml b/.github/workflows/test-database.yml index ef02f8b707..325f10b94d 100644 --- a/.github/workflows/test-database.yml +++ b/.github/workflows/test-database.yml @@ -36,7 +36,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up .NET uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 @@ -147,7 +147,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up .NET uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1b4739df1d..216130a21b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -46,7 +46,7 @@ jobs: steps: - name: Check out repo - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Set up .NET uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1 diff --git a/.github/workflows/version-bump.yml b/.github/workflows/version-bump.yml index 0fb8c4a22a..e1d96ee4db 100644 --- a/.github/workflows/version-bump.yml +++ b/.github/workflows/version-bump.yml @@ -39,7 +39,7 @@ jobs: fi - name: Check out branch - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 - name: Check if RC branch exists if: ${{ inputs.cut_rc_branch == true }} @@ -230,7 +230,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: Check out branch - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7 + uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 with: ref: main diff --git a/bitwarden_license/src/Scim/Context/ScimContext.cs b/bitwarden_license/src/Scim/Context/ScimContext.cs index 71ea27df4c..efcc8dbde3 100644 --- a/bitwarden_license/src/Scim/Context/ScimContext.cs +++ b/bitwarden_license/src/Scim/Context/ScimContext.cs @@ -4,6 +4,7 @@ using Bit.Core.AdminConsole.Models.OrganizationConnectionConfigs; using Bit.Core.Enums; using Bit.Core.Repositories; using Bit.Core.Settings; +using Bit.Core.Utilities; namespace Bit.Scim.Context; @@ -11,6 +12,32 @@ public class ScimContext : IScimContext { private bool _builtHttpContext; + // See IP list from Ping in docs: https://support.pingidentity.com/s/article/PingOne-IP-Addresses + private static readonly HashSet _pingIpAddresses = + [ + "18.217.152.87", + "52.14.10.143", + "13.58.49.148", + "34.211.92.81", + "54.214.158.219", + "34.218.98.164", + "15.223.133.47", + "3.97.84.38", + "15.223.19.71", + "3.97.98.120", + "52.60.115.173", + "3.97.202.223", + "18.184.65.93", + "52.57.244.92", + "18.195.7.252", + "108.128.67.71", + "34.246.158.102", + "108.128.250.27", + "52.63.103.92", + "13.54.131.18", + "52.62.204.36" + ]; + public ScimProviderType RequestScimProvider { get; set; } = ScimProviderType.Default; public ScimConfig ScimConfiguration { get; set; } public Guid? OrganizationId { get; set; } @@ -55,10 +82,18 @@ public class ScimContext : IScimContext RequestScimProvider = ScimProviderType.Okta; } } + if (RequestScimProvider == ScimProviderType.Default && httpContext.Request.Headers.ContainsKey("Adscimversion")) { RequestScimProvider = ScimProviderType.AzureAd; } + + var ipAddress = CoreHelpers.GetIpAddress(httpContext, globalSettings); + if (RequestScimProvider == ScimProviderType.Default && + _pingIpAddresses.Contains(ipAddress)) + { + RequestScimProvider = ScimProviderType.Ping; + } } } diff --git a/bitwarden_license/src/Scim/Groups/PutGroupCommand.cs b/bitwarden_license/src/Scim/Groups/PutGroupCommand.cs index d9cfc0d86f..2503380a00 100644 --- a/bitwarden_license/src/Scim/Groups/PutGroupCommand.cs +++ b/bitwarden_license/src/Scim/Groups/PutGroupCommand.cs @@ -43,7 +43,8 @@ public class PutGroupCommand : IPutGroupCommand private async Task UpdateGroupMembersAsync(Group group, ScimGroupRequestModel model) { - if (_scimContext.RequestScimProvider != ScimProviderType.Okta) + if (_scimContext.RequestScimProvider != ScimProviderType.Okta && + _scimContext.RequestScimProvider != ScimProviderType.Ping) { return; } diff --git a/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs b/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs index 51250250fe..1bea930f1d 100644 --- a/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs +++ b/bitwarden_license/src/Scim/Users/GetUsersListQuery.cs @@ -20,15 +20,16 @@ public class GetUsersListQuery : IGetUsersListQuery string externalIdFilter = null; if (!string.IsNullOrWhiteSpace(filter)) { - if (filter.StartsWith("userName eq ")) + var filterLower = filter.ToLowerInvariant(); + if (filterLower.StartsWith("username eq ")) { - usernameFilter = filter.Substring(12).Trim('"').ToLowerInvariant(); + usernameFilter = filterLower.Substring(12).Trim('"'); if (usernameFilter.Contains("@")) { emailFilter = usernameFilter; } } - else if (filter.StartsWith("externalId eq ")) + else if (filterLower.StartsWith("externalid eq ")) { externalIdFilter = filter.Substring(14).Trim('"'); } diff --git a/bitwarden_license/test/Scim.IntegrationTest/Scim.IntegrationTest.csproj b/bitwarden_license/test/Scim.IntegrationTest/Scim.IntegrationTest.csproj index 7ece41ecac..a84813fd7c 100644 --- a/bitwarden_license/test/Scim.IntegrationTest/Scim.IntegrationTest.csproj +++ b/bitwarden_license/test/Scim.IntegrationTest/Scim.IntegrationTest.csproj @@ -9,7 +9,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive all - + diff --git a/src/Admin/Admin.csproj b/src/Admin/Admin.csproj index cd30e841b4..5493e65afd 100644 --- a/src/Admin/Admin.csproj +++ b/src/Admin/Admin.csproj @@ -14,6 +14,10 @@ + + + + diff --git a/src/Admin/AdminConsole/Controllers/ProvidersController.cs b/src/Admin/AdminConsole/Controllers/ProvidersController.cs index 4adf0fce0c..12e2c4d439 100644 --- a/src/Admin/AdminConsole/Controllers/ProvidersController.cs +++ b/src/Admin/AdminConsole/Controllers/ProvidersController.cs @@ -367,7 +367,7 @@ public class ProvidersController : Controller return BadRequest("Provider does not exist"); } - if (!string.Equals(providerName.Trim(), provider.Name, StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(providerName.Trim(), provider.DisplayName(), StringComparison.OrdinalIgnoreCase)) { return BadRequest("Invalid provider name"); } diff --git a/src/Admin/AdminConsole/Views/Providers/Edit.cshtml b/src/Admin/AdminConsole/Views/Providers/Edit.cshtml index f4cf36f925..37cda8417a 100644 --- a/src/Admin/AdminConsole/Views/Providers/Edit.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/Edit.cshtml @@ -174,18 +174,15 @@
- @if (FeatureService.IsEnabled(FeatureFlagKeys.EnableDeleteProvider)) - { -
- - +
+ + - - + + - + -
- } +
} diff --git a/src/Admin/AdminConsole/Views/Providers/_ProviderScripts.cshtml b/src/Admin/AdminConsole/Views/Providers/_ProviderScripts.cshtml index 4fa1ed757a..68af34ebd2 100644 --- a/src/Admin/AdminConsole/Views/Providers/_ProviderScripts.cshtml +++ b/src/Admin/AdminConsole/Views/Providers/_ProviderScripts.cshtml @@ -20,9 +20,10 @@ function deleteProvider(id) { const providerName = $('#DeleteModal input#provider-name').val(); + const encodedProviderName = encodeURIComponent(providerName); $.ajax({ type: "POST", - url: `@Url.Action("Delete", "Providers")?id=${id}&providerName=${providerName}`, + url: `@Url.Action("Delete", "Providers")?id=${id}&providerName=${encodedProviderName}`, dataType: 'json', contentType: false, processData: false, diff --git a/src/Admin/Billing/Controllers/MigrateProvidersController.cs b/src/Admin/Billing/Controllers/MigrateProvidersController.cs new file mode 100644 index 0000000000..d4ef105e34 --- /dev/null +++ b/src/Admin/Billing/Controllers/MigrateProvidersController.cs @@ -0,0 +1,83 @@ +using Bit.Admin.Billing.Models; +using Bit.Admin.Enums; +using Bit.Admin.Utilities; +using Bit.Core.Billing.Migration.Models; +using Bit.Core.Billing.Migration.Services; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Admin.Billing.Controllers; + +[Authorize] +[Route("migrate-providers")] +[SelfHosted(NotSelfHostedOnly = true)] +public class MigrateProvidersController( + IProviderMigrator providerMigrator) : Controller +{ + [HttpGet] + [RequirePermission(Permission.Tools_MigrateProviders)] + public IActionResult Index() + { + return View(new MigrateProvidersRequestModel()); + } + + [HttpPost] + [RequirePermission(Permission.Tools_MigrateProviders)] + [ValidateAntiForgeryToken] + public async Task PostAsync(MigrateProvidersRequestModel request) + { + var providerIds = GetProviderIdsFromInput(request.ProviderIds); + + if (providerIds.Count == 0) + { + return RedirectToAction("Index"); + } + + foreach (var providerId in providerIds) + { + await providerMigrator.Migrate(providerId); + } + + return RedirectToAction("Results", new { ProviderIds = string.Join("\r\n", providerIds) }); + } + + [HttpGet("results")] + [RequirePermission(Permission.Tools_MigrateProviders)] + public async Task ResultsAsync(MigrateProvidersRequestModel request) + { + var providerIds = GetProviderIdsFromInput(request.ProviderIds); + + if (providerIds.Count == 0) + { + return View(Array.Empty()); + } + + var results = await Task.WhenAll(providerIds.Select(providerMigrator.GetResult)); + + return View(results); + } + + [HttpGet("results/{providerId:guid}")] + [RequirePermission(Permission.Tools_MigrateProviders)] + public async Task DetailsAsync([FromRoute] Guid providerId) + { + var result = await providerMigrator.GetResult(providerId); + + if (result == null) + { + return RedirectToAction("Index"); + } + + return View(result); + } + + private static List GetProviderIdsFromInput(string text) => !string.IsNullOrEmpty(text) + ? text.Split( + ["\r\n", "\r", "\n"], + StringSplitOptions.TrimEntries + ) + .Select(id => new Guid(id)) + .ToList() + : []; +} diff --git a/src/Admin/Billing/Controllers/ProcessStripeEventsController.cs b/src/Admin/Billing/Controllers/ProcessStripeEventsController.cs new file mode 100644 index 0000000000..1a3f56a183 --- /dev/null +++ b/src/Admin/Billing/Controllers/ProcessStripeEventsController.cs @@ -0,0 +1,71 @@ +using System.Text.Json; +using Bit.Admin.Billing.Models.ProcessStripeEvents; +using Bit.Core.Settings; +using Bit.Core.Utilities; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace Bit.Admin.Billing.Controllers; + +[Authorize] +[Route("process-stripe-events")] +[SelfHosted(NotSelfHostedOnly = true)] +public class ProcessStripeEventsController( + IHttpClientFactory httpClientFactory, + IGlobalSettings globalSettings) : Controller +{ + [HttpGet] + public ActionResult Index() + { + return View(new EventsFormModel()); + } + + [HttpPost] + [ValidateAntiForgeryToken] + public async Task ProcessAsync([FromForm] EventsFormModel model) + { + var eventIds = model.GetEventIds(); + + const string baseEndpoint = "stripe/recovery/events"; + + var endpoint = model.Inspect ? $"{baseEndpoint}/inspect" : $"{baseEndpoint}/process"; + + var (response, failedResponseMessage) = await PostAsync(endpoint, new EventsRequestBody + { + EventIds = eventIds + }); + + if (response == null) + { + return StatusCode((int)failedResponseMessage.StatusCode, "An error occurred during your request."); + } + + response.ActionType = model.Inspect ? EventActionType.Inspect : EventActionType.Process; + + return View("Results", response); + } + + private async Task<(EventsResponseBody, HttpResponseMessage)> PostAsync( + string endpoint, + EventsRequestBody requestModel) + { + var client = httpClientFactory.CreateClient("InternalBilling"); + client.BaseAddress = new Uri(globalSettings.BaseServiceUri.InternalBilling); + + var json = JsonSerializer.Serialize(requestModel); + var requestBody = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); + + var responseMessage = await client.PostAsync(endpoint, requestBody); + + if (!responseMessage.IsSuccessStatusCode) + { + return (null, responseMessage); + } + + var responseContent = await responseMessage.Content.ReadAsStringAsync(); + + var response = JsonSerializer.Deserialize(responseContent); + + return (response, null); + } +} diff --git a/src/Admin/Billing/Models/MigrateProvidersRequestModel.cs b/src/Admin/Billing/Models/MigrateProvidersRequestModel.cs new file mode 100644 index 0000000000..fe1d88e224 --- /dev/null +++ b/src/Admin/Billing/Models/MigrateProvidersRequestModel.cs @@ -0,0 +1,10 @@ +using System.ComponentModel.DataAnnotations; + +namespace Bit.Admin.Billing.Models; + +public class MigrateProvidersRequestModel +{ + [Required] + [Display(Name = "Provider IDs")] + public string ProviderIds { get; set; } +} diff --git a/src/Admin/Billing/Models/ProcessStripeEvents/EventsFormModel.cs b/src/Admin/Billing/Models/ProcessStripeEvents/EventsFormModel.cs new file mode 100644 index 0000000000..5ead00e263 --- /dev/null +++ b/src/Admin/Billing/Models/ProcessStripeEvents/EventsFormModel.cs @@ -0,0 +1,29 @@ +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; + +namespace Bit.Admin.Billing.Models.ProcessStripeEvents; + +public class EventsFormModel : IValidatableObject +{ + [Required] + public string EventIds { get; set; } + + [Required] + [DisplayName("Inspect Only")] + public bool Inspect { get; set; } + + public List GetEventIds() => + EventIds?.Split([Environment.NewLine], StringSplitOptions.RemoveEmptyEntries) + .Select(eventId => eventId.Trim()) + .ToList() ?? []; + + public IEnumerable Validate(ValidationContext validationContext) + { + var eventIds = GetEventIds(); + + if (eventIds.Any(eventId => !eventId.StartsWith("evt_"))) + { + yield return new ValidationResult("Event Ids must start with 'evt_'."); + } + } +} diff --git a/src/Admin/Billing/Models/ProcessStripeEvents/EventsRequestBody.cs b/src/Admin/Billing/Models/ProcessStripeEvents/EventsRequestBody.cs new file mode 100644 index 0000000000..05a2444605 --- /dev/null +++ b/src/Admin/Billing/Models/ProcessStripeEvents/EventsRequestBody.cs @@ -0,0 +1,9 @@ +using System.Text.Json.Serialization; + +namespace Bit.Admin.Billing.Models.ProcessStripeEvents; + +public class EventsRequestBody +{ + [JsonPropertyName("eventIds")] + public List EventIds { get; set; } +} diff --git a/src/Admin/Billing/Models/ProcessStripeEvents/EventsResponseBody.cs b/src/Admin/Billing/Models/ProcessStripeEvents/EventsResponseBody.cs new file mode 100644 index 0000000000..84eeb35d29 --- /dev/null +++ b/src/Admin/Billing/Models/ProcessStripeEvents/EventsResponseBody.cs @@ -0,0 +1,39 @@ +using System.Text.Json.Serialization; + +namespace Bit.Admin.Billing.Models.ProcessStripeEvents; + +public class EventsResponseBody +{ + [JsonPropertyName("events")] + public List Events { get; set; } + + [JsonIgnore] + public EventActionType ActionType { get; set; } +} + +public class EventResponseBody +{ + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("url")] + public string URL { get; set; } + + [JsonPropertyName("apiVersion")] + public string APIVersion { get; set; } + + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("createdUTC")] + public DateTime CreatedUTC { get; set; } + + [JsonPropertyName("processingError")] + public string ProcessingError { get; set; } +} + +public enum EventActionType +{ + Inspect, + Process +} diff --git a/src/Admin/Billing/Views/MigrateProviders/Details.cshtml b/src/Admin/Billing/Views/MigrateProviders/Details.cshtml new file mode 100644 index 0000000000..303e6d2e45 --- /dev/null +++ b/src/Admin/Billing/Views/MigrateProviders/Details.cshtml @@ -0,0 +1,39 @@ +@using System.Text.Json +@model Bit.Core.Billing.Migration.Models.ProviderMigrationResult +@{ + ViewData["Title"] = "Results"; +} + +

Migrate Providers

+

Migration Details: @Model.ProviderName

+
+
Id
+
@Model.ProviderId
+ +
Result
+
@Model.Result
+
+

Client Organizations

+
+ + + + + + + + + + + @foreach (var clientResult in Model.Clients) + { + + + + + + + } + +
IDNameResultPrevious State
@clientResult.OrganizationId@clientResult.OrganizationName@clientResult.Result
@Html.Raw(JsonSerializer.Serialize(clientResult.PreviousState))
+
diff --git a/src/Admin/Billing/Views/MigrateProviders/Index.cshtml b/src/Admin/Billing/Views/MigrateProviders/Index.cshtml new file mode 100644 index 0000000000..f76996fe71 --- /dev/null +++ b/src/Admin/Billing/Views/MigrateProviders/Index.cshtml @@ -0,0 +1,46 @@ +@model Bit.Admin.Billing.Models.MigrateProvidersRequestModel; +@{ + ViewData["Title"] = "Migrate Providers"; +} + +

Migrate Providers

+

Bulk Consolidated Billing Migration Tool

+
+

+ This tool allows you to provide a list of IDs for Providers that you would like to migrate to Consolidated Billing. + Because of the expensive nature of the operation, you can only migrate 10 Providers at a time. +

+

+ Updates made through this tool are irreversible without manual intervention. +

+

Example Input (Please enter each Provider ID separated by a new line):

+
+
+
f513affc-2290-4336-879e-21ec3ecf3e78
+f7a5cb0d-4b74-445c-8d8c-232d1d32bbe2
+bf82d3cf-0e21-4f39-b81b-ef52b2fc6a3a
+174e82fc-70c3-448d-9fe7-00bad2a3ab00
+22a4bbbf-58e3-4e4c-a86a-a0d7caf4ff14
+
+
+
+
+
+ + +
+
+ +
+
+
+
+
+ + +
+
+ +
+
+
diff --git a/src/Admin/Billing/Views/MigrateProviders/Results.cshtml b/src/Admin/Billing/Views/MigrateProviders/Results.cshtml new file mode 100644 index 0000000000..45611de80e --- /dev/null +++ b/src/Admin/Billing/Views/MigrateProviders/Results.cshtml @@ -0,0 +1,28 @@ +@model Bit.Core.Billing.Migration.Models.ProviderMigrationResult[] +@{ + ViewData["Title"] = "Results"; +} + +

Migrate Providers

+

Results

+
+ + + + + + + + + + @foreach (var result in Model) + { + + + + + + } + +
IDNameResult
@result.ProviderId@result.ProviderName@result.Result
+
diff --git a/src/Admin/Billing/Views/ProcessStripeEvents/Index.cshtml b/src/Admin/Billing/Views/ProcessStripeEvents/Index.cshtml new file mode 100644 index 0000000000..a8f5454d8e --- /dev/null +++ b/src/Admin/Billing/Views/ProcessStripeEvents/Index.cshtml @@ -0,0 +1,25 @@ +@model Bit.Admin.Billing.Models.ProcessStripeEvents.EventsFormModel + +@{ + ViewData["Title"] = "Process Stripe Events"; +} + +

Process Stripe Events

+
+
+
+
+ +
+
+
+
+ + +
+
+
+
+ +
+
diff --git a/src/Admin/Billing/Views/ProcessStripeEvents/Results.cshtml b/src/Admin/Billing/Views/ProcessStripeEvents/Results.cshtml new file mode 100644 index 0000000000..2293f4833f --- /dev/null +++ b/src/Admin/Billing/Views/ProcessStripeEvents/Results.cshtml @@ -0,0 +1,49 @@ +@using Bit.Admin.Billing.Models.ProcessStripeEvents +@model Bit.Admin.Billing.Models.ProcessStripeEvents.EventsResponseBody + +@{ + var title = Model.ActionType == EventActionType.Inspect ? "Inspect Stripe Events" : "Process Stripe Events"; + ViewData["Title"] = title; +} + +

@title

+

Results

+ +
+ @if (!Model.Events.Any()) + { +

No data found.

+ } + else + { + + + + + + + + @if (Model.ActionType == EventActionType.Process) + { + + } + + + + @foreach (var eventResponseBody in Model.Events) + { + + + + + + @if (Model.ActionType == EventActionType.Process) + { + + } + + } + +
IDTypeAPI VersionCreatedProcessing Error
@eventResponseBody.Id@eventResponseBody.Type@eventResponseBody.APIVersion@eventResponseBody.CreatedUTC@eventResponseBody.ProcessingError
+ } +
diff --git a/src/Admin/Billing/Views/_ViewImports.cshtml b/src/Admin/Billing/Views/_ViewImports.cshtml new file mode 100644 index 0000000000..02423ba0e7 --- /dev/null +++ b/src/Admin/Billing/Views/_ViewImports.cshtml @@ -0,0 +1,5 @@ +@using Microsoft.AspNetCore.Identity +@using Bit.Admin.AdminConsole +@using Bit.Admin.AdminConsole.Models +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@addTagHelper "*, Admin" diff --git a/src/Admin/Billing/Views/_ViewStart.cshtml b/src/Admin/Billing/Views/_ViewStart.cshtml new file mode 100644 index 0000000000..820a2f6e02 --- /dev/null +++ b/src/Admin/Billing/Views/_ViewStart.cshtml @@ -0,0 +1,3 @@ +@{ + Layout = "_Layout"; +} diff --git a/src/Admin/Controllers/UsersController.cs b/src/Admin/Controllers/UsersController.cs index e233b61e42..842abaea67 100644 --- a/src/Admin/Controllers/UsersController.cs +++ b/src/Admin/Controllers/UsersController.cs @@ -1,11 +1,11 @@ -using Bit.Admin.Enums; +#nullable enable + +using Bit.Admin.Enums; using Bit.Admin.Models; using Bit.Admin.Services; using Bit.Admin.Utilities; using Bit.Core; using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces; -using Bit.Core.Context; -using Bit.Core.Entities; using Bit.Core.Repositories; using Bit.Core.Services; using Bit.Core.Settings; @@ -24,9 +24,9 @@ public class UsersController : Controller private readonly IPaymentService _paymentService; private readonly GlobalSettings _globalSettings; private readonly IAccessControlService _accessControlService; - private readonly ICurrentContext _currentContext; - private readonly IFeatureService _featureService; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; + private readonly IFeatureService _featureService; + private readonly IUserService _userService; public UsersController( IUserRepository userRepository, @@ -34,18 +34,18 @@ public class UsersController : Controller IPaymentService paymentService, GlobalSettings globalSettings, IAccessControlService accessControlService, - ICurrentContext currentContext, + ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, IFeatureService featureService, - ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery) + IUserService userService) { _userRepository = userRepository; _cipherRepository = cipherRepository; _paymentService = paymentService; _globalSettings = globalSettings; _accessControlService = accessControlService; - _currentContext = currentContext; - _featureService = featureService; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; + _featureService = featureService; + _userService = userService; } [RequirePermission(Permission.User_List_View)] @@ -64,19 +64,26 @@ public class UsersController : Controller var skip = (page - 1) * count; var users = await _userRepository.SearchAsync(email, skip, count); + var userModels = new List(); + if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization)) { - var user2Fa = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(users.Select(u => u.Id))).ToList(); - // TempDataSerializer is having an issue serializing an empty IEnumerable>, do not set if empty. - if (user2Fa.Count != 0) + var twoFactorAuthLookup = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(users.Select(u => u.Id))).ToList(); + + userModels = UserViewModel.MapViewModels(users, twoFactorAuthLookup).ToList(); + } + else + { + foreach (var user in users) { - TempData["UsersTwoFactorIsEnabled"] = user2Fa; + var isTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user); + userModels.Add(UserViewModel.MapViewModel(user, isTwoFactorEnabled)); } } return View(new UsersModel { - Items = users as List, + Items = userModels, Email = string.IsNullOrWhiteSpace(email) ? null : email, Page = page, Count = count, @@ -87,13 +94,17 @@ public class UsersController : Controller public async Task View(Guid id) { var user = await _userRepository.GetByIdAsync(id); + if (user == null) { return RedirectToAction("Index"); } var ciphers = await _cipherRepository.GetManyByUserIdAsync(id); - return View(new UserViewModel(user, ciphers)); + + var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); + + return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers)); } [SelfHosted(NotSelfHostedOnly = true)] @@ -108,7 +119,8 @@ public class UsersController : Controller var ciphers = await _cipherRepository.GetManyByUserIdAsync(id); var billingInfo = await _paymentService.GetBillingAsync(user); var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user); - return View(new UserEditModel(user, ciphers, billingInfo, billingHistoryInfo, _globalSettings)); + var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user); + return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings)); } [HttpPost] diff --git a/src/Admin/Enums/Permissions.cs b/src/Admin/Enums/Permissions.cs index 6b73ba4205..274db11cb4 100644 --- a/src/Admin/Enums/Permissions.cs +++ b/src/Admin/Enums/Permissions.cs @@ -47,5 +47,7 @@ public enum Permission Tools_GenerateLicenseFile, Tools_ManageTaxRates, Tools_ManageStripeSubscriptions, - Tools_CreateEditTransaction + Tools_CreateEditTransaction, + Tools_ProcessStripeEvents, + Tools_MigrateProviders } diff --git a/src/Admin/Models/UserEditModel.cs b/src/Admin/Models/UserEditModel.cs index f739af1995..52cdb4c80c 100644 --- a/src/Admin/Models/UserEditModel.cs +++ b/src/Admin/Models/UserEditModel.cs @@ -7,18 +7,23 @@ using Bit.Core.Vault.Entities; namespace Bit.Admin.Models; -public class UserEditModel : UserViewModel +public class UserEditModel { - public UserEditModel() { } + public UserEditModel() + { + + } public UserEditModel( User user, + bool isTwoFactorEnabled, IEnumerable ciphers, BillingInfo billingInfo, BillingHistoryInfo billingHistoryInfo, GlobalSettings globalSettings) - : base(user, ciphers) { + User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers); + BillingInfo = billingInfo; BillingHistoryInfo = billingHistoryInfo; BraintreeMerchantId = globalSettings.Braintree.MerchantId; @@ -35,32 +40,32 @@ public class UserEditModel : UserViewModel PremiumExpirationDate = user.PremiumExpirationDate; } - public BillingInfo BillingInfo { get; set; } - public BillingHistoryInfo BillingHistoryInfo { get; set; } + public UserViewModel User { get; init; } + public BillingInfo BillingInfo { get; init; } + public BillingHistoryInfo BillingHistoryInfo { get; init; } public string RandomLicenseKey => CoreHelpers.SecureRandomString(20); public string OneYearExpirationDate => DateTime.Now.AddYears(1).ToString("yyyy-MM-ddTHH:mm"); - public string BraintreeMerchantId { get; set; } + public string BraintreeMerchantId { get; init; } [Display(Name = "Name")] - public string Name { get; set; } + public string Name { get; init; } [Required] [Display(Name = "Email")] - public string Email { get; set; } + public string Email { get; init; } [Display(Name = "Email Verified")] - public bool EmailVerified { get; set; } + public bool EmailVerified { get; init; } [Display(Name = "Premium")] - public bool Premium { get; set; } + public bool Premium { get; init; } [Display(Name = "Max. Storage GB")] - public short? MaxStorageGb { get; set; } + public short? MaxStorageGb { get; init; } [Display(Name = "Gateway")] - public Core.Enums.GatewayType? Gateway { get; set; } + public Core.Enums.GatewayType? Gateway { get; init; } [Display(Name = "Gateway Customer Id")] - public string GatewayCustomerId { get; set; } + public string GatewayCustomerId { get; init; } [Display(Name = "Gateway Subscription Id")] - public string GatewaySubscriptionId { get; set; } + public string GatewaySubscriptionId { get; init; } [Display(Name = "License Key")] - public string LicenseKey { get; set; } + public string LicenseKey { get; init; } [Display(Name = "Premium Expiration Date")] - public DateTime? PremiumExpirationDate { get; set; } - + public DateTime? PremiumExpirationDate { get; init; } } diff --git a/src/Admin/Models/UserViewModel.cs b/src/Admin/Models/UserViewModel.cs index 05160f2e00..09b3d5577c 100644 --- a/src/Admin/Models/UserViewModel.cs +++ b/src/Admin/Models/UserViewModel.cs @@ -1,18 +1,131 @@ using Bit.Core.Entities; +using Bit.Core.Enums; using Bit.Core.Vault.Entities; namespace Bit.Admin.Models; public class UserViewModel { - public UserViewModel() { } + public Guid Id { get; } + public string Name { get; } + public string Email { get; } + public DateTime CreationDate { get; } + public DateTime? PremiumExpirationDate { get; } + public bool Premium { get; } + public short? MaxStorageGb { get; } + public bool EmailVerified { get; } + public bool TwoFactorEnabled { get; } + public DateTime AccountRevisionDate { get; } + public DateTime RevisionDate { get; } + public DateTime? LastEmailChangeDate { get; } + public DateTime? LastKdfChangeDate { get; } + public DateTime? LastKeyRotationDate { get; } + public DateTime? LastPasswordChangeDate { get; } + public GatewayType? Gateway { get; } + public string GatewayCustomerId { get; } + public string GatewaySubscriptionId { get; } + public string LicenseKey { get; } + public int CipherCount { get; set; } - public UserViewModel(User user, IEnumerable ciphers) + public UserViewModel(Guid id, + string name, + string email, + DateTime creationDate, + DateTime? premiumExpirationDate, + bool premium, + short? maxStorageGb, + bool emailVerified, + bool twoFactorEnabled, + DateTime accountRevisionDate, + DateTime revisionDate, + DateTime? lastEmailChangeDate, + DateTime? lastKdfChangeDate, + DateTime? lastKeyRotationDate, + DateTime? lastPasswordChangeDate, + GatewayType? gateway, + string gatewayCustomerId, + string gatewaySubscriptionId, + string licenseKey, + IEnumerable ciphers) { - User = user; + Id = id; + Name = name; + Email = email; + CreationDate = creationDate; + PremiumExpirationDate = premiumExpirationDate; + Premium = premium; + MaxStorageGb = maxStorageGb; + EmailVerified = emailVerified; + TwoFactorEnabled = twoFactorEnabled; + AccountRevisionDate = accountRevisionDate; + RevisionDate = revisionDate; + LastEmailChangeDate = lastEmailChangeDate; + LastKdfChangeDate = lastKdfChangeDate; + LastKeyRotationDate = lastKeyRotationDate; + LastPasswordChangeDate = lastPasswordChangeDate; + Gateway = gateway; + GatewayCustomerId = gatewayCustomerId; + GatewaySubscriptionId = gatewaySubscriptionId; + LicenseKey = licenseKey; CipherCount = ciphers.Count(); } - public User User { get; set; } - public int CipherCount { get; set; } + public static IEnumerable MapViewModels( + IEnumerable users, + IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup) => + users.Select(user => MapViewModel(user, lookup)); + + public static UserViewModel MapViewModel(User user, + IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup) => + new( + user.Id, + user.Name, + user.Email, + user.CreationDate, + user.PremiumExpirationDate, + user.Premium, + user.MaxStorageGb, + user.EmailVerified, + IsTwoFactorEnabled(user, lookup), + user.AccountRevisionDate, + user.RevisionDate, + user.LastEmailChangeDate, + user.LastKdfChangeDate, + user.LastKeyRotationDate, + user.LastPasswordChangeDate, + user.Gateway, + user.GatewayCustomerId ?? string.Empty, + user.GatewaySubscriptionId ?? string.Empty, + user.LicenseKey ?? string.Empty, + Array.Empty()); + + public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled) => + MapViewModel(user, isTwoFactorEnabled, Array.Empty()); + + public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable ciphers) => + new( + user.Id, + user.Name, + user.Email, + user.CreationDate, + user.PremiumExpirationDate, + user.Premium, + user.MaxStorageGb, + user.EmailVerified, + isTwoFactorEnabled, + user.AccountRevisionDate, + user.RevisionDate, + user.LastEmailChangeDate, + user.LastKdfChangeDate, + user.LastKeyRotationDate, + user.LastPasswordChangeDate, + user.Gateway, + user.GatewayCustomerId ?? string.Empty, + user.GatewaySubscriptionId ?? string.Empty, + user.LicenseKey ?? string.Empty, + ciphers); + + public static bool IsTwoFactorEnabled(User user, + IEnumerable<(Guid userId, bool twoFactorIsEnabled)> twoFactorIsEnabledLookup) => + twoFactorIsEnabledLookup.FirstOrDefault(x => x.userId == user.Id).twoFactorIsEnabled; } diff --git a/src/Admin/Models/UsersModel.cs b/src/Admin/Models/UsersModel.cs index 0a54e318db..33148301b2 100644 --- a/src/Admin/Models/UsersModel.cs +++ b/src/Admin/Models/UsersModel.cs @@ -1,8 +1,6 @@ -using Bit.Core.Entities; +namespace Bit.Admin.Models; -namespace Bit.Admin.Models; - -public class UsersModel : PagedModel +public class UsersModel : PagedModel { public string Email { get; set; } public string Action { get; set; } diff --git a/src/Admin/Startup.cs b/src/Admin/Startup.cs index 788908d42a..11f9e7ce68 100644 --- a/src/Admin/Startup.cs +++ b/src/Admin/Startup.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.Extensions.DependencyInjection.Extensions; using Bit.Admin.Services; using Bit.Core.Billing.Extensions; +using Bit.Core.Billing.Migration; #if !OSS using Bit.Commercial.Core.Utilities; @@ -88,7 +89,10 @@ public class Startup services.AddBaseServices(globalSettings); services.AddDefaultServices(globalSettings); services.AddScoped(); + services.AddDistributedCache(globalSettings); services.AddBillingOperations(); + services.AddHttpClient(); + services.AddProviderMigration(); #if OSS services.AddOosServices(); @@ -108,6 +112,7 @@ public class Startup { o.ViewLocationFormats.Add("/Auth/Views/{1}/{0}.cshtml"); o.ViewLocationFormats.Add("/AdminConsole/Views/{1}/{0}.cshtml"); + o.ViewLocationFormats.Add("/Billing/Views/{1}/{0}.cshtml"); }); // Jobs service diff --git a/src/Admin/Utilities/RolePermissionMapping.cs b/src/Admin/Utilities/RolePermissionMapping.cs index 0ca37f7139..e260c264f4 100644 --- a/src/Admin/Utilities/RolePermissionMapping.cs +++ b/src/Admin/Utilities/RolePermissionMapping.cs @@ -161,7 +161,9 @@ public static class RolePermissionMapping Permission.Tools_GenerateLicenseFile, Permission.Tools_ManageTaxRates, Permission.Tools_ManageStripeSubscriptions, - Permission.Tools_CreateEditTransaction + Permission.Tools_CreateEditTransaction, + Permission.Tools_ProcessStripeEvents, + Permission.Tools_MigrateProviders } }, { "sales", new List diff --git a/src/Admin/Views/Shared/_Layout.cshtml b/src/Admin/Views/Shared/_Layout.cshtml index 7b204f48f8..485c09b7f1 100644 --- a/src/Admin/Views/Shared/_Layout.cshtml +++ b/src/Admin/Views/Shared/_Layout.cshtml @@ -14,6 +14,8 @@ var canGenerateLicense = AccessControlService.UserHasPermission(Permission.Tools_GenerateLicenseFile); var canManageTaxRates = AccessControlService.UserHasPermission(Permission.Tools_ManageTaxRates); var canManageStripeSubscriptions = AccessControlService.UserHasPermission(Permission.Tools_ManageStripeSubscriptions); + var canProcessStripeEvents = AccessControlService.UserHasPermission(Permission.Tools_ProcessStripeEvents); + var canMigrateProviders = AccessControlService.UserHasPermission(Permission.Tools_MigrateProviders); var canViewTools = canChargeBraintree || canCreateTransaction || canPromoteAdmin || canGenerateLicense || canManageTaxRates || canManageStripeSubscriptions; @@ -107,6 +109,18 @@ Manage Stripe Subscriptions } + @if (canProcessStripeEvents) + { + + Process Stripe Events + + } + @if (canMigrateProviders) + { + + Migrate Providers + + } } diff --git a/src/Admin/Views/Users/Edit.cshtml b/src/Admin/Views/Users/Edit.cshtml index 2bc326d227..8f07b12a7a 100644 --- a/src/Admin/Views/Users/Edit.cshtml +++ b/src/Admin/Views/Users/Edit.cshtml @@ -86,7 +86,7 @@ @if (canViewUserInformation) {

User Information

- @await Html.PartialAsync("_ViewInformation", Model) + @await Html.PartialAsync("_ViewInformation", Model.User) } @if (canViewBillingInformation) { diff --git a/src/Admin/Views/Users/Index.cshtml b/src/Admin/Views/Users/Index.cshtml index 46419503fa..a53580350e 100644 --- a/src/Admin/Views/Users/Index.cshtml +++ b/src/Admin/Views/Users/Index.cshtml @@ -1,6 +1,4 @@ @model UsersModel -@inject Bit.Core.Services.IUserService userService -@inject Bit.Core.Services.IFeatureService featureService @{ ViewData["Title"] = "Users"; } @@ -16,100 +14,88 @@
- - - - - + + + + + - @if(!Model.Items.Any()) + @if (!Model.Items.Any()) + { + + + + } + else + { + @foreach (var user in Model.Items) { - + + + } - else - { - @foreach(var user in Model.Items) - { - - - - - - } - } + }
EmailCreatedDetails
EmailCreatedDetails
No results to list.
No results to list. + @user.Email + + + @user.CreationDate.ToShortDateString() + + + @if (user.Premium) + { + + + } + else + { + + } + @if (user.MaxStorageGb.HasValue && user.MaxStorageGb > 1) + { + + + } + else + { + + + } + @if (user.EmailVerified) + { + + } + else + { + + } + @if (user.TwoFactorEnabled) + { + + } + else + { + + } +
- @user.Email - - - @user.CreationDate.ToShortDateString() - - - @if(user.Premium) - { - - } - else - { - - } - @if(user.MaxStorageGb.HasValue && user.MaxStorageGb > 1) - { - - } - else - { - - } - @if(user.EmailVerified) - { - - } - else - { - - } - @if (featureService.IsEnabled(Bit.Core.FeatureFlagKeys.MembersTwoFAQueryOptimization)) - { - var usersTwoFactorIsEnabled = TempData["UsersTwoFactorIsEnabled"] as IEnumerable<(Guid userId, bool twoFactorIsEnabled)>; - var matchingUser2Fa = usersTwoFactorIsEnabled?.FirstOrDefault(tuple => tuple.userId == user.Id); - - @if(matchingUser2Fa is { twoFactorIsEnabled: true }) - { - - } - else - { - - } - } - else - { - @if(await userService.TwoFactorIsEnabledAsync(user)) - { - - } - else - { - - } - } -