diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 7945c3e7e..1b76bccf2 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 fc8a7b76e..c54e3abb2 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 a7bf370cc..fe4063f44 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 abd7c4bb4..3b3c2d55d 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 90523ba17..101e5730d 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 1fc4c511b..000000000 --- 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 9e2e03d67..3bbc7e74f 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 331f996c0..3c45f84b7 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 7e0e8ae26..c63302cbc 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 b4335ee49..0f4d060ba 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 ef02f8b70..325f10b94 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 1b4739df1..216130a21 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 0fb8c4a22..e1d96ee4d 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 71ea27df4..efcc8dbde 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 d9cfc0d86..2503380a0 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 51250250f..1bea930f1 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 7ece41eca..a84813fd7 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 cd30e841b..5493e65af 100644 --- a/src/Admin/Admin.csproj +++ b/src/Admin/Admin.csproj @@ -14,6 +14,10 @@ + + + + diff --git a/src/Admin/AdminConsole/Views/Providers/Edit.cshtml b/src/Admin/AdminConsole/Views/Providers/Edit.cshtml index f4cf36f92..37cda8417 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/Billing/Controllers/ProcessStripeEventsController.cs b/src/Admin/Billing/Controllers/ProcessStripeEventsController.cs new file mode 100644 index 000000000..1a3f56a18 --- /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/ProcessStripeEvents/EventsFormModel.cs b/src/Admin/Billing/Models/ProcessStripeEvents/EventsFormModel.cs new file mode 100644 index 000000000..5ead00e26 --- /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 000000000..05a244460 --- /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 000000000..84eeb35d2 --- /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/ProcessStripeEvents/Index.cshtml b/src/Admin/Billing/Views/ProcessStripeEvents/Index.cshtml new file mode 100644 index 000000000..a8f5454d8 --- /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 000000000..2293f4833 --- /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 000000000..02423ba0e --- /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 000000000..820a2f6e0 --- /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 e233b61e4..842abaea6 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 6b73ba420..a8168b9e1 100644 --- a/src/Admin/Enums/Permissions.cs +++ b/src/Admin/Enums/Permissions.cs @@ -47,5 +47,6 @@ public enum Permission Tools_GenerateLicenseFile, Tools_ManageTaxRates, Tools_ManageStripeSubscriptions, - Tools_CreateEditTransaction + Tools_CreateEditTransaction, + Tools_ProcessStripeEvents } diff --git a/src/Admin/Models/UserEditModel.cs b/src/Admin/Models/UserEditModel.cs index f739af199..52cdb4c80 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 05160f2e0..09b3d5577 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 0a54e318d..33148301b 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 788908d42..f25e5072d 100644 --- a/src/Admin/Startup.cs +++ b/src/Admin/Startup.cs @@ -89,6 +89,7 @@ public class Startup services.AddDefaultServices(globalSettings); services.AddScoped(); services.AddBillingOperations(); + services.AddHttpClient(); #if OSS services.AddOosServices(); @@ -108,6 +109,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 0ca37f713..cb4a0fe47 100644 --- a/src/Admin/Utilities/RolePermissionMapping.cs +++ b/src/Admin/Utilities/RolePermissionMapping.cs @@ -161,7 +161,8 @@ public static class RolePermissionMapping Permission.Tools_GenerateLicenseFile, Permission.Tools_ManageTaxRates, Permission.Tools_ManageStripeSubscriptions, - Permission.Tools_CreateEditTransaction + Permission.Tools_CreateEditTransaction, + Permission.Tools_ProcessStripeEvents, } }, { "sales", new List diff --git a/src/Admin/Views/Shared/_Layout.cshtml b/src/Admin/Views/Shared/_Layout.cshtml index 7b204f48f..d3bfc6313 100644 --- a/src/Admin/Views/Shared/_Layout.cshtml +++ b/src/Admin/Views/Shared/_Layout.cshtml @@ -14,6 +14,7 @@ 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 canViewTools = canChargeBraintree || canCreateTransaction || canPromoteAdmin || canGenerateLicense || canManageTaxRates || canManageStripeSubscriptions; @@ -107,6 +108,12 @@ Manage Stripe Subscriptions } + @if (canProcessStripeEvents) + { + + Process Stripe Events + + } } diff --git a/src/Admin/Views/Users/Edit.cshtml b/src/Admin/Views/Users/Edit.cshtml index 2bc326d22..8f07b12a7 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 46419503f..a53580350 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 - { - - } - } -