mirror of
https://github.com/bitwarden/server
synced 2026-01-13 22:13:43 +00:00
Merge branch 'main' into tools/pm-21918/send-authentication-commands
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"swashbuckle.aspnetcore.cli": {
|
||||
"version": "9.0.2",
|
||||
"version": "9.0.4",
|
||||
"commands": ["swagger"]
|
||||
},
|
||||
"dotnet-ef": {
|
||||
|
||||
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@@ -92,6 +92,8 @@ src/Admin/Views/Tools @bitwarden/team-billing-dev
|
||||
**/.dockerignore @bitwarden/team-platform-dev
|
||||
**/Dockerfile @bitwarden/team-platform-dev
|
||||
**/entrypoint.sh @bitwarden/team-platform-dev
|
||||
# The PushType enum is expected to be editted by anyone without need for Platform review
|
||||
src/Core/Platform/Push/PushType.cs
|
||||
|
||||
# Multiple owners - DO NOT REMOVE (BRE)
|
||||
**/packages.lock.json
|
||||
|
||||
112
.github/workflows/load-test.yml
vendored
Normal file
112
.github/workflows/load-test.yml
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
name: Load test
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * 1" # Run every Monday at 00:00
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test-id:
|
||||
type: string
|
||||
description: "Identifier label for Datadog metrics"
|
||||
default: "server-load-test"
|
||||
k6-test-path:
|
||||
type: string
|
||||
description: "Path to load test files"
|
||||
default: "perf/load/*.js"
|
||||
k6-flags:
|
||||
type: string
|
||||
description: "Additional k6 flags"
|
||||
api-env-url:
|
||||
type: string
|
||||
description: "URL of the API environment"
|
||||
default: "https://api.qa.bitwarden.pw"
|
||||
identity-env-url:
|
||||
type: string
|
||||
description: "URL of the Identity environment"
|
||||
default: "https://identity.qa.bitwarden.pw"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
env:
|
||||
# Secret configuration
|
||||
AZURE_KEY_VAULT_NAME: gh-server
|
||||
AZURE_KEY_VAULT_SECRETS: DD-API-KEY, K6-CLIENT-ID, K6-AUTH-USER-EMAIL, K6-AUTH-USER-PASSWORD-HASH
|
||||
# Specify defaults for scheduled runs
|
||||
TEST_ID: ${{ inputs.test-id || 'server-load-test' }}
|
||||
K6_TEST_PATH: ${{ inputs.k6-test-path || 'perf/load/*.js' }}
|
||||
API_ENV_URL: ${{ inputs.api-env-url || 'https://api.qa.bitwarden.pw' }}
|
||||
IDENTITY_ENV_URL: ${{ inputs.identity-env-url || 'https://identity.qa.bitwarden.pw' }}
|
||||
|
||||
jobs:
|
||||
run-tests:
|
||||
name: Run load tests
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- name: Log in to Azure
|
||||
uses: bitwarden/gh-actions/azure-login@main
|
||||
with:
|
||||
subscription_id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
|
||||
tenant_id: ${{ secrets.AZURE_TENANT_ID }}
|
||||
client_id: ${{ secrets.AZURE_CLIENT_ID }}
|
||||
|
||||
- name: Get Azure Key Vault secrets
|
||||
id: get-kv-secrets
|
||||
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||
with:
|
||||
keyvault: ${{ env.AZURE_KEY_VAULT_NAME }}
|
||||
secrets: ${{ env.AZURE_KEY_VAULT_SECRETS }}
|
||||
|
||||
- name: Log out of Azure
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
# Datadog agent for collecting OTEL metrics from k6
|
||||
- name: Start Datadog agent
|
||||
run: |
|
||||
docker run --detach \
|
||||
--name datadog-agent \
|
||||
-p 4317:4317 \
|
||||
-p 5555:5555 \
|
||||
-e DD_SITE=us3.datadoghq.com \
|
||||
-e DD_API_KEY=${{ steps.get-kv-secrets.outputs.DD-API-KEY }} \
|
||||
-e DD_DOGSTATSD_NON_LOCAL_TRAFFIC=1 \
|
||||
-e DD_OTLP_CONFIG_RECEIVER_PROTOCOLS_GRPC_ENDPOINT=0.0.0.0:4317 \
|
||||
-e DD_HEALTH_PORT=5555 \
|
||||
-e HOST_PROC=/proc \
|
||||
--volume /var/run/docker.sock:/var/run/docker.sock:ro \
|
||||
--volume /sys/fs/cgroup/:/host/sys/fs/cgroup:ro \
|
||||
--health-cmd "curl -f http://localhost:5555/health || exit 1" \
|
||||
--health-interval 10s \
|
||||
--health-timeout 5s \
|
||||
--health-retries 10 \
|
||||
--health-start-period 30s \
|
||||
--pid host \
|
||||
datadog/agent:7-full@sha256:7ea933dec3b8baa8c19683b1c3f6f801dbf3291f748d9ed59234accdaac4e479
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Set up k6
|
||||
uses: grafana/setup-k6-action@ffe7d7290dfa715e48c2ccc924d068444c94bde2 # v1.1.0
|
||||
|
||||
- name: Run k6 tests
|
||||
uses: grafana/run-k6-action@c6b79182b9b666aa4f630f4a6be9158ead62536e # v1.2.0
|
||||
continue-on-error: false
|
||||
env:
|
||||
K6_OTEL_METRIC_PREFIX: k6_
|
||||
K6_OTEL_GRPC_EXPORTER_INSECURE: true
|
||||
# Load test specific environment variables
|
||||
API_URL: ${{ env.API_ENV_URL }}
|
||||
IDENTITY_URL: ${{ env.IDENTITY_ENV_URL }}
|
||||
CLIENT_ID: ${{ steps.get-kv-secrets.outputs.K6-CLIENT-ID }}
|
||||
AUTH_USER_EMAIL: ${{ steps.get-kv-secrets.outputs.K6-AUTH-USER-EMAIL }}
|
||||
AUTH_USER_PASSWORD_HASH: ${{ steps.get-kv-secrets.outputs.K6-AUTH-USER-PASSWORD-HASH }}
|
||||
with:
|
||||
flags: >-
|
||||
--tag test-id=${{ env.TEST_ID }}
|
||||
-o experimental-opentelemetry
|
||||
${{ inputs.k6-flags }}
|
||||
path: ${{ env.K6_TEST_PATH }}
|
||||
4
.github/workflows/repository-management.yml
vendored
4
.github/workflows/repository-management.yml
vendored
@@ -82,7 +82,7 @@ jobs:
|
||||
version: ${{ inputs.version_number_override }}
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0
|
||||
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
@@ -200,7 +200,7 @@ jobs:
|
||||
uses: bitwarden/gh-actions/azure-logout@main
|
||||
|
||||
- name: Generate GH App token
|
||||
uses: actions/create-github-app-token@d72941d797fd3113feb6b93fd0dec494b13a2547 # v1.12.0
|
||||
uses: actions/create-github-app-token@a8d616148505b5069dccd32f177bb87d7f39123b # v2.1.1
|
||||
id: app-token
|
||||
with:
|
||||
app-id: ${{ steps.get-kv-secrets.outputs.BW-GHAPP-ID }}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<Version>2025.8.1</Version>
|
||||
<Version>2025.9.1</Version>
|
||||
|
||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System.Globalization;
|
||||
using Bit.Commercial.Core.Billing.Providers.Models;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
@@ -282,7 +283,7 @@ public class ProviderBillingService(
|
||||
]
|
||||
};
|
||||
|
||||
if (providerCustomer.Address is not { Country: "US" })
|
||||
if (providerCustomer.Address is not { Country: Constants.CountryAbbreviations.UnitedStates })
|
||||
{
|
||||
customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse;
|
||||
}
|
||||
@@ -525,7 +526,7 @@ public class ProviderBillingService(
|
||||
}
|
||||
};
|
||||
|
||||
if (taxInfo.BillingAddressCountry is not "US")
|
||||
if (taxInfo.BillingAddressCountry is not Constants.CountryAbbreviations.UnitedStates)
|
||||
{
|
||||
options.TaxExempt = StripeConstants.TaxExempt.Reverse;
|
||||
}
|
||||
@@ -635,10 +636,10 @@ public class ProviderBillingService(
|
||||
{
|
||||
case PaymentMethodType.BankAccount:
|
||||
{
|
||||
var setupIntentId = await setupIntentCache.Get(provider.Id);
|
||||
var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(provider.Id);
|
||||
await stripeAdapter.SetupIntentCancel(setupIntentId,
|
||||
new SetupIntentCancelOptions { CancellationReason = "abandoned" });
|
||||
await setupIntentCache.Remove(provider.Id);
|
||||
await setupIntentCache.RemoveSetupIntentForSubscriber(provider.Id);
|
||||
break;
|
||||
}
|
||||
case PaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):
|
||||
@@ -688,7 +689,7 @@ public class ProviderBillingService(
|
||||
});
|
||||
}
|
||||
|
||||
var setupIntentId = await setupIntentCache.Get(provider.Id);
|
||||
var setupIntentId = await setupIntentCache.GetSetupIntentIdForSubscriber(provider.Id);
|
||||
|
||||
var setupIntent = !string.IsNullOrEmpty(setupIntentId)
|
||||
? await stripeAdapter.SetupIntentGet(setupIntentId, new SetupIntentGetOptions
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
|
||||
@@ -108,36 +108,32 @@ public class AccountController : Controller
|
||||
// Validate domain_hint provided
|
||||
if (string.IsNullOrWhiteSpace(domainHint))
|
||||
{
|
||||
return InvalidJson("NoOrganizationIdentifierProvidedError");
|
||||
_logger.LogError(new ArgumentException("domainHint is required."), "domainHint not specified.");
|
||||
return InvalidJson("SsoInvalidIdentifierError");
|
||||
}
|
||||
|
||||
// Validate organization exists from domain_hint
|
||||
var organization = await _organizationRepository.GetByIdentifierAsync(domainHint);
|
||||
if (organization == null)
|
||||
if (organization is not { UseSso: true })
|
||||
{
|
||||
return InvalidJson("OrganizationNotFoundByIdentifierError");
|
||||
}
|
||||
if (!organization.UseSso)
|
||||
{
|
||||
return InvalidJson("SsoNotAllowedForOrganizationError");
|
||||
_logger.LogError("Organization not configured to use SSO.");
|
||||
return InvalidJson("SsoInvalidIdentifierError");
|
||||
}
|
||||
|
||||
// Validate SsoConfig exists and is Enabled
|
||||
var ssoConfig = await _ssoConfigRepository.GetByIdentifierAsync(domainHint);
|
||||
if (ssoConfig == null)
|
||||
if (ssoConfig is not { Enabled: true })
|
||||
{
|
||||
return InvalidJson("SsoConfigurationNotFoundForOrganizationError");
|
||||
}
|
||||
if (!ssoConfig.Enabled)
|
||||
{
|
||||
return InvalidJson("SsoNotEnabledForOrganizationError");
|
||||
_logger.LogError("SsoConfig not enabled.");
|
||||
return InvalidJson("SsoInvalidIdentifierError");
|
||||
}
|
||||
|
||||
// Validate Authentication Scheme exists and is loaded (cache)
|
||||
var scheme = await _schemeProvider.GetSchemeAsync(organization.Id.ToString());
|
||||
if (scheme == null || !(scheme is IDynamicAuthenticationScheme dynamicScheme))
|
||||
if (scheme is not IDynamicAuthenticationScheme dynamicScheme)
|
||||
{
|
||||
return InvalidJson("NoSchemeOrHandlerForSsoConfigurationFoundError");
|
||||
_logger.LogError("Invalid authentication scheme for organization.");
|
||||
return InvalidJson("SsoInvalidIdentifierError");
|
||||
}
|
||||
|
||||
// Run scheme validation
|
||||
@@ -147,13 +143,8 @@ public class AccountController : Controller
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var translatedException = _i18nService.GetLocalizedHtmlString(ex.Message);
|
||||
var errorKey = "InvalidSchemeConfigurationError";
|
||||
if (!translatedException.ResourceNotFound)
|
||||
{
|
||||
errorKey = ex.Message;
|
||||
}
|
||||
return InvalidJson(errorKey, translatedException.ResourceNotFound ? ex : null);
|
||||
_logger.LogError(ex, "An error occurred while validating SSO dynamic scheme.");
|
||||
return InvalidJson("SsoInvalidIdentifierError");
|
||||
}
|
||||
|
||||
var tokenable = new SsoTokenable(organization, _globalSettings.Sso.SsoTokenLifetimeInSeconds);
|
||||
@@ -163,7 +154,8 @@ public class AccountController : Controller
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return InvalidJson("PreValidationError", ex);
|
||||
_logger.LogError(ex, "An error occurred during SSO prevalidation.");
|
||||
return InvalidJson("SsoInvalidIdentifierError");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,7 +344,7 @@ public class AccountController : Controller
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to map the external identity to a Bitwarden user, through the SsoUser table, which holds the `externalId`.
|
||||
/// Attempts to map the external identity to a Bitwarden user, through the SsoUser table, which holds the `externalId`.
|
||||
/// The claims on the external identity are used to determine an `externalId`, and that is used to find the appropriate `SsoUser` and `User` records.
|
||||
/// </summary>
|
||||
private async Task<(User user, string provider, string providerUserId, IEnumerable<Claim> claims, SsoConfigurationData config)>
|
||||
@@ -485,7 +477,7 @@ public class AccountController : Controller
|
||||
allowedStatuses: [OrganizationUserStatusType.Accepted, OrganizationUserStatusType.Confirmed]);
|
||||
|
||||
|
||||
// Since we're in the auto-provisioning logic, this means that the user exists, but they have not
|
||||
// Since we're in the auto-provisioning logic, this means that the user exists, but they have not
|
||||
// authenticated with the org's SSO provider before now (otherwise we wouldn't be auto-provisioning them).
|
||||
// We've verified that the user is Accepted or Confnirmed, so we can create an SsoUser link and proceed
|
||||
// with authentication.
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Auth.Enums;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Auth.Models.Data;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
@@ -416,7 +417,7 @@ public class DynamicAuthenticationSchemeProvider : AuthenticationSchemeProvider
|
||||
SPOptions = spOptions,
|
||||
SignInScheme = AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme,
|
||||
SignOutScheme = IdentityServerConstants.DefaultCookieAuthenticationScheme,
|
||||
CookieManager = new IdentityServer.DistributedCacheCookieManager(),
|
||||
CookieManager = new DistributedCacheCookieManager(),
|
||||
};
|
||||
options.IdentityProviders.Add(idp);
|
||||
|
||||
|
||||
182
bitwarden_license/src/Sso/package-lock.json
generated
182
bitwarden_license/src/Sso/package-lock.json
generated
@@ -17,9 +17,9 @@
|
||||
"css-loader": "7.1.2",
|
||||
"expose-loader": "5.0.1",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"sass": "1.89.2",
|
||||
"sass": "1.91.0",
|
||||
"sass-loader": "16.0.5",
|
||||
"webpack": "5.99.8",
|
||||
"webpack": "5.101.3",
|
||||
"webpack-cli": "5.1.4"
|
||||
}
|
||||
},
|
||||
@@ -34,18 +34,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
||||
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.2.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
@@ -58,20 +54,10 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/set-array": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/source-map": {
|
||||
"version": "0.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
|
||||
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
|
||||
"version": "0.3.11",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
|
||||
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -80,16 +66,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.25",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||
"version": "0.3.30",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
|
||||
"integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -441,9 +427,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -455,13 +441,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.15.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
|
||||
"integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
|
||||
"version": "24.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz",
|
||||
"integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
"undici-types": "~7.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
@@ -687,9 +673,9 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -699,6 +685,19 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-import-phases": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
|
||||
"integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"acorn": "^8.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
@@ -781,9 +780,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.24.5",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
|
||||
"integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
|
||||
"version": "4.25.4",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz",
|
||||
"integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -801,8 +800,8 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001716",
|
||||
"electron-to-chromium": "^1.5.149",
|
||||
"caniuse-lite": "^1.0.30001737",
|
||||
"electron-to-chromium": "^1.5.211",
|
||||
"node-releases": "^2.0.19",
|
||||
"update-browserslist-db": "^1.1.3"
|
||||
},
|
||||
@@ -821,9 +820,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001718",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz",
|
||||
"integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==",
|
||||
"version": "1.0.30001741",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz",
|
||||
"integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -975,16 +974,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.155",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz",
|
||||
"integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==",
|
||||
"version": "1.5.215",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.215.tgz",
|
||||
"integrity": "sha512-TIvGp57UpeNetj/wV/xpFNpWGb0b/ROw372lHPx5Aafx02gjTBtWnEEcaSX3W2dLM3OSdGGyHX/cHl01JQsLaQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.18.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
|
||||
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
|
||||
"version": "5.18.3",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1107,9 +1106,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
||||
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -1241,9 +1240,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
|
||||
"integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==",
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz",
|
||||
"integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -1528,9 +1527,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.19",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
||||
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
||||
"version": "2.0.20",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.20.tgz",
|
||||
"integrity": "sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -1635,9 +1634,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
||||
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -1655,7 +1654,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.8",
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -1860,9 +1859,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.89.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz",
|
||||
"integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==",
|
||||
"version": "1.91.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.91.0.tgz",
|
||||
"integrity": "sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2061,24 +2060,28 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
|
||||
"integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz",
|
||||
"integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.39.2",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz",
|
||||
"integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==",
|
||||
"version": "5.44.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
|
||||
"integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.14.0",
|
||||
"acorn": "^8.15.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
@@ -2139,9 +2142,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"version": "7.10.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
|
||||
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -2198,22 +2201,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.99.8",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz",
|
||||
"integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==",
|
||||
"version": "5.101.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz",
|
||||
"integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@types/estree": "^1.0.8",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@webassemblyjs/ast": "^1.14.1",
|
||||
"@webassemblyjs/wasm-edit": "^1.14.1",
|
||||
"@webassemblyjs/wasm-parser": "^1.14.1",
|
||||
"acorn": "^8.14.0",
|
||||
"acorn": "^8.15.0",
|
||||
"acorn-import-phases": "^1.0.3",
|
||||
"browserslist": "^4.24.0",
|
||||
"chrome-trace-event": "^1.0.2",
|
||||
"enhanced-resolve": "^5.17.1",
|
||||
"enhanced-resolve": "^5.17.3",
|
||||
"es-module-lexer": "^1.2.1",
|
||||
"eslint-scope": "5.1.1",
|
||||
"events": "^3.2.0",
|
||||
@@ -2227,7 +2231,7 @@
|
||||
"tapable": "^2.1.1",
|
||||
"terser-webpack-plugin": "^5.3.11",
|
||||
"watchpack": "^2.4.1",
|
||||
"webpack-sources": "^3.2.3"
|
||||
"webpack-sources": "^3.3.3"
|
||||
},
|
||||
"bin": {
|
||||
"webpack": "bin/webpack.js"
|
||||
@@ -2317,9 +2321,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-sources": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
|
||||
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
|
||||
"integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
"css-loader": "7.1.2",
|
||||
"expose-loader": "5.0.1",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"sass": "1.89.2",
|
||||
"sass": "1.91.0",
|
||||
"sass-loader": "16.0.5",
|
||||
"webpack": "5.99.8",
|
||||
"webpack": "5.101.3",
|
||||
"webpack-cli": "5.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Providers.Entities;
|
||||
using Bit.Core.Billing.Providers.Repositories;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
@@ -15,6 +15,7 @@ using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Providers.Entities;
|
||||
using Bit.Core.Billing.Providers.Models;
|
||||
using Bit.Core.Billing.Providers.Repositories;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Tax.Services;
|
||||
using Bit.Core.Entities;
|
||||
@@ -1002,7 +1003,7 @@ public class ProviderBillingServiceTests
|
||||
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
|
||||
.Throws<StripeException>();
|
||||
|
||||
sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns("setup_intent_id");
|
||||
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns("setup_intent_id");
|
||||
|
||||
await Assert.ThrowsAsync<StripeException>(() =>
|
||||
sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
|
||||
@@ -1012,7 +1013,7 @@ public class ProviderBillingServiceTests
|
||||
await stripeAdapter.Received(1).SetupIntentCancel("setup_intent_id", Arg.Is<SetupIntentCancelOptions>(options =>
|
||||
options.CancellationReason == "abandoned"));
|
||||
|
||||
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Remove(provider.Id);
|
||||
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).RemoveSetupIntentForSubscriber(provider.Id);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
@@ -1643,7 +1644,7 @@ public class ProviderBillingServiceTests
|
||||
|
||||
const string setupIntentId = "seti_123";
|
||||
|
||||
sutProvider.GetDependency<ISetupIntentCache>().Get(provider.Id).Returns(setupIntentId);
|
||||
sutProvider.GetDependency<ISetupIntentCache>().GetSetupIntentIdForSubscriber(provider.Id).Returns(setupIntentId);
|
||||
|
||||
sutProvider.GetDependency<IStripeAdapter>().SetupIntentGet(setupIntentId, Arg.Is<SetupIntentGetOptions>(options =>
|
||||
options.Expand.Contains("payment_method"))).Returns(new SetupIntent
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using Bit.Commercial.Core.Billing.Providers.Services;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Providers.Services;
|
||||
using Stripe;
|
||||
using Xunit;
|
||||
|
||||
|
||||
@@ -11,9 +11,18 @@ dotnet tool restore
|
||||
Set-Location "./src/Identity"
|
||||
dotnet build
|
||||
dotnet swagger tofile --output "../../identity.json" --host "https://identity.bitwarden.com" "./bin/Debug/net8.0/Identity.dll" "v1"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
# Api internal & public
|
||||
Set-Location "../../src/Api"
|
||||
dotnet build
|
||||
dotnet swagger tofile --output "../../api.json" --host "https://api.bitwarden.com" "./bin/Debug/net8.0/Api.dll" "internal"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
dotnet swagger tofile --output "../../api.public.json" --host "https://api.bitwarden.com" "./bin/Debug/net8.0/Api.dll" "public"
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
@@ -34,6 +34,9 @@
|
||||
},
|
||||
{
|
||||
"Name": "events-hec-subscription"
|
||||
},
|
||||
{
|
||||
"Name": "events-datadog-subscription"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -81,6 +84,20 @@
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"Name": "integration-datadog-subscription",
|
||||
"Rules": [
|
||||
{
|
||||
"Name": "datadog-integration-filter",
|
||||
"Properties": {
|
||||
"FilterType": "Correlation",
|
||||
"CorrelationFilter": {
|
||||
"Label": "datadog"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -9,12 +9,6 @@ const AUTH_USERNAME = __ENV.AUTH_USER_EMAIL;
|
||||
const AUTH_PASSWORD = __ENV.AUTH_USER_PASSWORD_HASH;
|
||||
|
||||
export const options = {
|
||||
ext: {
|
||||
loadimpact: {
|
||||
projectID: 3639465,
|
||||
name: "Config",
|
||||
},
|
||||
},
|
||||
scenarios: {
|
||||
constant_load: {
|
||||
executor: "constant-arrival-rate",
|
||||
|
||||
@@ -10,12 +10,6 @@ const AUTH_CLIENT_ID = __ENV.AUTH_CLIENT_ID;
|
||||
const AUTH_CLIENT_SECRET = __ENV.AUTH_CLIENT_SECRET;
|
||||
|
||||
export const options = {
|
||||
ext: {
|
||||
loadimpact: {
|
||||
projectID: 3639465,
|
||||
name: "Groups",
|
||||
},
|
||||
},
|
||||
scenarios: {
|
||||
constant_load: {
|
||||
executor: "constant-arrival-rate",
|
||||
|
||||
@@ -6,12 +6,6 @@ const AUTH_USERNAME = __ENV.AUTH_USER_EMAIL;
|
||||
const AUTH_PASSWORD = __ENV.AUTH_USER_PASSWORD_HASH;
|
||||
|
||||
export const options = {
|
||||
ext: {
|
||||
loadimpact: {
|
||||
projectID: 3639465,
|
||||
name: "Login",
|
||||
},
|
||||
},
|
||||
scenarios: {
|
||||
constant_load: {
|
||||
executor: "constant-arrival-rate",
|
||||
|
||||
@@ -9,12 +9,6 @@ const AUTH_USERNAME = __ENV.AUTH_USER_EMAIL;
|
||||
const AUTH_PASSWORD = __ENV.AUTH_USER_PASSWORD_HASH;
|
||||
|
||||
export const options = {
|
||||
ext: {
|
||||
loadimpact: {
|
||||
projectID: 3639465,
|
||||
name: "Sync",
|
||||
},
|
||||
},
|
||||
scenarios: {
|
||||
constant_load: {
|
||||
executor: "constant-arrival-rate",
|
||||
|
||||
@@ -7,7 +7,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.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
|
||||
@@ -39,28 +38,26 @@ namespace Bit.Admin.AdminConsole.Controllers;
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public class ProvidersController : Controller
|
||||
{
|
||||
private readonly string _stripeUrl;
|
||||
private readonly string _braintreeMerchantUrl;
|
||||
private readonly string _braintreeMerchantId;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IResellerClientOrganizationSignUpCommand _resellerClientOrganizationSignUpCommand;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
private readonly IProviderUserRepository _providerUserRepository;
|
||||
private readonly IProviderOrganizationRepository _providerOrganizationRepository;
|
||||
private readonly IProviderService _providerService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly IProviderService _providerService;
|
||||
private readonly ICreateProviderCommand _createProviderCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IProviderPlanRepository _providerPlanRepository;
|
||||
private readonly IProviderBillingService _providerBillingService;
|
||||
private readonly IPricingClient _pricingClient;
|
||||
private readonly IStripeAdapter _stripeAdapter;
|
||||
private readonly IAccessControlService _accessControlService;
|
||||
private readonly ISubscriberService _subscriberService;
|
||||
private readonly string _stripeUrl;
|
||||
private readonly string _braintreeMerchantUrl;
|
||||
private readonly string _braintreeMerchantId;
|
||||
|
||||
public ProvidersController(
|
||||
IOrganizationRepository organizationRepository,
|
||||
public ProvidersController(IOrganizationRepository organizationRepository,
|
||||
IResellerClientOrganizationSignUpCommand resellerClientOrganizationSignUpCommand,
|
||||
IProviderRepository providerRepository,
|
||||
IProviderUserRepository providerUserRepository,
|
||||
@@ -69,7 +66,6 @@ public class ProvidersController : Controller
|
||||
GlobalSettings globalSettings,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
ICreateProviderCommand createProviderCommand,
|
||||
IFeatureService featureService,
|
||||
IProviderPlanRepository providerPlanRepository,
|
||||
IProviderBillingService providerBillingService,
|
||||
IWebHostEnvironment webHostEnvironment,
|
||||
@@ -87,15 +83,14 @@ public class ProvidersController : Controller
|
||||
_globalSettings = globalSettings;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_createProviderCommand = createProviderCommand;
|
||||
_featureService = featureService;
|
||||
_providerPlanRepository = providerPlanRepository;
|
||||
_providerBillingService = providerBillingService;
|
||||
_pricingClient = pricingClient;
|
||||
_stripeAdapter = stripeAdapter;
|
||||
_accessControlService = accessControlService;
|
||||
_stripeUrl = webHostEnvironment.GetStripeUrl();
|
||||
_braintreeMerchantUrl = webHostEnvironment.GetBraintreeMerchantUrl();
|
||||
_braintreeMerchantId = globalSettings.Braintree.MerchantId;
|
||||
_accessControlService = accessControlService;
|
||||
_subscriberService = subscriberService;
|
||||
}
|
||||
|
||||
@@ -344,21 +339,17 @@ public class ProvidersController : Controller
|
||||
]);
|
||||
await _providerBillingService.UpdateSeatMinimums(updateMspSeatMinimumsCommand);
|
||||
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically))
|
||||
var customer = await _stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId);
|
||||
if (model.PayByInvoice != customer.ApprovedToPayByInvoice())
|
||||
{
|
||||
var customer = await _stripeAdapter.CustomerGetAsync(provider.GatewayCustomerId);
|
||||
|
||||
if (model.PayByInvoice != customer.ApprovedToPayByInvoice())
|
||||
var approvedToPayByInvoice = model.PayByInvoice ? "1" : "0";
|
||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||
{
|
||||
var approvedToPayByInvoice = model.PayByInvoice ? "1" : "0";
|
||||
await _stripeAdapter.CustomerUpdateAsync(customer.Id, new CustomerUpdateOptions
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
[StripeConstants.MetadataKeys.InvoiceApproved] = approvedToPayByInvoice
|
||||
}
|
||||
});
|
||||
}
|
||||
[StripeConstants.MetadataKeys.InvoiceApproved] = approvedToPayByInvoice
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
case ProviderType.BusinessUnit:
|
||||
@@ -403,8 +394,7 @@ public class ProvidersController : Controller
|
||||
}
|
||||
|
||||
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
||||
var payByInvoice = _featureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically) &&
|
||||
((await _subscriberService.GetCustomer(provider))?.ApprovedToPayByInvoice() ?? false);
|
||||
var payByInvoice = ((await _subscriberService.GetCustomer(provider))?.ApprovedToPayByInvoice() ?? false);
|
||||
|
||||
return new ProviderEditModel(
|
||||
provider, users, providerOrganizations,
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
@model Bit.Core.AdminConsole.Entities.Provider.Provider
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4 col-lg-3">Provider Name</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@Model.DisplayName()</dd>
|
||||
<dd class="col-sm-8 col-lg-9">
|
||||
<a asp-controller="Providers" asp-action="Edit" asp-route-id="@Model.Id">@Model.DisplayName()</a>
|
||||
</dd>
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Provider Type</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@(Model.Type.GetDisplayAttribute()?.GetName())</dd>
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
@using Bit.Admin.Enums;
|
||||
@using Bit.Core
|
||||
@inject IAccessControlService AccessControlService
|
||||
|
||||
@using Bit.Admin.Enums
|
||||
@using Bit.Admin.Services
|
||||
@using Bit.Core.AdminConsole.Enums.Provider
|
||||
@using Bit.Core.Billing.Enums
|
||||
@using Bit.Core.Billing.Extensions
|
||||
@using Microsoft.AspNetCore.Mvc.TagHelpers
|
||||
@inject Bit.Admin.Services.IAccessControlService AccessControlService
|
||||
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||
|
||||
@using Bit.Core.Enums
|
||||
@model ProviderEditModel
|
||||
@{
|
||||
ViewData["Title"] = "Provider: " + Model.Provider.DisplayName();
|
||||
@@ -114,7 +113,7 @@
|
||||
<div class="col-sm">
|
||||
<div class="mb-3">
|
||||
<label asp-for="Gateway" class="form-label"></label>
|
||||
<select class="form-control" asp-for="Gateway" asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
||||
<select class="form-control" asp-for="Gateway" asp-items="Html.GetEnumSelectList<GatewayType>()">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -144,7 +143,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically) && Model.Provider.Type == ProviderType.Msp && Model.Provider.IsBillable())
|
||||
@if (Model.Provider.Type == ProviderType.Msp && Model.Provider.IsBillable())
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
|
||||
182
src/Admin/package-lock.json
generated
182
src/Admin/package-lock.json
generated
@@ -18,9 +18,9 @@
|
||||
"css-loader": "7.1.2",
|
||||
"expose-loader": "5.0.1",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"sass": "1.89.2",
|
||||
"sass": "1.91.0",
|
||||
"sass-loader": "16.0.5",
|
||||
"webpack": "5.99.8",
|
||||
"webpack": "5.101.3",
|
||||
"webpack-cli": "5.1.4"
|
||||
}
|
||||
},
|
||||
@@ -35,18 +35,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
||||
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.2.1",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.10",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
@@ -59,20 +55,10 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/set-array": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/source-map": {
|
||||
"version": "0.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz",
|
||||
"integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==",
|
||||
"version": "0.3.11",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
|
||||
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -81,16 +67,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.25",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||
"version": "0.3.30",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz",
|
||||
"integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -442,9 +428,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -456,13 +442,13 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.15.21",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
|
||||
"integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
|
||||
"version": "24.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.1.tgz",
|
||||
"integrity": "sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
"undici-types": "~7.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@webassemblyjs/ast": {
|
||||
@@ -688,9 +674,9 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.14.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"version": "8.15.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -700,6 +686,19 @@
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn-import-phases": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz",
|
||||
"integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"acorn": "^8.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "8.17.1",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
@@ -782,9 +781,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.24.5",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
|
||||
"integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
|
||||
"version": "4.25.4",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz",
|
||||
"integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -802,8 +801,8 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001716",
|
||||
"electron-to-chromium": "^1.5.149",
|
||||
"caniuse-lite": "^1.0.30001737",
|
||||
"electron-to-chromium": "^1.5.211",
|
||||
"node-releases": "^2.0.19",
|
||||
"update-browserslist-db": "^1.1.3"
|
||||
},
|
||||
@@ -822,9 +821,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001718",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz",
|
||||
"integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==",
|
||||
"version": "1.0.30001741",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001741.tgz",
|
||||
"integrity": "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -976,16 +975,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.5.155",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz",
|
||||
"integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==",
|
||||
"version": "1.5.215",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.215.tgz",
|
||||
"integrity": "sha512-TIvGp57UpeNetj/wV/xpFNpWGb0b/ROw372lHPx5Aafx02gjTBtWnEEcaSX3W2dLM3OSdGGyHX/cHl01JQsLaQ==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/enhanced-resolve": {
|
||||
"version": "5.18.1",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz",
|
||||
"integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==",
|
||||
"version": "5.18.3",
|
||||
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
|
||||
"integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1108,9 +1107,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-uri": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz",
|
||||
"integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==",
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
|
||||
"integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -1242,9 +1241,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.2.tgz",
|
||||
"integrity": "sha512-qHKXW1q6liAk1Oys6umoaZbDRqjcjgSrbnrifHsfsttza7zcvRAsL7mMV6xWcyhwQy7Xj5v4hhbr6b+iDYwlmQ==",
|
||||
"version": "5.1.3",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz",
|
||||
"integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -1529,9 +1528,9 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.19",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
|
||||
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
|
||||
"version": "2.0.20",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.20.tgz",
|
||||
"integrity": "sha512-7gK6zSXEH6neM212JgfYFXe+GmZQM+fia5SsusuBIUgnPheLFBmIPhtFoAQRj8/7wASYQnbDlHPVwY0BefoFgA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -1636,9 +1635,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
||||
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -1656,7 +1655,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.8",
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -1861,9 +1860,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.89.2",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.89.2.tgz",
|
||||
"integrity": "sha512-xCmtksBKd/jdJ9Bt9p7nPKiuqrlBMBuuGkQlkhZjjQk3Ty48lv93k5Dq6OPkKt4XwxDJ7tvlfrTa1MPA9bf+QA==",
|
||||
"version": "1.91.0",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.91.0.tgz",
|
||||
"integrity": "sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2062,24 +2061,28 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tapable": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz",
|
||||
"integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==",
|
||||
"version": "2.2.3",
|
||||
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz",
|
||||
"integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/webpack"
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.39.2",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.2.tgz",
|
||||
"integrity": "sha512-yEPUmWve+VA78bI71BW70Dh0TuV4HHd+I5SHOAfS1+QBOmvmCiiffgjR8ryyEd3KIfvPGFqoADt8LdQ6XpXIvg==",
|
||||
"version": "5.44.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz",
|
||||
"integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.14.0",
|
||||
"acorn": "^8.15.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
@@ -2148,9 +2151,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"version": "7.10.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
|
||||
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -2207,22 +2210,23 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack": {
|
||||
"version": "5.99.8",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz",
|
||||
"integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==",
|
||||
"version": "5.101.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz",
|
||||
"integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.6",
|
||||
"@types/estree": "^1.0.8",
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@webassemblyjs/ast": "^1.14.1",
|
||||
"@webassemblyjs/wasm-edit": "^1.14.1",
|
||||
"@webassemblyjs/wasm-parser": "^1.14.1",
|
||||
"acorn": "^8.14.0",
|
||||
"acorn": "^8.15.0",
|
||||
"acorn-import-phases": "^1.0.3",
|
||||
"browserslist": "^4.24.0",
|
||||
"chrome-trace-event": "^1.0.2",
|
||||
"enhanced-resolve": "^5.17.1",
|
||||
"enhanced-resolve": "^5.17.3",
|
||||
"es-module-lexer": "^1.2.1",
|
||||
"eslint-scope": "5.1.1",
|
||||
"events": "^3.2.0",
|
||||
@@ -2236,7 +2240,7 @@
|
||||
"tapable": "^2.1.1",
|
||||
"terser-webpack-plugin": "^5.3.11",
|
||||
"watchpack": "^2.4.1",
|
||||
"webpack-sources": "^3.2.3"
|
||||
"webpack-sources": "^3.3.3"
|
||||
},
|
||||
"bin": {
|
||||
"webpack": "bin/webpack.js"
|
||||
@@ -2326,9 +2330,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/webpack-sources": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz",
|
||||
"integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==",
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz",
|
||||
"integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
"css-loader": "7.1.2",
|
||||
"expose-loader": "5.0.1",
|
||||
"mini-css-extract-plugin": "2.9.2",
|
||||
"sass": "1.89.2",
|
||||
"sass": "1.91.0",
|
||||
"sass-loader": "16.0.5",
|
||||
"webpack": "5.99.8",
|
||||
"webpack": "5.101.3",
|
||||
"webpack-cli": "5.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Authorization;
|
||||
|
||||
public static class AuthorizationHandlerCollectionExtensions
|
||||
{
|
||||
public static void AddAdminConsoleAuthorizationHandlers(this IServiceCollection services)
|
||||
{
|
||||
services.TryAddScoped<IOrganizationContext, OrganizationContext>();
|
||||
|
||||
services.TryAddEnumerable([
|
||||
ServiceDescriptor.Scoped<IAuthorizationHandler, BulkCollectionAuthorizationHandler>(),
|
||||
ServiceDescriptor.Scoped<IAuthorizationHandler, CollectionAuthorizationHandler>(),
|
||||
ServiceDescriptor.Scoped<IAuthorizationHandler, GroupAuthorizationHandler>(),
|
||||
ServiceDescriptor.Scoped<IAuthorizationHandler, OrganizationRequirementHandler>(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
#nullable enable
|
||||
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Security.Claims;
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.Models.Data;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Authorization;
|
||||
|
||||
84
src/Api/AdminConsole/Authorization/OrganizationContext.cs
Normal file
84
src/Api/AdminConsole/Authorization/OrganizationContext.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Models.Data.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Services;
|
||||
|
||||
// Note: do not move this into Core! See remarks below.
|
||||
namespace Bit.Api.AdminConsole.Authorization;
|
||||
|
||||
/// <summary>
|
||||
/// Provides information about a user's membership or provider relationship with an organization.
|
||||
/// Used for authorization decisions in the API layer, usually called by a controller or authorization handler or attribute.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is intended to deprecate organization-related methods in <see cref="ICurrentContext"/>.
|
||||
/// It should remain in the API layer (not Core) because it is closely tied to user claims and authentication.
|
||||
/// </remarks>
|
||||
public interface IOrganizationContext
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses the provided <see cref="ClaimsPrincipal"/> for claims relating to the specified organization.
|
||||
/// A user will have organization claims if they are a confirmed member of the organization.
|
||||
/// </summary>
|
||||
/// <param name="user">The claims for the user.</param>
|
||||
/// <param name="organizationId">The organization to extract claims for.</param>
|
||||
/// <returns>
|
||||
/// A <see cref="CurrentContextOrganization"/> representing the user's claims for the organization,
|
||||
/// or null if the user has no claims.
|
||||
/// </returns>
|
||||
public CurrentContextOrganization? GetOrganizationClaims(ClaimsPrincipal user, Guid organizationId);
|
||||
/// <summary>
|
||||
/// Used to determine whether the user is a ProviderUser for the specified organization.
|
||||
/// </summary>
|
||||
/// <param name="user">The claims for the user.</param>
|
||||
/// <param name="organizationId">The organization to check the provider relationship for.</param>
|
||||
/// <returns>True if the user is a ProviderUser for the specified organization, otherwise false.</returns>
|
||||
/// <remarks>
|
||||
/// This requires a database call, but the results are cached for the lifetime of the service instance.
|
||||
/// Try to check purely claims-based sources of authorization first (such as organization membership with
|
||||
/// <see cref="GetOrganizationClaims"/>) to avoid unnecessary database calls.
|
||||
/// </remarks>
|
||||
public Task<bool> IsProviderUserForOrganization(ClaimsPrincipal user, Guid organizationId);
|
||||
}
|
||||
|
||||
public class OrganizationContext(
|
||||
IUserService userService,
|
||||
IProviderUserRepository providerUserRepository) : IOrganizationContext
|
||||
{
|
||||
public const string NoUserIdError = "This method should only be called on the private api with a logged in user.";
|
||||
|
||||
/// <summary>
|
||||
/// Caches provider relationships by UserId.
|
||||
/// In practice this should only have 1 entry (for the current user), but this approach ensures that a mix-up
|
||||
/// between users cannot occur if <see cref="IsProviderUserForOrganization"/> is called with a different
|
||||
/// ClaimsPrincipal for any reason.
|
||||
/// </summary>
|
||||
private readonly Dictionary<Guid, IEnumerable<ProviderUserOrganizationDetails>> _providerUserOrganizationsCache = new();
|
||||
|
||||
public CurrentContextOrganization? GetOrganizationClaims(ClaimsPrincipal user, Guid organizationId)
|
||||
{
|
||||
return user.GetCurrentContextOrganization(organizationId);
|
||||
}
|
||||
|
||||
public async Task<bool> IsProviderUserForOrganization(ClaimsPrincipal user, Guid organizationId)
|
||||
{
|
||||
var userId = userService.GetProperUserId(user);
|
||||
if (!userId.HasValue)
|
||||
{
|
||||
throw new InvalidOperationException(NoUserIdError);
|
||||
}
|
||||
|
||||
if (!_providerUserOrganizationsCache.TryGetValue(userId.Value, out var providerUserOrganizations))
|
||||
{
|
||||
providerUserOrganizations =
|
||||
await providerUserRepository.GetManyOrganizationDetailsByUserAsync(userId.Value,
|
||||
ProviderUserStatusType.Confirmed);
|
||||
providerUserOrganizations = providerUserOrganizations.ToList();
|
||||
_providerUserOrganizationsCache[userId.Value] = providerUserOrganizations;
|
||||
}
|
||||
|
||||
return providerUserOrganizations.Any(o => o.OrganizationId == organizationId);
|
||||
}
|
||||
}
|
||||
@@ -163,7 +163,6 @@ public class GroupsController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[HttpPost("{id}")]
|
||||
public async Task<GroupResponseModel> Put(Guid orgId, Guid id, [FromBody] GroupRequestModel model)
|
||||
{
|
||||
if (!await _currentContext.ManageGroups(orgId))
|
||||
@@ -237,8 +236,14 @@ public class GroupsController : Controller
|
||||
return new GroupResponseModel(group);
|
||||
}
|
||||
|
||||
[HttpPost("{id}")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT method instead")]
|
||||
public async Task<GroupResponseModel> PostPut(Guid orgId, Guid id, [FromBody] GroupRequestModel model)
|
||||
{
|
||||
return await Put(orgId, id, model);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpPost("{id}/delete")]
|
||||
public async Task Delete(string orgId, string id)
|
||||
{
|
||||
var group = await _groupRepository.GetByIdAsync(new Guid(id));
|
||||
@@ -250,8 +255,14 @@ public class GroupsController : Controller
|
||||
await _deleteGroupCommand.DeleteAsync(group);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE method instead")]
|
||||
public async Task PostDelete(string orgId, string id)
|
||||
{
|
||||
await Delete(orgId, id);
|
||||
}
|
||||
|
||||
[HttpDelete("")]
|
||||
[HttpPost("delete")]
|
||||
public async Task BulkDelete([FromBody] GroupBulkRequestModel model)
|
||||
{
|
||||
var groups = await _groupRepository.GetManyByManyIds(model.Ids);
|
||||
@@ -267,9 +278,15 @@ public class GroupsController : Controller
|
||||
await _deleteGroupCommand.DeleteManyAsync(groups);
|
||||
}
|
||||
|
||||
[HttpPost("delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE method instead")]
|
||||
public async Task PostBulkDelete([FromBody] GroupBulkRequestModel model)
|
||||
{
|
||||
await BulkDelete(model);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}/user/{orgUserId}")]
|
||||
[HttpPost("{id}/delete-user/{orgUserId}")]
|
||||
public async Task Delete(string orgId, string id, string orgUserId)
|
||||
public async Task DeleteUser(string orgId, string id, string orgUserId)
|
||||
{
|
||||
var group = await _groupRepository.GetByIdAsync(new Guid(id));
|
||||
if (group == null || !await _currentContext.ManageGroups(group.OrganizationId))
|
||||
@@ -279,4 +296,11 @@ public class GroupsController : Controller
|
||||
|
||||
await _groupService.DeleteUserAsync(group, new Guid(orgUserId));
|
||||
}
|
||||
|
||||
[HttpPost("{id}/delete-user/{orgUserId}")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE method instead")]
|
||||
public async Task PostDeleteUser(string orgId, string id, string orgUserId)
|
||||
{
|
||||
await DeleteUser(orgId, id, orgUserId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +140,6 @@ public class OrganizationConnectionsController : Controller
|
||||
}
|
||||
|
||||
[HttpDelete("{organizationConnectionId}")]
|
||||
[HttpPost("{organizationConnectionId}/delete")]
|
||||
public async Task DeleteConnection(Guid organizationConnectionId)
|
||||
{
|
||||
var connection = await _organizationConnectionRepository.GetByIdAsync(organizationConnectionId);
|
||||
@@ -158,6 +157,13 @@ public class OrganizationConnectionsController : Controller
|
||||
await _deleteOrganizationConnectionCommand.DeleteAsync(connection);
|
||||
}
|
||||
|
||||
[HttpPost("{organizationConnectionId}/delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE method instead")]
|
||||
public async Task PostDeleteConnection(Guid organizationConnectionId)
|
||||
{
|
||||
await DeleteConnection(organizationConnectionId);
|
||||
}
|
||||
|
||||
private async Task<ICollection<OrganizationConnection>> GetConnectionsAsync(Guid organizationId, OrganizationConnectionType type) =>
|
||||
await _organizationConnectionRepository.GetByOrganizationIdTypeAsync(organizationId, type);
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ public class OrganizationDomainController : Controller
|
||||
}
|
||||
|
||||
[HttpGet("{orgId}/domain")]
|
||||
public async Task<ListResponseModel<OrganizationDomainResponseModel>> Get(Guid orgId)
|
||||
public async Task<ListResponseModel<OrganizationDomainResponseModel>> GetAll(Guid orgId)
|
||||
{
|
||||
await ValidateOrganizationAccessAsync(orgId);
|
||||
|
||||
@@ -105,7 +105,6 @@ public class OrganizationDomainController : Controller
|
||||
}
|
||||
|
||||
[HttpDelete("{orgId}/domain/{id}")]
|
||||
[HttpPost("{orgId}/domain/{id}/remove")]
|
||||
public async Task RemoveDomain(Guid orgId, Guid id)
|
||||
{
|
||||
await ValidateOrganizationAccessAsync(orgId);
|
||||
@@ -119,6 +118,13 @@ public class OrganizationDomainController : Controller
|
||||
await _deleteOrganizationDomainCommand.DeleteAsync(domain);
|
||||
}
|
||||
|
||||
[HttpPost("{orgId}/domain/{id}/remove")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE method instead")]
|
||||
public async Task PostRemoveDomain(Guid orgId, Guid id)
|
||||
{
|
||||
await RemoveDomain(orgId, id);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("domain/sso/details")] // must be post to accept email cleanly
|
||||
public async Task<OrganizationDomainSsoDetailsResponseModel> GetOrgDomainSsoDetails(
|
||||
|
||||
@@ -98,7 +98,6 @@ public class OrganizationIntegrationConfigurationController(
|
||||
}
|
||||
|
||||
[HttpDelete("{configurationId:guid}")]
|
||||
[HttpPost("{configurationId:guid}/delete")]
|
||||
public async Task DeleteAsync(Guid organizationId, Guid integrationId, Guid configurationId)
|
||||
{
|
||||
if (!await HasPermission(organizationId))
|
||||
@@ -120,6 +119,13 @@ public class OrganizationIntegrationConfigurationController(
|
||||
await integrationConfigurationRepository.DeleteAsync(configuration);
|
||||
}
|
||||
|
||||
[HttpPost("{configurationId:guid}/delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE method instead")]
|
||||
public async Task PostDeleteAsync(Guid organizationId, Guid integrationId, Guid configurationId)
|
||||
{
|
||||
await DeleteAsync(organizationId, integrationId, configurationId);
|
||||
}
|
||||
|
||||
private async Task<bool> HasPermission(Guid organizationId)
|
||||
{
|
||||
return await currentContext.OrganizationOwner(organizationId);
|
||||
|
||||
@@ -64,7 +64,6 @@ public class OrganizationIntegrationController(
|
||||
}
|
||||
|
||||
[HttpDelete("{integrationId:guid}")]
|
||||
[HttpPost("{integrationId:guid}/delete")]
|
||||
public async Task DeleteAsync(Guid organizationId, Guid integrationId)
|
||||
{
|
||||
if (!await HasPermission(organizationId))
|
||||
@@ -81,6 +80,13 @@ public class OrganizationIntegrationController(
|
||||
await integrationRepository.DeleteAsync(integration);
|
||||
}
|
||||
|
||||
[HttpPost("{integrationId:guid}/delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE method instead")]
|
||||
public async Task PostDeleteAsync(Guid organizationId, Guid integrationId)
|
||||
{
|
||||
await DeleteAsync(organizationId, integrationId);
|
||||
}
|
||||
|
||||
private async Task<bool> HasPermission(Guid organizationId)
|
||||
{
|
||||
return await currentContext.OrganizationOwner(organizationId);
|
||||
|
||||
@@ -11,6 +11,7 @@ using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.DeleteClaimedAccountvNext;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.InviteUsers;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||
@@ -23,6 +24,7 @@ using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||
@@ -59,6 +61,7 @@ public class OrganizationUsersController : Controller
|
||||
private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
private readonly IDeleteClaimedOrganizationUserAccountCommand _deleteClaimedOrganizationUserAccountCommand;
|
||||
private readonly IDeleteClaimedOrganizationUserAccountCommandvNext _deleteClaimedOrganizationUserAccountCommandvNext;
|
||||
private readonly IGetOrganizationUsersClaimedStatusQuery _getOrganizationUsersClaimedStatusQuery;
|
||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
@@ -87,6 +90,7 @@ public class OrganizationUsersController : Controller
|
||||
IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||
IDeleteClaimedOrganizationUserAccountCommand deleteClaimedOrganizationUserAccountCommand,
|
||||
IDeleteClaimedOrganizationUserAccountCommandvNext deleteClaimedOrganizationUserAccountCommandvNext,
|
||||
IGetOrganizationUsersClaimedStatusQuery getOrganizationUsersClaimedStatusQuery,
|
||||
IPolicyRequirementQuery policyRequirementQuery,
|
||||
IFeatureService featureService,
|
||||
@@ -115,6 +119,7 @@ public class OrganizationUsersController : Controller
|
||||
_organizationUserUserDetailsQuery = organizationUserUserDetailsQuery;
|
||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||
_deleteClaimedOrganizationUserAccountCommand = deleteClaimedOrganizationUserAccountCommand;
|
||||
_deleteClaimedOrganizationUserAccountCommandvNext = deleteClaimedOrganizationUserAccountCommandvNext;
|
||||
_getOrganizationUsersClaimedStatusQuery = getOrganizationUsersClaimedStatusQuery;
|
||||
_policyRequirementQuery = policyRequirementQuery;
|
||||
_featureService = featureService;
|
||||
@@ -167,7 +172,7 @@ public class OrganizationUsersController : Controller
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> Get(Guid orgId, bool includeGroups = false, bool includeCollections = false)
|
||||
public async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> GetAll(Guid orgId, bool includeGroups = false, bool includeCollections = false)
|
||||
{
|
||||
var request = new OrganizationUserUserDetailsQueryRequest
|
||||
{
|
||||
@@ -360,7 +365,6 @@ public class OrganizationUsersController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[HttpPost("{id}")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task Put(Guid orgId, Guid id, [FromBody] OrganizationUserUpdateRequestModel model)
|
||||
{
|
||||
@@ -436,6 +440,14 @@ public class OrganizationUsersController : Controller
|
||||
collectionsToSave, groupsToSave);
|
||||
}
|
||||
|
||||
[HttpPost("{id}")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT method instead")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task PostPut(Guid orgId, Guid id, [FromBody] OrganizationUserUpdateRequestModel model)
|
||||
{
|
||||
await Put(orgId, id, model);
|
||||
}
|
||||
|
||||
[HttpPut("{userId}/reset-password-enrollment")]
|
||||
public async Task PutResetPasswordEnrollment(Guid orgId, Guid userId, [FromBody] OrganizationUserResetPasswordEnrollmentRequestModel model)
|
||||
{
|
||||
@@ -492,7 +504,6 @@ public class OrganizationUsersController : Controller
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpPost("{id}/remove")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task Remove(Guid orgId, Guid id)
|
||||
{
|
||||
@@ -500,8 +511,15 @@ public class OrganizationUsersController : Controller
|
||||
await _removeOrganizationUserCommand.RemoveUserAsync(orgId, id, userId.Value);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/remove")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE method instead")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task PostRemove(Guid orgId, Guid id)
|
||||
{
|
||||
await Remove(orgId, id);
|
||||
}
|
||||
|
||||
[HttpDelete("")]
|
||||
[HttpPost("remove")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRemove(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||
{
|
||||
@@ -511,11 +529,24 @@ public class OrganizationUsersController : Controller
|
||||
new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage)));
|
||||
}
|
||||
|
||||
[HttpPost("remove")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE method instead")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> PostBulkRemove(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||
{
|
||||
return await BulkRemove(orgId, model);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}/delete-account")]
|
||||
[HttpPost("{id}/delete-account")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task DeleteAccount(Guid orgId, Guid id)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.DeleteClaimedUserAccountRefactor))
|
||||
{
|
||||
await DeleteAccountvNext(orgId, id);
|
||||
return;
|
||||
}
|
||||
|
||||
var currentUser = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (currentUser == null)
|
||||
{
|
||||
@@ -525,11 +556,41 @@ public class OrganizationUsersController : Controller
|
||||
await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/delete-account")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE method instead")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task PostDeleteAccount(Guid orgId, Guid id)
|
||||
{
|
||||
await DeleteAccount(orgId, id);
|
||||
}
|
||||
|
||||
private async Task<IResult> DeleteAccountvNext(Guid orgId, Guid id)
|
||||
{
|
||||
var currentUserId = _userService.GetProperUserId(User);
|
||||
if (currentUserId == null)
|
||||
{
|
||||
return TypedResults.Unauthorized();
|
||||
}
|
||||
|
||||
var commandResult = await _deleteClaimedOrganizationUserAccountCommandvNext.DeleteUserAsync(orgId, id, currentUserId.Value);
|
||||
|
||||
return commandResult.Result.Match<IResult>(
|
||||
error => error is NotFoundError
|
||||
? TypedResults.NotFound(new ErrorResponseModel(error.Message))
|
||||
: TypedResults.BadRequest(new ErrorResponseModel(error.Message)),
|
||||
TypedResults.Ok
|
||||
);
|
||||
}
|
||||
|
||||
[HttpDelete("delete-account")]
|
||||
[HttpPost("delete-account")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||
{
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.DeleteClaimedUserAccountRefactor))
|
||||
{
|
||||
return await BulkDeleteAccountvNext(orgId, model);
|
||||
}
|
||||
|
||||
var currentUser = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (currentUser == null)
|
||||
{
|
||||
@@ -542,7 +603,32 @@ public class OrganizationUsersController : Controller
|
||||
new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage)));
|
||||
}
|
||||
|
||||
[HttpPatch("{id}/revoke")]
|
||||
[HttpPost("delete-account")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE method instead")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> PostBulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||
{
|
||||
return await BulkDeleteAccount(orgId, model);
|
||||
}
|
||||
|
||||
private async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccountvNext(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||
{
|
||||
var currentUserId = _userService.GetProperUserId(User);
|
||||
if (currentUserId == null)
|
||||
{
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
var result = await _deleteClaimedOrganizationUserAccountCommandvNext.DeleteManyUsersAsync(orgId, model.Ids, currentUserId.Value);
|
||||
|
||||
var responses = result.Select(r => r.Result.Match(
|
||||
error => new OrganizationUserBulkResponseModel(r.Id, error.Message),
|
||||
_ => new OrganizationUserBulkResponseModel(r.Id, string.Empty)
|
||||
));
|
||||
|
||||
return new ListResponseModel<OrganizationUserBulkResponseModel>(responses);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/revoke")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task RevokeAsync(Guid orgId, Guid id)
|
||||
@@ -550,7 +636,14 @@ public class OrganizationUsersController : Controller
|
||||
await RestoreOrRevokeUserAsync(orgId, id, _revokeOrganizationUserCommand.RevokeUserAsync);
|
||||
}
|
||||
|
||||
[HttpPatch("revoke")]
|
||||
[HttpPatch("{id}/revoke")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT method instead")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task PatchRevokeAsync(Guid orgId, Guid id)
|
||||
{
|
||||
await RevokeAsync(orgId, id);
|
||||
}
|
||||
|
||||
[HttpPut("revoke")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRevokeAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||
@@ -558,7 +651,14 @@ public class OrganizationUsersController : Controller
|
||||
return await RestoreOrRevokeUsersAsync(orgId, model, _revokeOrganizationUserCommand.RevokeUsersAsync);
|
||||
}
|
||||
|
||||
[HttpPatch("{id}/restore")]
|
||||
[HttpPatch("revoke")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT method instead")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> PatchBulkRevokeAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||
{
|
||||
return await BulkRevokeAsync(orgId, model);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/restore")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task RestoreAsync(Guid orgId, Guid id)
|
||||
@@ -566,7 +666,14 @@ public class OrganizationUsersController : Controller
|
||||
await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _restoreOrganizationUserCommand.RestoreUserAsync(orgUser, userId));
|
||||
}
|
||||
|
||||
[HttpPatch("restore")]
|
||||
[HttpPatch("{id}/restore")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT method instead")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task PatchRestoreAsync(Guid orgId, Guid id)
|
||||
{
|
||||
await RestoreAsync(orgId, id);
|
||||
}
|
||||
|
||||
[HttpPut("restore")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRestoreAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||
@@ -574,7 +681,14 @@ public class OrganizationUsersController : Controller
|
||||
return await RestoreOrRevokeUsersAsync(orgId, model, (orgId, orgUserIds, restoringUserId) => _restoreOrganizationUserCommand.RestoreUsersAsync(orgId, orgUserIds, restoringUserId, _userService));
|
||||
}
|
||||
|
||||
[HttpPatch("enable-secrets-manager")]
|
||||
[HttpPatch("restore")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT method instead")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> PatchBulkRestoreAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||
{
|
||||
return await BulkRestoreAsync(orgId, model);
|
||||
}
|
||||
|
||||
[HttpPut("enable-secrets-manager")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task BulkEnableSecretsManagerAsync(Guid orgId,
|
||||
@@ -607,6 +721,15 @@ public class OrganizationUsersController : Controller
|
||||
await _organizationUserRepository.ReplaceManyAsync(orgUsers);
|
||||
}
|
||||
|
||||
[HttpPatch("enable-secrets-manager")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT method instead")]
|
||||
[Authorize<ManageUsersRequirement>]
|
||||
public async Task PatchBulkEnableSecretsManagerAsync(Guid orgId,
|
||||
[FromBody] OrganizationUserBulkRequestModel model)
|
||||
{
|
||||
await BulkEnableSecretsManagerAsync(orgId, model);
|
||||
}
|
||||
|
||||
private async Task RestoreOrRevokeUserAsync(
|
||||
Guid orgId,
|
||||
Guid id,
|
||||
|
||||
@@ -225,7 +225,6 @@ public class OrganizationsController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[HttpPost("{id}")]
|
||||
public async Task<OrganizationResponseModel> Put(string id, [FromBody] OrganizationUpdateRequestModel model)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
@@ -252,6 +251,13 @@ public class OrganizationsController : Controller
|
||||
return new OrganizationResponseModel(organization, plan);
|
||||
}
|
||||
|
||||
[HttpPost("{id}")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT method instead")]
|
||||
public async Task<OrganizationResponseModel> PostPut(string id, [FromBody] OrganizationUpdateRequestModel model)
|
||||
{
|
||||
return await Put(id, model);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/storage")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task<PaymentResponseModel> PostStorage(string id, [FromBody] StorageRequestModel model)
|
||||
@@ -291,7 +297,6 @@ public class OrganizationsController : Controller
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpPost("{id}/delete")]
|
||||
public async Task Delete(string id, [FromBody] SecretVerificationRequestModel model)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
@@ -334,6 +339,13 @@ public class OrganizationsController : Controller
|
||||
await _organizationDeleteCommand.DeleteAsync(organization);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE method instead")]
|
||||
public async Task PostDelete(string id, [FromBody] SecretVerificationRequestModel model)
|
||||
{
|
||||
await Delete(id, model);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/delete-recover-token")]
|
||||
[AllowAnonymous]
|
||||
public async Task PostDeleteRecoverToken(Guid id, [FromBody] OrganizationVerifyDeleteRecoverRequestModel model)
|
||||
@@ -554,18 +566,12 @@ public class OrganizationsController : Controller
|
||||
[HttpPut("{id}/collection-management")]
|
||||
public async Task<OrganizationResponseModel> PutCollectionManagement(Guid id, [FromBody] OrganizationCollectionManagementUpdateRequestModel model)
|
||||
{
|
||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||
if (organization == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (!await _currentContext.OrganizationOwner(id))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _organizationService.UpdateAsync(model.ToOrganization(organization, _featureService), eventType: EventType.Organization_CollectionManagement_Updated);
|
||||
var organization = await _organizationService.UpdateCollectionManagementSettingsAsync(id, model.ToSettings());
|
||||
var plan = await _pricingClient.GetPlan(organization.PlanType);
|
||||
return new OrganizationResponseModel(organization, plan);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Api.AdminConsole.Authorization;
|
||||
using Bit.Api.AdminConsole.Authorization.Requirements;
|
||||
using Bit.Api.AdminConsole.Models.Request;
|
||||
using Bit.Api.AdminConsole.Models.Response.Helpers;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||
@@ -30,7 +33,6 @@ namespace Bit.Api.AdminConsole.Controllers;
|
||||
public class PoliciesController : Controller
|
||||
{
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IOrganizationHasVerifiedDomainsQuery _organizationHasVerifiedDomainsQuery;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
@@ -49,7 +51,6 @@ public class PoliciesController : Controller
|
||||
GlobalSettings globalSettings,
|
||||
IDataProtectionProvider dataProtectionProvider,
|
||||
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
|
||||
IFeatureService featureService,
|
||||
IOrganizationHasVerifiedDomainsQuery organizationHasVerifiedDomainsQuery,
|
||||
IOrganizationRepository organizationRepository,
|
||||
ISavePolicyCommand savePolicyCommand)
|
||||
@@ -63,7 +64,6 @@ public class PoliciesController : Controller
|
||||
"OrganizationServiceDataProtector");
|
||||
_organizationRepository = organizationRepository;
|
||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||
_featureService = featureService;
|
||||
_organizationHasVerifiedDomainsQuery = organizationHasVerifiedDomainsQuery;
|
||||
_savePolicyCommand = savePolicyCommand;
|
||||
}
|
||||
@@ -90,7 +90,7 @@ public class PoliciesController : Controller
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<PolicyResponseModel>> Get(string orgId)
|
||||
public async Task<ListResponseModel<PolicyResponseModel>> GetAll(string orgId)
|
||||
{
|
||||
var orgIdGuid = new Guid(orgId);
|
||||
if (!await _currentContext.ManagePolicies(orgIdGuid))
|
||||
@@ -212,4 +212,18 @@ public class PoliciesController : Controller
|
||||
var policy = await _savePolicyCommand.SaveAsync(policyUpdate);
|
||||
return new PolicyResponseModel(policy);
|
||||
}
|
||||
|
||||
|
||||
[HttpPut("{type}/vnext")]
|
||||
[RequireFeatureAttribute(FeatureFlagKeys.CreateDefaultLocation)]
|
||||
[Authorize<ManagePoliciesRequirement>]
|
||||
public async Task<PolicyResponseModel> PutVNext(Guid orgId, [FromBody] SavePolicyRequest model)
|
||||
{
|
||||
var savePolicyRequest = await model.ToSavePolicyModelAsync(orgId, _currentContext);
|
||||
|
||||
var policy = await _savePolicyCommand.VNextSaveAsync(savePolicyRequest);
|
||||
|
||||
return new PolicyResponseModel(policy);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -93,7 +93,6 @@ public class ProviderOrganizationsController : Controller
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[HttpPost("{id:guid}/delete")]
|
||||
public async Task Delete(Guid providerId, Guid id)
|
||||
{
|
||||
if (!_currentContext.ManageProviderOrganizations(providerId))
|
||||
@@ -112,4 +111,11 @@ public class ProviderOrganizationsController : Controller
|
||||
providerOrganization,
|
||||
organization);
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE method instead")]
|
||||
public async Task PostDelete(Guid providerId, Guid id)
|
||||
{
|
||||
await Delete(providerId, id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,7 +49,7 @@ public class ProviderUsersController : Controller
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<ProviderUserUserDetailsResponseModel>> Get(Guid providerId)
|
||||
public async Task<ListResponseModel<ProviderUserUserDetailsResponseModel>> GetAll(Guid providerId)
|
||||
{
|
||||
if (!_currentContext.ProviderManageUsers(providerId))
|
||||
{
|
||||
@@ -155,7 +155,6 @@ public class ProviderUsersController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[HttpPost("{id:guid}")]
|
||||
public async Task Put(Guid providerId, Guid id, [FromBody] ProviderUserUpdateRequestModel model)
|
||||
{
|
||||
if (!_currentContext.ProviderManageUsers(providerId))
|
||||
@@ -173,8 +172,14 @@ public class ProviderUsersController : Controller
|
||||
await _providerService.SaveUserAsync(model.ToProviderUser(providerUser), userId.Value);
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT method instead")]
|
||||
public async Task PostPut(Guid providerId, Guid id, [FromBody] ProviderUserUpdateRequestModel model)
|
||||
{
|
||||
await Put(providerId, id, model);
|
||||
}
|
||||
|
||||
[HttpDelete("{id:guid}")]
|
||||
[HttpPost("{id:guid}/delete")]
|
||||
public async Task Delete(Guid providerId, Guid id)
|
||||
{
|
||||
if (!_currentContext.ProviderManageUsers(providerId))
|
||||
@@ -186,8 +191,14 @@ public class ProviderUsersController : Controller
|
||||
await _providerService.DeleteUsersAsync(providerId, new[] { id }, userId.Value);
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE method instead")]
|
||||
public async Task PostDelete(Guid providerId, Guid id)
|
||||
{
|
||||
await Delete(providerId, id);
|
||||
}
|
||||
|
||||
[HttpDelete("")]
|
||||
[HttpPost("delete")]
|
||||
public async Task<ListResponseModel<ProviderUserBulkResponseModel>> BulkDelete(Guid providerId, [FromBody] ProviderUserBulkRequestModel model)
|
||||
{
|
||||
if (!_currentContext.ProviderManageUsers(providerId))
|
||||
@@ -200,4 +211,11 @@ public class ProviderUsersController : Controller
|
||||
return new ListResponseModel<ProviderUserBulkResponseModel>(result.Select(r =>
|
||||
new ProviderUserBulkResponseModel(r.Item1.Id, r.Item2)));
|
||||
}
|
||||
|
||||
[HttpPost("delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE method instead")]
|
||||
public async Task<ListResponseModel<ProviderUserBulkResponseModel>> PostBulkDelete(Guid providerId, [FromBody] ProviderUserBulkRequestModel model)
|
||||
{
|
||||
return await BulkDelete(providerId, model);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@ public class ProvidersController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("{id:guid}")]
|
||||
[HttpPost("{id:guid}")]
|
||||
public async Task<ProviderResponseModel> Put(Guid id, [FromBody] ProviderUpdateRequestModel model)
|
||||
{
|
||||
if (!_currentContext.ProviderProviderAdmin(id))
|
||||
@@ -71,6 +70,13 @@ public class ProvidersController : Controller
|
||||
return new ProviderResponseModel(provider);
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT method instead")]
|
||||
public async Task<ProviderResponseModel> PostPut(Guid id, [FromBody] ProviderUpdateRequestModel model)
|
||||
{
|
||||
return await Put(id, model);
|
||||
}
|
||||
|
||||
[HttpPost("{id:guid}/setup")]
|
||||
public async Task<ProviderResponseModel> Setup(Guid id, [FromBody] ProviderSetupRequestModel model)
|
||||
{
|
||||
@@ -120,7 +126,6 @@ public class ProvidersController : Controller
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpPost("{id}/delete")]
|
||||
public async Task Delete(Guid id)
|
||||
{
|
||||
if (!_currentContext.ProviderProviderAdmin(id))
|
||||
@@ -142,4 +147,11 @@ public class ProvidersController : Controller
|
||||
|
||||
await _providerService.DeleteAsync(provider);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE method instead")]
|
||||
public async Task PostDelete(Guid id)
|
||||
{
|
||||
await Delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json.Serialization;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@@ -139,7 +140,7 @@ public class OrganizationCreateRequestModel : IValidatableObject
|
||||
new string[] { nameof(BillingAddressCountry) });
|
||||
}
|
||||
|
||||
if (PlanType != PlanType.Free && BillingAddressCountry == "US" &&
|
||||
if (PlanType != PlanType.Free && BillingAddressCountry == Constants.CountryAbbreviations.UnitedStates &&
|
||||
string.IsNullOrWhiteSpace(BillingAddressPostalCode))
|
||||
{
|
||||
yield return new ValidationResult("Zip / postal code is required.",
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#nullable enable
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Enums;
|
||||
@@ -36,6 +34,10 @@ public class OrganizationIntegrationConfigurationRequestModel
|
||||
return !string.IsNullOrWhiteSpace(Template) &&
|
||||
Configuration is null &&
|
||||
IsFiltersValid();
|
||||
case IntegrationType.Datadog:
|
||||
return !string.IsNullOrWhiteSpace(Template) &&
|
||||
Configuration is null &&
|
||||
IsFiltersValid();
|
||||
default:
|
||||
return false;
|
||||
|
||||
|
||||
@@ -4,15 +4,13 @@ using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Models.Data.EventIntegrations;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
#nullable enable
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
|
||||
public class OrganizationIntegrationRequestModel : IValidatableObject
|
||||
{
|
||||
public string? Configuration { get; set; }
|
||||
public string? Configuration { get; init; }
|
||||
|
||||
public IntegrationType Type { get; set; }
|
||||
public IntegrationType Type { get; init; }
|
||||
|
||||
public OrganizationIntegration ToOrganizationIntegration(Guid organizationId)
|
||||
{
|
||||
@@ -35,54 +33,55 @@ public class OrganizationIntegrationRequestModel : IValidatableObject
|
||||
switch (Type)
|
||||
{
|
||||
case IntegrationType.CloudBillingSync or IntegrationType.Scim:
|
||||
yield return new ValidationResult($"{nameof(Type)} integrations are not yet supported.", new[] { nameof(Type) });
|
||||
yield return new ValidationResult($"{nameof(Type)} integrations are not yet supported.", [nameof(Type)]);
|
||||
break;
|
||||
case IntegrationType.Slack:
|
||||
yield return new ValidationResult($"{nameof(Type)} integrations cannot be created directly.", new[] { nameof(Type) });
|
||||
yield return new ValidationResult($"{nameof(Type)} integrations cannot be created directly.", [nameof(Type)]);
|
||||
break;
|
||||
case IntegrationType.Webhook:
|
||||
if (string.IsNullOrWhiteSpace(Configuration))
|
||||
{
|
||||
break;
|
||||
}
|
||||
if (!IsIntegrationValid<WebhookIntegration>())
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
"Webhook integrations must include valid configuration.",
|
||||
new[] { nameof(Configuration) });
|
||||
}
|
||||
foreach (var r in ValidateConfiguration<WebhookIntegration>(allowNullOrEmpty: true))
|
||||
yield return r;
|
||||
break;
|
||||
case IntegrationType.Hec:
|
||||
if (!IsIntegrationValid<HecIntegration>())
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
"HEC integrations must include valid configuration.",
|
||||
new[] { nameof(Configuration) });
|
||||
}
|
||||
foreach (var r in ValidateConfiguration<HecIntegration>(allowNullOrEmpty: false))
|
||||
yield return r;
|
||||
break;
|
||||
case IntegrationType.Datadog:
|
||||
foreach (var r in ValidateConfiguration<DatadogIntegration>(allowNullOrEmpty: false))
|
||||
yield return r;
|
||||
break;
|
||||
default:
|
||||
yield return new ValidationResult(
|
||||
$"Integration type '{Type}' is not recognized.",
|
||||
new[] { nameof(Type) });
|
||||
[nameof(Type)]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsIntegrationValid<T>()
|
||||
private List<ValidationResult> ValidateConfiguration<T>(bool allowNullOrEmpty)
|
||||
{
|
||||
var results = new List<ValidationResult>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(Configuration))
|
||||
{
|
||||
return false;
|
||||
if (!allowNullOrEmpty)
|
||||
results.Add(InvalidConfig<T>());
|
||||
return results;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var config = JsonSerializer.Deserialize<T>(Configuration);
|
||||
return config is not null;
|
||||
if (JsonSerializer.Deserialize<T>(Configuration) is null)
|
||||
results.Add(InvalidConfig<T>());
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
results.Add(InvalidConfig<T>());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private static ValidationResult InvalidConfig<T>() =>
|
||||
new(errorMessage: $"Must include valid {typeof(T).Name} configuration.", memberNames: [nameof(Configuration)]);
|
||||
}
|
||||
|
||||
61
src/Api/AdminConsole/Models/Request/SavePolicyRequest.cs
Normal file
61
src/Api/AdminConsole/Models/Request/SavePolicyRequest.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Text.Json;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Request;
|
||||
|
||||
public class SavePolicyRequest
|
||||
{
|
||||
[Required]
|
||||
public PolicyRequestModel Policy { get; set; } = null!;
|
||||
|
||||
public Dictionary<string, object>? Metadata { get; set; }
|
||||
|
||||
public async Task<SavePolicyModel> ToSavePolicyModelAsync(Guid organizationId, ICurrentContext currentContext)
|
||||
{
|
||||
var performedBy = new StandardUser(currentContext.UserId!.Value, await currentContext.OrganizationOwner(organizationId));
|
||||
|
||||
var updatedPolicy = new PolicyUpdate()
|
||||
{
|
||||
Type = Policy.Type!.Value,
|
||||
OrganizationId = organizationId,
|
||||
Data = Policy.Data != null ? JsonSerializer.Serialize(Policy.Data) : null,
|
||||
Enabled = Policy.Enabled.GetValueOrDefault(),
|
||||
};
|
||||
|
||||
var metadata = MapToPolicyMetadata();
|
||||
|
||||
return new SavePolicyModel(updatedPolicy, performedBy, metadata);
|
||||
}
|
||||
|
||||
private IPolicyMetadataModel MapToPolicyMetadata()
|
||||
{
|
||||
if (Metadata == null)
|
||||
{
|
||||
return new EmptyMetadataModel();
|
||||
}
|
||||
|
||||
return Policy?.Type switch
|
||||
{
|
||||
PolicyType.OrganizationDataOwnership => MapToPolicyMetadata<OrganizationModelOwnershipPolicyModel>(),
|
||||
_ => new EmptyMetadataModel()
|
||||
};
|
||||
}
|
||||
|
||||
private IPolicyMetadataModel MapToPolicyMetadata<T>() where T : IPolicyMetadataModel, new()
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(Metadata);
|
||||
return CoreHelpers.LoadClassFromJsonData<T>(json);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new EmptyMetadataModel();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,8 +236,8 @@ public class OrganizationUserPublicKeyResponseModel : ResponseModel
|
||||
|
||||
public class OrganizationUserBulkResponseModel : ResponseModel
|
||||
{
|
||||
public OrganizationUserBulkResponseModel(Guid id, string error,
|
||||
string obj = "OrganizationBulkConfirmResponseModel") : base(obj)
|
||||
public OrganizationUserBulkResponseModel(Guid id, string error)
|
||||
: base("OrganizationBulkConfirmResponseModel")
|
||||
{
|
||||
Id = id;
|
||||
Error = error;
|
||||
|
||||
@@ -10,6 +10,10 @@ namespace Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
|
||||
public class PolicyResponseModel : ResponseModel
|
||||
{
|
||||
public PolicyResponseModel() : base("policy")
|
||||
{
|
||||
}
|
||||
|
||||
public PolicyResponseModel(Policy policy, string obj = "policy")
|
||||
: base(obj)
|
||||
{
|
||||
|
||||
@@ -78,12 +78,14 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
UseRiskInsights = organization.UseRiskInsights;
|
||||
UseOrganizationDomains = organization.UseOrganizationDomains;
|
||||
UseAdminSponsoredFamilies = organization.UseAdminSponsoredFamilies;
|
||||
SsoEnabled = organization.SsoEnabled ?? false;
|
||||
|
||||
if (organization.SsoConfig != null)
|
||||
{
|
||||
var ssoConfigData = SsoConfigurationData.Deserialize(organization.SsoConfig);
|
||||
KeyConnectorEnabled = ssoConfigData.MemberDecryptionType == MemberDecryptionType.KeyConnector && !string.IsNullOrEmpty(ssoConfigData.KeyConnectorUrl);
|
||||
KeyConnectorUrl = ssoConfigData.KeyConnectorUrl;
|
||||
SsoMemberDecryptionType = ssoConfigData.MemberDecryptionType;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,4 +162,6 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
public bool UseOrganizationDomains { get; set; }
|
||||
public bool UseAdminSponsoredFamilies { get; set; }
|
||||
public bool IsAdminInitiated { get; set; }
|
||||
public bool SsoEnabled { get; set; }
|
||||
public MemberDecryptionType? SsoMemberDecryptionType { get; set; }
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.2" />
|
||||
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.1" />
|
||||
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.25.0" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.2" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -344,7 +344,6 @@ public class AccountsController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("profile")]
|
||||
[HttpPost("profile")]
|
||||
public async Task<ProfileResponseModel> PutProfile([FromBody] UpdateProfileRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
@@ -363,8 +362,14 @@ public class AccountsController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("profile")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /profile instead.")]
|
||||
public async Task<ProfileResponseModel> PostProfile([FromBody] UpdateProfileRequestModel model)
|
||||
{
|
||||
return await PutProfile(model);
|
||||
}
|
||||
|
||||
[HttpPut("avatar")]
|
||||
[HttpPost("avatar")]
|
||||
public async Task<ProfileResponseModel> PutAvatar([FromBody] UpdateAvatarRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
@@ -382,6 +387,13 @@ public class AccountsController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("avatar")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /avatar instead.")]
|
||||
public async Task<ProfileResponseModel> PostAvatar([FromBody] UpdateAvatarRequestModel model)
|
||||
{
|
||||
return await PutAvatar(model);
|
||||
}
|
||||
|
||||
[HttpGet("revision-date")]
|
||||
public async Task<long?> GetAccountRevisionDate()
|
||||
{
|
||||
@@ -430,7 +442,6 @@ public class AccountsController : Controller
|
||||
}
|
||||
|
||||
[HttpDelete]
|
||||
[HttpPost("delete")]
|
||||
public async Task Delete([FromBody] SecretVerificationRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
@@ -467,6 +478,13 @@ public class AccountsController : Controller
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
[HttpPost("delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE / instead.")]
|
||||
public async Task PostDelete([FromBody] SecretVerificationRequestModel model)
|
||||
{
|
||||
await Delete(model);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("delete-recover")]
|
||||
public async Task PostDeleteRecover([FromBody] DeleteRecoverRequestModel model)
|
||||
@@ -638,7 +656,6 @@ public class AccountsController : Controller
|
||||
await _twoFactorEmailService.SendNewDeviceVerificationEmailAsync(user);
|
||||
}
|
||||
|
||||
[HttpPost("verify-devices")]
|
||||
[HttpPut("verify-devices")]
|
||||
public async Task SetUserVerifyDevicesAsync([FromBody] SetVerifyDevicesRequestModel request)
|
||||
{
|
||||
@@ -654,6 +671,13 @@ public class AccountsController : Controller
|
||||
await _userService.SaveUserAsync(user);
|
||||
}
|
||||
|
||||
[HttpPost("verify-devices")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /verify-devices instead.")]
|
||||
public async Task PostSetUserVerifyDevicesAsync([FromBody] SetVerifyDevicesRequestModel request)
|
||||
{
|
||||
await SetUserVerifyDevicesAsync(request);
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<Guid>> GetOrganizationIdsClaimingUserAsync(Guid userId)
|
||||
{
|
||||
var organizationsClaimingUser = await _userService.GetOrganizationsClaimingUserAsync(userId);
|
||||
|
||||
@@ -31,7 +31,7 @@ public class AuthRequestsController(
|
||||
private readonly IAuthRequestService _authRequestService = authRequestService;
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<AuthRequestResponseModel>> Get()
|
||||
public async Task<ListResponseModel<AuthRequestResponseModel>> GetAll()
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var authRequests = await _authRequestRepository.GetManyByUserIdAsync(userId);
|
||||
|
||||
@@ -79,7 +79,6 @@ public class EmergencyAccessController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[HttpPost("{id}")]
|
||||
public async Task Put(Guid id, [FromBody] EmergencyAccessUpdateRequestModel model)
|
||||
{
|
||||
var emergencyAccess = await _emergencyAccessRepository.GetByIdAsync(id);
|
||||
@@ -92,14 +91,27 @@ public class EmergencyAccessController : Controller
|
||||
await _emergencyAccessService.SaveAsync(model.ToEmergencyAccess(emergencyAccess), user);
|
||||
}
|
||||
|
||||
[HttpPost("{id}")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /{id} instead.")]
|
||||
public async Task Post(Guid id, [FromBody] EmergencyAccessUpdateRequestModel model)
|
||||
{
|
||||
await Put(id, model);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpPost("{id}/delete")]
|
||||
public async Task Delete(Guid id)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
await _emergencyAccessService.DeleteAsync(id, userId.Value);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE /{id} instead.")]
|
||||
public async Task PostDelete(Guid id)
|
||||
{
|
||||
await Delete(id);
|
||||
}
|
||||
|
||||
[HttpPost("invite")]
|
||||
public async Task Invite([FromBody] EmergencyAccessInviteRequestModel model)
|
||||
{
|
||||
@@ -136,7 +148,7 @@ public class EmergencyAccessController : Controller
|
||||
}
|
||||
|
||||
[HttpPost("{id}/approve")]
|
||||
public async Task Accept(Guid id)
|
||||
public async Task Approve(Guid id)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
await _emergencyAccessService.ApproveAsync(id, user);
|
||||
|
||||
@@ -110,7 +110,6 @@ public class TwoFactorController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("authenticator")]
|
||||
[HttpPost("authenticator")]
|
||||
public async Task<TwoFactorAuthenticatorResponseModel> PutAuthenticator(
|
||||
[FromBody] UpdateTwoFactorAuthenticatorRequestModel model)
|
||||
{
|
||||
@@ -133,6 +132,14 @@ public class TwoFactorController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("authenticator")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /authenticator instead.")]
|
||||
public async Task<TwoFactorAuthenticatorResponseModel> PostAuthenticator(
|
||||
[FromBody] UpdateTwoFactorAuthenticatorRequestModel model)
|
||||
{
|
||||
return await PutAuthenticator(model);
|
||||
}
|
||||
|
||||
[HttpDelete("authenticator")]
|
||||
public async Task<TwoFactorProviderResponseModel> DisableAuthenticator(
|
||||
[FromBody] TwoFactorAuthenticatorDisableRequestModel model)
|
||||
@@ -157,7 +164,6 @@ public class TwoFactorController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("yubikey")]
|
||||
[HttpPost("yubikey")]
|
||||
public async Task<TwoFactorYubiKeyResponseModel> PutYubiKey([FromBody] UpdateTwoFactorYubicoOtpRequestModel model)
|
||||
{
|
||||
var user = await CheckAsync(model, true);
|
||||
@@ -174,6 +180,13 @@ public class TwoFactorController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("yubikey")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /yubikey instead.")]
|
||||
public async Task<TwoFactorYubiKeyResponseModel> PostYubiKey([FromBody] UpdateTwoFactorYubicoOtpRequestModel model)
|
||||
{
|
||||
return await PutYubiKey(model);
|
||||
}
|
||||
|
||||
[HttpPost("get-duo")]
|
||||
public async Task<TwoFactorDuoResponseModel> GetDuo([FromBody] SecretVerificationRequestModel model)
|
||||
{
|
||||
@@ -183,7 +196,6 @@ public class TwoFactorController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("duo")]
|
||||
[HttpPost("duo")]
|
||||
public async Task<TwoFactorDuoResponseModel> PutDuo([FromBody] UpdateTwoFactorDuoRequestModel model)
|
||||
{
|
||||
var user = await CheckAsync(model, true);
|
||||
@@ -199,6 +211,13 @@ public class TwoFactorController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("duo")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /duo instead.")]
|
||||
public async Task<TwoFactorDuoResponseModel> PostDuo([FromBody] UpdateTwoFactorDuoRequestModel model)
|
||||
{
|
||||
return await PutDuo(model);
|
||||
}
|
||||
|
||||
[HttpPost("~/organizations/{id}/two-factor/get-duo")]
|
||||
public async Task<TwoFactorDuoResponseModel> GetOrganizationDuo(string id,
|
||||
[FromBody] SecretVerificationRequestModel model)
|
||||
@@ -217,7 +236,6 @@ public class TwoFactorController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("~/organizations/{id}/two-factor/duo")]
|
||||
[HttpPost("~/organizations/{id}/two-factor/duo")]
|
||||
public async Task<TwoFactorDuoResponseModel> PutOrganizationDuo(string id,
|
||||
[FromBody] UpdateTwoFactorDuoRequestModel model)
|
||||
{
|
||||
@@ -243,6 +261,14 @@ public class TwoFactorController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("~/organizations/{id}/two-factor/duo")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /organizations/{id}/two-factor/duo instead.")]
|
||||
public async Task<TwoFactorDuoResponseModel> PostOrganizationDuo(string id,
|
||||
[FromBody] UpdateTwoFactorDuoRequestModel model)
|
||||
{
|
||||
return await PutOrganizationDuo(id, model);
|
||||
}
|
||||
|
||||
[HttpPost("get-webauthn")]
|
||||
public async Task<TwoFactorWebAuthnResponseModel> GetWebAuthn([FromBody] SecretVerificationRequestModel model)
|
||||
{
|
||||
@@ -261,7 +287,6 @@ public class TwoFactorController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("webauthn")]
|
||||
[HttpPost("webauthn")]
|
||||
public async Task<TwoFactorWebAuthnResponseModel> PutWebAuthn([FromBody] TwoFactorWebAuthnRequestModel model)
|
||||
{
|
||||
var user = await CheckAsync(model, false);
|
||||
@@ -277,6 +302,13 @@ public class TwoFactorController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("webauthn")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /webauthn instead.")]
|
||||
public async Task<TwoFactorWebAuthnResponseModel> PostWebAuthn([FromBody] TwoFactorWebAuthnRequestModel model)
|
||||
{
|
||||
return await PutWebAuthn(model);
|
||||
}
|
||||
|
||||
[HttpDelete("webauthn")]
|
||||
public async Task<TwoFactorWebAuthnResponseModel> DeleteWebAuthn(
|
||||
[FromBody] TwoFactorWebAuthnDeleteRequestModel model)
|
||||
@@ -349,7 +381,6 @@ public class TwoFactorController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("email")]
|
||||
[HttpPost("email")]
|
||||
public async Task<TwoFactorEmailResponseModel> PutEmail([FromBody] UpdateTwoFactorEmailRequestModel model)
|
||||
{
|
||||
var user = await CheckAsync(model, false);
|
||||
@@ -367,8 +398,14 @@ public class TwoFactorController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("email")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /email instead.")]
|
||||
public async Task<TwoFactorEmailResponseModel> PostEmail([FromBody] UpdateTwoFactorEmailRequestModel model)
|
||||
{
|
||||
return await PutEmail(model);
|
||||
}
|
||||
|
||||
[HttpPut("disable")]
|
||||
[HttpPost("disable")]
|
||||
public async Task<TwoFactorProviderResponseModel> PutDisable([FromBody] TwoFactorProviderRequestModel model)
|
||||
{
|
||||
var user = await CheckAsync(model, false);
|
||||
@@ -377,8 +414,14 @@ public class TwoFactorController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("disable")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /disable instead.")]
|
||||
public async Task<TwoFactorProviderResponseModel> PostDisable([FromBody] TwoFactorProviderRequestModel model)
|
||||
{
|
||||
return await PutDisable(model);
|
||||
}
|
||||
|
||||
[HttpPut("~/organizations/{id}/two-factor/disable")]
|
||||
[HttpPost("~/organizations/{id}/two-factor/disable")]
|
||||
public async Task<TwoFactorProviderResponseModel> PutOrganizationDisable(string id,
|
||||
[FromBody] TwoFactorProviderRequestModel model)
|
||||
{
|
||||
@@ -401,6 +444,14 @@ public class TwoFactorController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("~/organizations/{id}/two-factor/disable")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /organizations/{id}/two-factor/disable instead.")]
|
||||
public async Task<TwoFactorProviderResponseModel> PostOrganizationDisable(string id,
|
||||
[FromBody] TwoFactorProviderRequestModel model)
|
||||
{
|
||||
return await PutOrganizationDisable(id, model);
|
||||
}
|
||||
|
||||
[HttpPost("get-recover")]
|
||||
public async Task<TwoFactorRecoverResponseModel> GetRecover([FromBody] SecretVerificationRequestModel model)
|
||||
{
|
||||
@@ -409,21 +460,6 @@ public class TwoFactorController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// To be removed when the feature flag pm-17128-recovery-code-login is removed PM-18175.
|
||||
/// </summary>
|
||||
[Obsolete("Two Factor recovery is handled in the TwoFactorAuthenticationValidator.")]
|
||||
[HttpPost("recover")]
|
||||
[AllowAnonymous]
|
||||
public async Task PostRecover([FromBody] TwoFactorRecoveryRequestModel model)
|
||||
{
|
||||
if (!await _userService.RecoverTwoFactorAsync(model.Email, model.MasterPasswordHash, model.RecoveryCode))
|
||||
{
|
||||
await Task.Delay(2000);
|
||||
throw new BadRequestException(string.Empty, "Invalid information. Try again.");
|
||||
}
|
||||
}
|
||||
|
||||
[Obsolete("Leaving this for backwards compatibility on clients")]
|
||||
[HttpGet("get-device-verification-settings")]
|
||||
public Task<DeviceVerificationResponseModel> GetDeviceVerificationSettings()
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
using Bit.Api.Utilities;
|
||||
|
||||
namespace Bit.Api.Billing.Attributes;
|
||||
|
||||
public class PaymentMethodTypeValidationAttribute : StringMatchesAttribute
|
||||
{
|
||||
private static readonly string[] _acceptedValues = ["bankAccount", "card", "payPal"];
|
||||
|
||||
public PaymentMethodTypeValidationAttribute() : base(_acceptedValues)
|
||||
{
|
||||
ErrorMessage = $"Payment method type must be one of: {string.Join(", ", _acceptedValues)}";
|
||||
}
|
||||
}
|
||||
@@ -304,7 +304,7 @@ public class OrganizationBillingController(
|
||||
sale.Organization.UsePolicies = plan.HasPolicies;
|
||||
sale.Organization.UseSso = plan.HasSso;
|
||||
sale.Organization.UseResetPassword = plan.HasResetPassword;
|
||||
sale.Organization.UseKeyConnector = plan.HasKeyConnector;
|
||||
sale.Organization.UseKeyConnector = plan.HasKeyConnector ? organization.UseKeyConnector : false;
|
||||
sale.Organization.UseScim = plan.HasScim;
|
||||
sale.Organization.UseCustomPermissions = plan.HasCustomPermissions;
|
||||
sale.Organization.UseOrganizationDomains = plan.HasOrganizationDomains;
|
||||
|
||||
@@ -208,7 +208,6 @@ public class OrganizationSponsorshipsController : Controller
|
||||
|
||||
[Authorize("Application")]
|
||||
[HttpDelete("{sponsoringOrganizationId}")]
|
||||
[HttpPost("{sponsoringOrganizationId}/delete")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task RevokeSponsorship(Guid sponsoringOrganizationId)
|
||||
{
|
||||
@@ -225,6 +224,15 @@ public class OrganizationSponsorshipsController : Controller
|
||||
await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);
|
||||
}
|
||||
|
||||
[Authorize("Application")]
|
||||
[HttpPost("{sponsoringOrganizationId}/delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE /{sponsoringOrganizationId} instead.")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PostRevokeSponsorship(Guid sponsoringOrganizationId)
|
||||
{
|
||||
await RevokeSponsorship(sponsoringOrganizationId);
|
||||
}
|
||||
|
||||
[Authorize("Application")]
|
||||
[HttpDelete("{sponsoringOrgId}/{sponsoredFriendlyName}/revoke")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
@@ -241,7 +249,6 @@ public class OrganizationSponsorshipsController : Controller
|
||||
|
||||
[Authorize("Application")]
|
||||
[HttpDelete("sponsored/{sponsoredOrgId}")]
|
||||
[HttpPost("sponsored/{sponsoredOrgId}/remove")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task RemoveSponsorship(Guid sponsoredOrgId)
|
||||
{
|
||||
@@ -257,6 +264,15 @@ public class OrganizationSponsorshipsController : Controller
|
||||
await _removeSponsorshipCommand.RemoveSponsorshipAsync(existingOrgSponsorship);
|
||||
}
|
||||
|
||||
[Authorize("Application")]
|
||||
[HttpPost("sponsored/{sponsoredOrgId}/remove")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE /sponsored/{sponsoredOrgId} instead.")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public async Task PostRemoveSponsorship(Guid sponsoredOrgId)
|
||||
{
|
||||
await RemoveSponsorship(sponsoredOrgId);
|
||||
}
|
||||
|
||||
[HttpGet("{sponsoringOrgId}/sync-status")]
|
||||
public async Task<object> GetSyncStatus(Guid sponsoringOrgId)
|
||||
{
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using Bit.Api.Billing.Models.Requests;
|
||||
using Bit.Api.Billing.Models.Responses;
|
||||
using Bit.Commercial.Core.Billing.Providers.Services;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Pricing;
|
||||
using Bit.Core.Billing.Providers.Models;
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
#nullable enable
|
||||
using Bit.Api.Billing.Attributes;
|
||||
using Bit.Api.Billing.Models.Requests.Payment;
|
||||
using Bit.Api.Billing.Models.Requests.Premium;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Billing.Payment.Commands;
|
||||
using Bit.Core.Billing.Payment.Queries;
|
||||
using Bit.Core.Billing.Premium.Commands;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
@@ -16,6 +19,7 @@ namespace Bit.Api.Billing.Controllers.VNext;
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
public class AccountBillingVNextController(
|
||||
ICreateBitPayInvoiceForCreditCommand createBitPayInvoiceForCreditCommand,
|
||||
ICreatePremiumCloudHostedSubscriptionCommand createPremiumCloudHostedSubscriptionCommand,
|
||||
IGetCreditQuery getCreditQuery,
|
||||
IGetPaymentMethodQuery getPaymentMethodQuery,
|
||||
IUpdatePaymentMethodCommand updatePaymentMethodCommand) : BaseBillingController
|
||||
@@ -61,4 +65,17 @@ public class AccountBillingVNextController(
|
||||
var result = await updatePaymentMethodCommand.Run(user, paymentMethod, billingAddress);
|
||||
return Handle(result);
|
||||
}
|
||||
|
||||
[HttpPost("subscription")]
|
||||
[RequireFeature(FeatureFlagKeys.PM23385_UseNewPremiumFlow)]
|
||||
[InjectUser]
|
||||
public async Task<IResult> CreateSubscriptionAsync(
|
||||
[BindNever] User user,
|
||||
[FromBody] PremiumCloudHostedSubscriptionRequest request)
|
||||
{
|
||||
var (paymentMethod, billingAddress, additionalStorageGb) = request.ToDomain();
|
||||
var result = await createPremiumCloudHostedSubscriptionCommand.Run(
|
||||
user, paymentMethod, billingAddress, additionalStorageGb);
|
||||
return Handle(result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,7 @@ public class OrganizationBillingVNextController(
|
||||
IGetOrganizationWarningsQuery getOrganizationWarningsQuery,
|
||||
IGetPaymentMethodQuery getPaymentMethodQuery,
|
||||
IUpdateBillingAddressCommand updateBillingAddressCommand,
|
||||
IUpdatePaymentMethodCommand updatePaymentMethodCommand,
|
||||
IVerifyBankAccountCommand verifyBankAccountCommand) : BaseBillingController
|
||||
IUpdatePaymentMethodCommand updatePaymentMethodCommand) : BaseBillingController
|
||||
{
|
||||
[Authorize<ManageOrganizationBillingRequirement>]
|
||||
[HttpGet("address")]
|
||||
@@ -96,17 +95,6 @@ public class OrganizationBillingVNextController(
|
||||
return Handle(result);
|
||||
}
|
||||
|
||||
[Authorize<ManageOrganizationBillingRequirement>]
|
||||
[HttpPost("payment-method/verify-bank-account")]
|
||||
[InjectOrganization]
|
||||
public async Task<IResult> VerifyBankAccountAsync(
|
||||
[BindNever] Organization organization,
|
||||
[FromBody] VerifyBankAccountRequest request)
|
||||
{
|
||||
var result = await verifyBankAccountCommand.Run(organization, request.DescriptorCode);
|
||||
return Handle(result);
|
||||
}
|
||||
|
||||
[Authorize<MemberOrProviderRequirement>]
|
||||
[HttpGet("warnings")]
|
||||
[InjectOrganization]
|
||||
|
||||
@@ -23,8 +23,7 @@ public class ProviderBillingVNextController(
|
||||
IGetProviderWarningsQuery getProviderWarningsQuery,
|
||||
IProviderService providerService,
|
||||
IUpdateBillingAddressCommand updateBillingAddressCommand,
|
||||
IUpdatePaymentMethodCommand updatePaymentMethodCommand,
|
||||
IVerifyBankAccountCommand verifyBankAccountCommand) : BaseBillingController
|
||||
IUpdatePaymentMethodCommand updatePaymentMethodCommand) : BaseBillingController
|
||||
{
|
||||
[HttpGet("address")]
|
||||
[InjectProvider(ProviderUserType.ProviderAdmin)]
|
||||
@@ -97,16 +96,6 @@ public class ProviderBillingVNextController(
|
||||
return Handle(result);
|
||||
}
|
||||
|
||||
[HttpPost("payment-method/verify-bank-account")]
|
||||
[InjectProvider(ProviderUserType.ProviderAdmin)]
|
||||
public async Task<IResult> VerifyBankAccountAsync(
|
||||
[BindNever] Provider provider,
|
||||
[FromBody] VerifyBankAccountRequest request)
|
||||
{
|
||||
var result = await verifyBankAccountCommand.Run(provider, request.DescriptorCode);
|
||||
return Handle(result);
|
||||
}
|
||||
|
||||
[HttpGet("warnings")]
|
||||
[InjectProvider(ProviderUserType.ServiceUser)]
|
||||
public async Task<IResult> GetWarningsAsync(
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#nullable enable
|
||||
using Bit.Api.Billing.Attributes;
|
||||
using Bit.Api.Billing.Models.Requests.Premium;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Billing.Models.Business;
|
||||
using Bit.Core.Billing.Premium.Commands;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||
|
||||
namespace Bit.Api.Billing.Controllers.VNext;
|
||||
|
||||
[Authorize("Application")]
|
||||
[Route("account/billing/vnext/self-host")]
|
||||
[SelfHosted(SelfHostedOnly = true)]
|
||||
public class SelfHostedAccountBillingController(
|
||||
ICreatePremiumSelfHostedSubscriptionCommand createPremiumSelfHostedSubscriptionCommand) : BaseBillingController
|
||||
{
|
||||
[HttpPost("license")]
|
||||
[RequireFeature(FeatureFlagKeys.PM23385_UseNewPremiumFlow)]
|
||||
[InjectUser]
|
||||
public async Task<IResult> UploadLicenseAsync(
|
||||
[BindNever] User user,
|
||||
PremiumSelfHostedSubscriptionRequest request)
|
||||
{
|
||||
var license = await ApiHelpers.ReadJsonFileFromBody<UserLicense>(HttpContext, request.License);
|
||||
if (license == null)
|
||||
{
|
||||
throw new BadRequestException("Invalid license.");
|
||||
}
|
||||
var result = await createPremiumSelfHostedSubscriptionCommand.Run(user, license);
|
||||
return Handle(result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
#nullable enable
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Api.Billing.Attributes;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Requests.Payment;
|
||||
|
||||
public class MinimalTokenizedPaymentMethodRequest
|
||||
{
|
||||
[Required]
|
||||
[PaymentMethodTypeValidation]
|
||||
public required string Type { get; set; }
|
||||
|
||||
[Required]
|
||||
public required string Token { get; set; }
|
||||
|
||||
public TokenizedPaymentMethod ToDomain()
|
||||
{
|
||||
return new TokenizedPaymentMethod
|
||||
{
|
||||
Type = TokenizablePaymentMethodTypeExtensions.From(Type),
|
||||
Token = Token
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
#nullable enable
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Api.Utilities;
|
||||
using Bit.Api.Billing.Attributes;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Requests.Payment;
|
||||
@@ -8,8 +8,7 @@ namespace Bit.Api.Billing.Models.Requests.Payment;
|
||||
public class TokenizedPaymentMethodRequest
|
||||
{
|
||||
[Required]
|
||||
[StringMatches("bankAccount", "card", "payPal",
|
||||
ErrorMessage = "Payment method type must be one of: bankAccount, card, payPal")]
|
||||
[PaymentMethodTypeValidation]
|
||||
public required string Type { get; set; }
|
||||
|
||||
[Required]
|
||||
@@ -21,14 +20,7 @@ public class TokenizedPaymentMethodRequest
|
||||
{
|
||||
var paymentMethod = new TokenizedPaymentMethod
|
||||
{
|
||||
Type = Type switch
|
||||
{
|
||||
"bankAccount" => TokenizablePaymentMethodType.BankAccount,
|
||||
"card" => TokenizablePaymentMethodType.Card,
|
||||
"payPal" => TokenizablePaymentMethodType.PayPal,
|
||||
_ => throw new InvalidOperationException(
|
||||
$"Invalid value for {nameof(TokenizedPaymentMethod)}.{nameof(TokenizedPaymentMethod.Type)}")
|
||||
},
|
||||
Type = TokenizablePaymentMethodTypeExtensions.From(Type),
|
||||
Token = Token
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
#nullable enable
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Api.Billing.Models.Requests.Payment;
|
||||
using Bit.Core.Billing.Payment.Models;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Requests.Premium;
|
||||
|
||||
public class PremiumCloudHostedSubscriptionRequest
|
||||
{
|
||||
[Required]
|
||||
public required MinimalTokenizedPaymentMethodRequest TokenizedPaymentMethod { get; set; }
|
||||
|
||||
[Required]
|
||||
public required MinimalBillingAddressRequest BillingAddress { get; set; }
|
||||
|
||||
[Range(0, 99)]
|
||||
public short AdditionalStorageGb { get; set; } = 0;
|
||||
|
||||
public (TokenizedPaymentMethod, BillingAddress, short) ToDomain()
|
||||
{
|
||||
var paymentMethod = TokenizedPaymentMethod.ToDomain();
|
||||
var billingAddress = BillingAddress.ToDomain();
|
||||
|
||||
return (paymentMethod, billingAddress, AdditionalStorageGb);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
#nullable enable
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Bit.Api.Billing.Models.Requests.Premium;
|
||||
|
||||
public class PremiumSelfHostedSubscriptionRequest
|
||||
{
|
||||
[Required]
|
||||
public required IFormFile License { get; set; }
|
||||
}
|
||||
@@ -4,7 +4,7 @@ using Bit.Core.Billing.Tax.Models;
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
|
||||
public record PaymentMethodResponse(
|
||||
long AccountCredit,
|
||||
decimal AccountCredit,
|
||||
PaymentSource PaymentSource,
|
||||
string SubscriptionStatus,
|
||||
TaxInformation TaxInformation)
|
||||
|
||||
@@ -102,7 +102,7 @@ public class CollectionsController : Controller
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<CollectionResponseModel>> Get(Guid orgId)
|
||||
public async Task<ListResponseModel<CollectionResponseModel>> GetAll(Guid orgId)
|
||||
{
|
||||
IEnumerable<Collection> orgCollections;
|
||||
|
||||
@@ -173,7 +173,6 @@ public class CollectionsController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[HttpPost("{id}")]
|
||||
public async Task<CollectionResponseModel> Put(Guid orgId, Guid id, [FromBody] UpdateCollectionRequestModel model)
|
||||
{
|
||||
var collection = await _collectionRepository.GetByIdAsync(id);
|
||||
@@ -198,6 +197,13 @@ public class CollectionsController : Controller
|
||||
return new CollectionAccessDetailsResponseModel(collectionWithPermissions);
|
||||
}
|
||||
|
||||
[HttpPost("{id}")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /{id} instead.")]
|
||||
public async Task<CollectionResponseModel> Post(Guid orgId, Guid id, [FromBody] UpdateCollectionRequestModel model)
|
||||
{
|
||||
return await Put(orgId, id, model);
|
||||
}
|
||||
|
||||
[HttpPost("bulk-access")]
|
||||
public async Task PostBulkCollectionAccess(Guid orgId, [FromBody] BulkCollectionAccessRequestModel model)
|
||||
{
|
||||
@@ -222,7 +228,6 @@ public class CollectionsController : Controller
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpPost("{id}/delete")]
|
||||
public async Task Delete(Guid orgId, Guid id)
|
||||
{
|
||||
var collection = await _collectionRepository.GetByIdAsync(id);
|
||||
@@ -235,8 +240,14 @@ public class CollectionsController : Controller
|
||||
await _deleteCollectionCommand.DeleteAsync(collection);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE /{id} instead.")]
|
||||
public async Task PostDelete(Guid orgId, Guid id)
|
||||
{
|
||||
await Delete(orgId, id);
|
||||
}
|
||||
|
||||
[HttpDelete("")]
|
||||
[HttpPost("delete")]
|
||||
public async Task DeleteMany(Guid orgId, [FromBody] CollectionBulkDeleteRequestModel model)
|
||||
{
|
||||
var collections = await _collectionRepository.GetManyByManyIdsAsync(model.Ids);
|
||||
@@ -248,4 +259,11 @@ public class CollectionsController : Controller
|
||||
|
||||
await _deleteCollectionCommand.DeleteManyAsync(collections);
|
||||
}
|
||||
|
||||
[HttpPost("delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE / instead.")]
|
||||
public async Task PostDeleteMany(Guid orgId, [FromBody] CollectionBulkDeleteRequestModel model)
|
||||
{
|
||||
await DeleteMany(orgId, model);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ public class DevicesController : Controller
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<DeviceAuthRequestResponseModel>> Get()
|
||||
public async Task<ListResponseModel<DeviceAuthRequestResponseModel>> GetAll()
|
||||
{
|
||||
var devicesWithPendingAuthData = await _deviceRepository.GetManyByUserIdWithDeviceAuth(_userService.GetProperUserId(User).Value);
|
||||
|
||||
@@ -99,7 +99,6 @@ public class DevicesController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[HttpPost("{id}")]
|
||||
public async Task<DeviceResponseModel> Put(string id, [FromBody] DeviceRequestModel model)
|
||||
{
|
||||
var device = await _deviceRepository.GetByIdAsync(new Guid(id), _userService.GetProperUserId(User).Value);
|
||||
@@ -114,8 +113,14 @@ public class DevicesController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("{id}")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /{id} instead.")]
|
||||
public async Task<DeviceResponseModel> Post(string id, [FromBody] DeviceRequestModel model)
|
||||
{
|
||||
return await Put(id, model);
|
||||
}
|
||||
|
||||
[HttpPut("{identifier}/keys")]
|
||||
[HttpPost("{identifier}/keys")]
|
||||
public async Task<DeviceResponseModel> PutKeys(string identifier, [FromBody] DeviceKeysRequestModel model)
|
||||
{
|
||||
var device = await _deviceRepository.GetByIdentifierAsync(identifier, _userService.GetProperUserId(User).Value);
|
||||
@@ -130,6 +135,13 @@ public class DevicesController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("{identifier}/keys")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /{identifier}/keys instead.")]
|
||||
public async Task<DeviceResponseModel> PostKeys(string identifier, [FromBody] DeviceKeysRequestModel model)
|
||||
{
|
||||
return await PutKeys(identifier, model);
|
||||
}
|
||||
|
||||
[HttpPost("{identifier}/retrieve-keys")]
|
||||
[Obsolete("This endpoint is deprecated. The keys are on the regular device GET endpoints now.")]
|
||||
public async Task<ProtectedDeviceResponseModel> GetDeviceKeys(string identifier)
|
||||
@@ -187,7 +199,6 @@ public class DevicesController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("identifier/{identifier}/token")]
|
||||
[HttpPost("identifier/{identifier}/token")]
|
||||
public async Task PutToken(string identifier, [FromBody] DeviceTokenRequestModel model)
|
||||
{
|
||||
var device = await _deviceRepository.GetByIdentifierAsync(identifier, _userService.GetProperUserId(User).Value);
|
||||
@@ -199,8 +210,14 @@ public class DevicesController : Controller
|
||||
await _deviceService.SaveAsync(model.ToDevice(device));
|
||||
}
|
||||
|
||||
[HttpPost("identifier/{identifier}/token")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /identifier/{identifier}/token instead.")]
|
||||
public async Task PostToken(string identifier, [FromBody] DeviceTokenRequestModel model)
|
||||
{
|
||||
await PutToken(identifier, model);
|
||||
}
|
||||
|
||||
[HttpPut("identifier/{identifier}/web-push-auth")]
|
||||
[HttpPost("identifier/{identifier}/web-push-auth")]
|
||||
public async Task PutWebPushAuth(string identifier, [FromBody] WebPushAuthRequestModel model)
|
||||
{
|
||||
var device = await _deviceRepository.GetByIdentifierAsync(identifier, _userService.GetProperUserId(User).Value);
|
||||
@@ -216,9 +233,15 @@ public class DevicesController : Controller
|
||||
);
|
||||
}
|
||||
|
||||
[HttpPost("identifier/{identifier}/web-push-auth")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /identifier/{identifier}/web-push-auth instead.")]
|
||||
public async Task PostWebPushAuth(string identifier, [FromBody] WebPushAuthRequestModel model)
|
||||
{
|
||||
await PutWebPushAuth(identifier, model);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPut("identifier/{identifier}/clear-token")]
|
||||
[HttpPost("identifier/{identifier}/clear-token")]
|
||||
public async Task PutClearToken(string identifier)
|
||||
{
|
||||
var device = await _deviceRepository.GetByIdentifierAsync(identifier);
|
||||
@@ -230,8 +253,15 @@ public class DevicesController : Controller
|
||||
await _deviceService.ClearTokenAsync(device);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("identifier/{identifier}/clear-token")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /identifier/{identifier}/clear-token instead.")]
|
||||
public async Task PostClearToken(string identifier)
|
||||
{
|
||||
await PutClearToken(identifier);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpPost("{id}/deactivate")]
|
||||
public async Task Deactivate(string id)
|
||||
{
|
||||
var device = await _deviceRepository.GetByIdAsync(new Guid(id), _userService.GetProperUserId(User).Value);
|
||||
@@ -243,17 +273,24 @@ public class DevicesController : Controller
|
||||
await _deviceService.DeactivateAsync(device);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/deactivate")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE /{id} instead.")]
|
||||
public async Task PostDeactivate(string id)
|
||||
{
|
||||
await Deactivate(id);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpGet("knowndevice")]
|
||||
public async Task<bool> GetByIdentifierQuery(
|
||||
[Required][FromHeader(Name = "X-Request-Email")] string Email,
|
||||
[Required][FromHeader(Name = "X-Device-Identifier")] string DeviceIdentifier)
|
||||
=> await GetByIdentifier(CoreHelpers.Base64UrlDecodeString(Email), DeviceIdentifier);
|
||||
=> await GetByEmailAndIdentifier(CoreHelpers.Base64UrlDecodeString(Email), DeviceIdentifier);
|
||||
|
||||
[Obsolete("Path is deprecated due to encoding issues, use /knowndevice instead.")]
|
||||
[AllowAnonymous]
|
||||
[HttpGet("knowndevice/{email}/{identifier}")]
|
||||
public async Task<bool> GetByIdentifier(string email, string identifier)
|
||||
public async Task<bool> GetByEmailAndIdentifier(string email, string identifier)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(email) || string.IsNullOrWhiteSpace(identifier))
|
||||
{
|
||||
|
||||
@@ -6,12 +6,18 @@ namespace Bit.Api.Controllers;
|
||||
public class InfoController : Controller
|
||||
{
|
||||
[HttpGet("~/alive")]
|
||||
[HttpGet("~/now")]
|
||||
public DateTime GetAlive()
|
||||
{
|
||||
return DateTime.UtcNow;
|
||||
}
|
||||
|
||||
[HttpGet("~/now")]
|
||||
[Obsolete("This endpoint is deprecated. Use GET /alive instead.")]
|
||||
public DateTime GetNow()
|
||||
{
|
||||
return GetAlive();
|
||||
}
|
||||
|
||||
[HttpGet("~/version")]
|
||||
public JsonResult GetVersion()
|
||||
{
|
||||
|
||||
@@ -53,7 +53,7 @@ public class SelfHostedOrganizationLicensesController : Controller
|
||||
}
|
||||
|
||||
[HttpPost("")]
|
||||
public async Task<OrganizationResponseModel> PostLicenseAsync(OrganizationCreateLicenseRequestModel model)
|
||||
public async Task<OrganizationResponseModel> CreateLicenseAsync(OrganizationCreateLicenseRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
if (user == null)
|
||||
@@ -74,7 +74,7 @@ public class SelfHostedOrganizationLicensesController : Controller
|
||||
}
|
||||
|
||||
[HttpPost("{id}")]
|
||||
public async Task PostLicenseAsync(string id, LicenseRequestModel model)
|
||||
public async Task UpdateLicenseAsync(string id, LicenseRequestModel model)
|
||||
{
|
||||
var orgIdGuid = new Guid(id);
|
||||
if (!await _currentContext.OrganizationOwner(orgIdGuid))
|
||||
|
||||
@@ -79,7 +79,6 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
|
||||
}
|
||||
|
||||
[HttpDelete("{sponsoringOrgId}")]
|
||||
[HttpPost("{sponsoringOrgId}/delete")]
|
||||
public async Task RevokeSponsorship(Guid sponsoringOrgId)
|
||||
{
|
||||
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default);
|
||||
@@ -95,6 +94,13 @@ public class SelfHostedOrganizationSponsorshipsController : Controller
|
||||
await _revokeSponsorshipCommand.RevokeSponsorshipAsync(existingOrgSponsorship);
|
||||
}
|
||||
|
||||
[HttpPost("{sponsoringOrgId}/delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE /{sponsoringOrgId} instead.")]
|
||||
public async Task PostRevokeSponsorship(Guid sponsoringOrgId)
|
||||
{
|
||||
await RevokeSponsorship(sponsoringOrgId);
|
||||
}
|
||||
|
||||
[HttpDelete("{sponsoringOrgId}/{sponsoredFriendlyName}/revoke")]
|
||||
public async Task AdminInitiatedRevokeSponsorshipAsync(Guid sponsoringOrgId, string sponsoredFriendlyName)
|
||||
{
|
||||
|
||||
@@ -32,7 +32,6 @@ public class SettingsController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("domains")]
|
||||
[HttpPost("domains")]
|
||||
public async Task<DomainsResponseModel> PutDomains([FromBody] UpdateDomainsRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
@@ -46,4 +45,11 @@ public class SettingsController : Controller
|
||||
var response = new DomainsResponseModel(user);
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("domains")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT /domains instead.")]
|
||||
public async Task<DomainsResponseModel> PostDomains([FromBody] UpdateDomainsRequestModel model)
|
||||
{
|
||||
return await PutDomains(model);
|
||||
}
|
||||
}
|
||||
|
||||
297
src/Api/Dirt/Controllers/OrganizationReportsController.cs
Normal file
297
src/Api/Dirt/Controllers/OrganizationReportsController.cs
Normal file
@@ -0,0 +1,297 @@
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures.Interfaces;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures.Requests;
|
||||
using Bit.Core.Exceptions;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace Bit.Api.Dirt.Controllers;
|
||||
|
||||
[Route("reports/organizations")]
|
||||
[Authorize("Application")]
|
||||
public class OrganizationReportsController : Controller
|
||||
{
|
||||
private readonly ICurrentContext _currentContext;
|
||||
private readonly IGetOrganizationReportQuery _getOrganizationReportQuery;
|
||||
private readonly IAddOrganizationReportCommand _addOrganizationReportCommand;
|
||||
private readonly IUpdateOrganizationReportCommand _updateOrganizationReportCommand;
|
||||
private readonly IUpdateOrganizationReportSummaryCommand _updateOrganizationReportSummaryCommand;
|
||||
private readonly IGetOrganizationReportSummaryDataQuery _getOrganizationReportSummaryDataQuery;
|
||||
private readonly IGetOrganizationReportSummaryDataByDateRangeQuery _getOrganizationReportSummaryDataByDateRangeQuery;
|
||||
private readonly IGetOrganizationReportDataQuery _getOrganizationReportDataQuery;
|
||||
private readonly IUpdateOrganizationReportDataCommand _updateOrganizationReportDataCommand;
|
||||
private readonly IGetOrganizationReportApplicationDataQuery _getOrganizationReportApplicationDataQuery;
|
||||
private readonly IUpdateOrganizationReportApplicationDataCommand _updateOrganizationReportApplicationDataCommand;
|
||||
|
||||
public OrganizationReportsController(
|
||||
ICurrentContext currentContext,
|
||||
IGetOrganizationReportQuery getOrganizationReportQuery,
|
||||
IAddOrganizationReportCommand addOrganizationReportCommand,
|
||||
IUpdateOrganizationReportCommand updateOrganizationReportCommand,
|
||||
IUpdateOrganizationReportSummaryCommand updateOrganizationReportSummaryCommand,
|
||||
IGetOrganizationReportSummaryDataQuery getOrganizationReportSummaryDataQuery,
|
||||
IGetOrganizationReportSummaryDataByDateRangeQuery getOrganizationReportSummaryDataByDateRangeQuery,
|
||||
IGetOrganizationReportDataQuery getOrganizationReportDataQuery,
|
||||
IUpdateOrganizationReportDataCommand updateOrganizationReportDataCommand,
|
||||
IGetOrganizationReportApplicationDataQuery getOrganizationReportApplicationDataQuery,
|
||||
IUpdateOrganizationReportApplicationDataCommand updateOrganizationReportApplicationDataCommand
|
||||
)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
_getOrganizationReportQuery = getOrganizationReportQuery;
|
||||
_addOrganizationReportCommand = addOrganizationReportCommand;
|
||||
_updateOrganizationReportCommand = updateOrganizationReportCommand;
|
||||
_updateOrganizationReportSummaryCommand = updateOrganizationReportSummaryCommand;
|
||||
_getOrganizationReportSummaryDataQuery = getOrganizationReportSummaryDataQuery;
|
||||
_getOrganizationReportSummaryDataByDateRangeQuery = getOrganizationReportSummaryDataByDateRangeQuery;
|
||||
_getOrganizationReportDataQuery = getOrganizationReportDataQuery;
|
||||
_updateOrganizationReportDataCommand = updateOrganizationReportDataCommand;
|
||||
_getOrganizationReportApplicationDataQuery = getOrganizationReportApplicationDataQuery;
|
||||
_updateOrganizationReportApplicationDataCommand = updateOrganizationReportApplicationDataCommand;
|
||||
}
|
||||
|
||||
#region Whole OrganizationReport Endpoints
|
||||
|
||||
[HttpGet("{organizationId}/latest")]
|
||||
public async Task<IActionResult> GetLatestOrganizationReportAsync(Guid organizationId)
|
||||
{
|
||||
if (!await _currentContext.AccessReports(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var latestReport = await _getOrganizationReportQuery.GetLatestOrganizationReportAsync(organizationId);
|
||||
|
||||
return Ok(latestReport);
|
||||
}
|
||||
|
||||
[HttpGet("{organizationId}/{reportId}")]
|
||||
public async Task<IActionResult> GetOrganizationReportAsync(Guid organizationId, Guid reportId)
|
||||
{
|
||||
if (!await _currentContext.AccessReports(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var report = await _getOrganizationReportQuery.GetOrganizationReportAsync(reportId);
|
||||
|
||||
if (report == null)
|
||||
{
|
||||
throw new NotFoundException("Report not found for the specified organization.");
|
||||
}
|
||||
|
||||
if (report.OrganizationId != organizationId)
|
||||
{
|
||||
throw new BadRequestException("Invalid report ID");
|
||||
}
|
||||
|
||||
return Ok(report);
|
||||
}
|
||||
|
||||
[HttpPost("{organizationId}")]
|
||||
public async Task<IActionResult> CreateOrganizationReportAsync(Guid organizationId, [FromBody] AddOrganizationReportRequest request)
|
||||
{
|
||||
if (!await _currentContext.AccessReports(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (request.OrganizationId != organizationId)
|
||||
{
|
||||
throw new BadRequestException("Organization ID in the request body must match the route parameter");
|
||||
}
|
||||
|
||||
var report = await _addOrganizationReportCommand.AddOrganizationReportAsync(request);
|
||||
return Ok(report);
|
||||
}
|
||||
|
||||
[HttpPatch("{organizationId}/{reportId}")]
|
||||
public async Task<IActionResult> UpdateOrganizationReportAsync(Guid organizationId, [FromBody] UpdateOrganizationReportRequest request)
|
||||
{
|
||||
if (!await _currentContext.AccessReports(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (request.OrganizationId != organizationId)
|
||||
{
|
||||
throw new BadRequestException("Organization ID in the request body must match the route parameter");
|
||||
}
|
||||
|
||||
var updatedReport = await _updateOrganizationReportCommand.UpdateOrganizationReportAsync(request);
|
||||
return Ok(updatedReport);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
# region SummaryData Field Endpoints
|
||||
|
||||
[HttpGet("{organizationId}/data/summary")]
|
||||
public async Task<IActionResult> GetOrganizationReportSummaryDataByDateRangeAsync(
|
||||
Guid organizationId, [FromQuery] DateTime startDate, [FromQuery] DateTime endDate)
|
||||
{
|
||||
if (!await _currentContext.AccessReports(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (organizationId.Equals(null))
|
||||
{
|
||||
throw new BadRequestException("Organization ID is required.");
|
||||
}
|
||||
|
||||
var summaryDataList = await _getOrganizationReportSummaryDataByDateRangeQuery
|
||||
.GetOrganizationReportSummaryDataByDateRangeAsync(organizationId, startDate, endDate);
|
||||
|
||||
return Ok(summaryDataList);
|
||||
}
|
||||
|
||||
[HttpGet("{organizationId}/data/summary/{reportId}")]
|
||||
public async Task<IActionResult> GetOrganizationReportSummaryAsync(Guid organizationId, Guid reportId)
|
||||
{
|
||||
if (!await _currentContext.AccessReports(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var summaryData =
|
||||
await _getOrganizationReportSummaryDataQuery.GetOrganizationReportSummaryDataAsync(organizationId, reportId);
|
||||
|
||||
if (summaryData == null)
|
||||
{
|
||||
throw new NotFoundException("Report not found for the specified organization.");
|
||||
}
|
||||
|
||||
return Ok(summaryData);
|
||||
}
|
||||
|
||||
[HttpPatch("{organizationId}/data/summary/{reportId}")]
|
||||
public async Task<IActionResult> UpdateOrganizationReportSummaryAsync(Guid organizationId, Guid reportId, [FromBody] UpdateOrganizationReportSummaryRequest request)
|
||||
{
|
||||
if (!await _currentContext.AccessReports(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (request.OrganizationId != organizationId)
|
||||
{
|
||||
throw new BadRequestException("Organization ID in the request body must match the route parameter");
|
||||
}
|
||||
|
||||
if (request.ReportId != reportId)
|
||||
{
|
||||
throw new BadRequestException("Report ID in the request body must match the route parameter");
|
||||
}
|
||||
|
||||
var updatedReport = await _updateOrganizationReportSummaryCommand.UpdateOrganizationReportSummaryAsync(request);
|
||||
|
||||
return Ok(updatedReport);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region ReportData Field Endpoints
|
||||
|
||||
[HttpGet("{organizationId}/data/report/{reportId}")]
|
||||
public async Task<IActionResult> GetOrganizationReportDataAsync(Guid organizationId, Guid reportId)
|
||||
{
|
||||
if (!await _currentContext.AccessReports(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var reportData = await _getOrganizationReportDataQuery.GetOrganizationReportDataAsync(organizationId, reportId);
|
||||
|
||||
if (reportData == null)
|
||||
{
|
||||
throw new NotFoundException("Organization report data not found.");
|
||||
}
|
||||
|
||||
return Ok(reportData);
|
||||
}
|
||||
|
||||
[HttpPatch("{organizationId}/data/report/{reportId}")]
|
||||
public async Task<IActionResult> UpdateOrganizationReportDataAsync(Guid organizationId, Guid reportId, [FromBody] UpdateOrganizationReportDataRequest request)
|
||||
{
|
||||
if (!await _currentContext.AccessReports(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (request.OrganizationId != organizationId)
|
||||
{
|
||||
throw new BadRequestException("Organization ID in the request body must match the route parameter");
|
||||
}
|
||||
|
||||
if (request.ReportId != reportId)
|
||||
{
|
||||
throw new BadRequestException("Report ID in the request body must match the route parameter");
|
||||
}
|
||||
|
||||
var updatedReport = await _updateOrganizationReportDataCommand.UpdateOrganizationReportDataAsync(request);
|
||||
return Ok(updatedReport);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ApplicationData Field Endpoints
|
||||
|
||||
[HttpGet("{organizationId}/data/application/{reportId}")]
|
||||
public async Task<IActionResult> GetOrganizationReportApplicationDataAsync(Guid organizationId, Guid reportId)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!await _currentContext.AccessReports(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var applicationData = await _getOrganizationReportApplicationDataQuery.GetOrganizationReportApplicationDataAsync(organizationId, reportId);
|
||||
|
||||
if (applicationData == null)
|
||||
{
|
||||
throw new NotFoundException("Organization report application data not found.");
|
||||
}
|
||||
|
||||
return Ok(applicationData);
|
||||
}
|
||||
catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException))
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPatch("{organizationId}/data/application/{reportId}")]
|
||||
public async Task<IActionResult> UpdateOrganizationReportApplicationDataAsync(Guid organizationId, Guid reportId, [FromBody] UpdateOrganizationReportApplicationDataRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
if (!await _currentContext.AccessReports(organizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
if (request.OrganizationId != organizationId)
|
||||
{
|
||||
throw new BadRequestException("Organization ID in the request body must match the route parameter");
|
||||
}
|
||||
|
||||
if (request.Id != reportId)
|
||||
{
|
||||
throw new BadRequestException("Report ID in the request body must match the route parameter");
|
||||
}
|
||||
|
||||
var updatedReport = await _updateOrganizationReportApplicationDataCommand.UpdateOrganizationReportApplicationDataAsync(request);
|
||||
|
||||
|
||||
|
||||
return Ok(updatedReport);
|
||||
}
|
||||
catch (Exception ex) when (!(ex is BadRequestException || ex is NotFoundException))
|
||||
{
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Bit.Api.Dirt.Models;
|
||||
using Bit.Api.Dirt.Models.Response;
|
||||
using Bit.Api.Tools.Models.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Dirt.Entities;
|
||||
using Bit.Core.Dirt.Reports.Models.Data;
|
||||
@@ -24,8 +25,8 @@ public class ReportsController : Controller
|
||||
private readonly IGetPasswordHealthReportApplicationQuery _getPwdHealthReportAppQuery;
|
||||
private readonly IDropPasswordHealthReportApplicationCommand _dropPwdHealthReportAppCommand;
|
||||
private readonly IAddOrganizationReportCommand _addOrganizationReportCommand;
|
||||
private readonly IDropOrganizationReportCommand _dropOrganizationReportCommand;
|
||||
private readonly IGetOrganizationReportQuery _getOrganizationReportQuery;
|
||||
private readonly ILogger<ReportsController> _logger;
|
||||
|
||||
public ReportsController(
|
||||
ICurrentContext currentContext,
|
||||
@@ -36,7 +37,7 @@ public class ReportsController : Controller
|
||||
IDropPasswordHealthReportApplicationCommand dropPwdHealthReportAppCommand,
|
||||
IGetOrganizationReportQuery getOrganizationReportQuery,
|
||||
IAddOrganizationReportCommand addOrganizationReportCommand,
|
||||
IDropOrganizationReportCommand dropOrganizationReportCommand
|
||||
ILogger<ReportsController> logger
|
||||
)
|
||||
{
|
||||
_currentContext = currentContext;
|
||||
@@ -47,7 +48,7 @@ public class ReportsController : Controller
|
||||
_dropPwdHealthReportAppCommand = dropPwdHealthReportAppCommand;
|
||||
_getOrganizationReportQuery = getOrganizationReportQuery;
|
||||
_addOrganizationReportCommand = addOrganizationReportCommand;
|
||||
_dropOrganizationReportCommand = dropOrganizationReportCommand;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -86,32 +87,24 @@ public class ReportsController : Controller
|
||||
{
|
||||
if (!await _currentContext.AccessReports(orgId))
|
||||
{
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId,
|
||||
"AccessReports Check - UserId: {userId} OrgId: {orgId} DeviceType: {deviceType}",
|
||||
_currentContext.UserId, orgId, _currentContext.DeviceType);
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var accessDetails = await GetMemberAccessDetails(new MemberAccessReportRequest { OrganizationId = orgId });
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId,
|
||||
"MemberAccessReportQuery starts - UserId: {userId} OrgId: {orgId} DeviceType: {deviceType}",
|
||||
_currentContext.UserId, orgId, _currentContext.DeviceType);
|
||||
|
||||
var accessDetails = await _memberAccessReportQuery
|
||||
.GetMemberAccessReportsAsync(new MemberAccessReportRequest { OrganizationId = orgId });
|
||||
|
||||
var responses = accessDetails.Select(x => new MemberAccessDetailReportResponseModel(x));
|
||||
|
||||
return responses;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains the organization member info, the cipher ids associated with the member,
|
||||
/// and details on their collections, groups, and permissions
|
||||
/// </summary>
|
||||
/// <param name="request">Request parameters</param>
|
||||
/// <returns>
|
||||
/// List of a user's permissions at a group and collection level as well as the number of ciphers
|
||||
/// associated with that group/collection
|
||||
/// </returns>
|
||||
private async Task<IEnumerable<MemberAccessReportDetail>> GetMemberAccessDetails(
|
||||
MemberAccessReportRequest request)
|
||||
{
|
||||
var accessDetails = await _memberAccessReportQuery.GetMemberAccessReportsAsync(request);
|
||||
return accessDetails;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the risk insights report details from the risk insights query. Associates a user to their cipher ids
|
||||
/// </summary>
|
||||
@@ -213,195 +206,4 @@ public class ReportsController : Controller
|
||||
|
||||
await _dropPwdHealthReportAppCommand.DropPasswordHealthReportApplicationAsync(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a new organization report
|
||||
/// </summary>
|
||||
/// <param name="request">A single instance of AddOrganizationReportRequest</param>
|
||||
/// <returns>A single instance of OrganizationReport</returns>
|
||||
/// <exception cref="NotFoundException">If user does not have access to the organization</exception>
|
||||
/// <exception cref="BadRequestException">If the organization Id is not valid</exception>
|
||||
[HttpPost("organization-reports")]
|
||||
public async Task<OrganizationReport> AddOrganizationReport([FromBody] AddOrganizationReportRequest request)
|
||||
{
|
||||
if (!await _currentContext.AccessReports(request.OrganizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
return await _addOrganizationReportCommand.AddOrganizationReportAsync(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drops organization reports for an organization
|
||||
/// </summary>
|
||||
/// <param name="request">A single instance of DropOrganizationReportRequest</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotFoundException">If user does not have access to the organization</exception>
|
||||
/// <exception cref="BadRequestException">If the organization does not have any records</exception>
|
||||
[HttpDelete("organization-reports")]
|
||||
public async Task DropOrganizationReport([FromBody] DropOrganizationReportRequest request)
|
||||
{
|
||||
if (!await _currentContext.AccessReports(request.OrganizationId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
await _dropOrganizationReportCommand.DropOrganizationReportAsync(request);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets organization reports for an organization
|
||||
/// </summary>
|
||||
/// <param name="orgId">A valid Organization Id</param>
|
||||
/// <returns>An Enumerable of OrganizationReport</returns>
|
||||
/// <exception cref="NotFoundException">If user does not have access to the organization</exception>
|
||||
/// <exception cref="BadRequestException">If the organization Id is not valid</exception>
|
||||
[HttpGet("organization-reports/{orgId}")]
|
||||
public async Task<IEnumerable<OrganizationReport>> GetOrganizationReports(Guid orgId)
|
||||
{
|
||||
if (!await _currentContext.AccessReports(orgId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
return await _getOrganizationReportQuery.GetOrganizationReportAsync(orgId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the latest organization report for an organization
|
||||
/// </summary>
|
||||
/// <param name="orgId">A valid Organization Id</param>
|
||||
/// <returns>A single instance of OrganizationReport</returns>
|
||||
/// <exception cref="NotFoundException">If user does not have access to the organization</exception>
|
||||
/// <exception cref="BadRequestException">If the organization Id is not valid</exception>
|
||||
[HttpGet("organization-reports/latest/{orgId}")]
|
||||
public async Task<OrganizationReport> GetLatestOrganizationReport(Guid orgId)
|
||||
{
|
||||
if (!await _currentContext.AccessReports(orgId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
return await _getOrganizationReportQuery.GetLatestOrganizationReportAsync(orgId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the Organization Report Summary for an organization.
|
||||
/// This includes the latest report's encrypted data, encryption key, and date.
|
||||
/// This is a mock implementation and should be replaced with actual data retrieval logic.
|
||||
/// </summary>
|
||||
/// <param name="orgId"></param>
|
||||
/// <param name="from">Min date (example: 2023-01-01)</param>
|
||||
/// <param name="to">Max date (example: 2023-12-31)</param>
|
||||
/// <returns></returns>
|
||||
/// <exception cref="NotFoundException"></exception>
|
||||
[HttpGet("organization-report-summary/{orgId}")]
|
||||
public IEnumerable<OrganizationReportSummaryModel> GetOrganizationReportSummary(
|
||||
[FromRoute] Guid orgId,
|
||||
[FromQuery] DateOnly from,
|
||||
[FromQuery] DateOnly to)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
GuardOrganizationAccess(orgId);
|
||||
|
||||
// FIXME: remove this mock class when actual data retrieval is implemented
|
||||
return MockOrganizationReportSummary.GetMockData()
|
||||
.Where(_ => _.OrganizationId == orgId
|
||||
&& _.Date >= from.ToDateTime(TimeOnly.MinValue)
|
||||
&& _.Date <= to.ToDateTime(TimeOnly.MaxValue));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Organization Report Summary for an organization.
|
||||
/// This is a mock implementation and should be replaced with actual creation logic.
|
||||
/// </summary>
|
||||
/// <param name="model"></param>
|
||||
/// <returns>Returns 204 Created with the created OrganizationReportSummaryModel</returns>
|
||||
/// <exception cref="NotFoundException"></exception>
|
||||
[HttpPost("organization-report-summary")]
|
||||
public IActionResult CreateOrganizationReportSummary([FromBody] OrganizationReportSummaryModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
GuardOrganizationAccess(model.OrganizationId);
|
||||
|
||||
// TODO: Implement actual creation logic
|
||||
|
||||
// Returns 204 No Content as a placeholder
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
[HttpPut("organization-report-summary")]
|
||||
public IActionResult UpdateOrganizationReportSummary([FromBody] OrganizationReportSummaryModel model)
|
||||
{
|
||||
if (!ModelState.IsValid)
|
||||
{
|
||||
throw new BadRequestException(ModelState);
|
||||
}
|
||||
|
||||
GuardOrganizationAccess(model.OrganizationId);
|
||||
|
||||
// TODO: Implement actual update logic
|
||||
|
||||
// Returns 204 No Content as a placeholder
|
||||
return NoContent();
|
||||
}
|
||||
|
||||
private void GuardOrganizationAccess(Guid organizationId)
|
||||
{
|
||||
if (!_currentContext.AccessReports(organizationId).Result)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: remove this mock class when actual data retrieval is implemented
|
||||
private class MockOrganizationReportSummary
|
||||
{
|
||||
public static List<OrganizationReportSummaryModel> GetMockData()
|
||||
{
|
||||
return new List<OrganizationReportSummaryModel>
|
||||
{
|
||||
new OrganizationReportSummaryModel
|
||||
{
|
||||
OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"),
|
||||
EncryptedData = "2.EtCcxDEBoF1MYChYHC4Q1w==|RyZ07R7qEFBbc/ICLFpEMockL9K+PD6rOod6DGHHrkaRLHUDqDwmxbu3jnD0cg8s7GIYmp0jApHXC+82QdApk87pA0Kr8fN2Rj0+8bDQCjhKfoRTipAB25S/n2E+ttjvlFfag92S66XqUH9S/eZw/Q==|0bPfykHk3SqS/biLNcNoYtH6YTstBEKu3AhvdZZLxhU=",
|
||||
EncryptionKey = "2.Dd/TtdNwxWdYg9+fRkxh6w==|8KAiK9SoadgFRmyVOchd4tNh2vErD1Rv9x1gqtsE5tzxKE/V/5kkr1WuVG+QpEj//YaQt221UEMESRSXicZ7a9cB6xXLBkbbFwmecQRJVBs=|902em44n9cwciZzYrYuX6MRzRa+4hh1HHfNAxyJx/IM=",
|
||||
Date = DateTime.UtcNow
|
||||
},
|
||||
new OrganizationReportSummaryModel
|
||||
{
|
||||
OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"),
|
||||
EncryptedData = "2.HvY4fAvbzYV1hqa3255m5Q==|WcKga2Wka5i8fVso8MgjzfBAwxaqdhZDL3bnvhDsisZ0r9lNKQcG3YUQSFpJxr74cgg5QRQaFieCUe2YppciHDT6bsaE2VzFce3cNNB821uTFqnlJClkGJpG1nGvPupdErrg4Ik57WenEzYesmR4pw==|F0aJfF+1MlPm+eAlQnDgFnwfv198N9VtPqFJa4+UFqk=",
|
||||
EncryptionKey = "2.ctMgLN4ycPusbQArG/uiag==|NtqiQsAoUxMSTBQsxAMyVLWdt5lVEUGZQNxZSBU4l76ywH2f6dx5FWFrcF3t3GBqy5yDoc5eBg0VlJDW9coqzp8j9n8h1iMrtmXPyBMAhbc=|pbH+w68BUdUKYCfNRpjd8NENw2lZ0vfxgMuTrsrRCTQ=",
|
||||
Date = DateTime.UtcNow.AddMonths(-1)
|
||||
},
|
||||
new OrganizationReportSummaryModel
|
||||
{
|
||||
OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"),
|
||||
EncryptedData = "2.NH4qLZYUkz/+qpB/mRsLTA==|LEFt05jJz0ngh+Hl5lqk6kebj7lZMefA3eFdL1kLJSGdD3uTOngRwH7GXLQNFeQOxutnLX9YUILbUEPwaM8gCwNQ1KWYdB1Z+Ky4nzKRb60N7L5aTA2za6zXTIdjv7Zwhg0jPZ6sPevTuvSyqjMCuA==|Uuu6gZaF0wvB2mHFwtvHegMxfe8DgsYWTRfGiVn4lkM=",
|
||||
EncryptionKey = "2.3YwG78ykSxAn44NcymdG4w==|4jfn0nLoFielicAFbmq27DNUUjV4SwGePnjYRmOa7hk4pEPnQRS3MsTJFbutVyXOgKFY9Yn2yGFZownY9EmXOMM+gHPD0t6TfzUKqQcRyuI=|wasP9zZEL9mFH5HzJYrMxnKUr/XlFKXCxG9uW66uaPU=",
|
||||
Date = DateTime.UtcNow.AddMonths(-1)
|
||||
},
|
||||
new OrganizationReportSummaryModel
|
||||
{
|
||||
OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"),
|
||||
EncryptedData = "2.YmKWj/707wDPONh+JXPBOw==|Fx4jcUHmnUnSMCU8vdThMSYpDyKPnC09TxpSbNxia0M6MFbd5WHElcVribrYgTENyU0HlqPW43hThJ6xXCM0EjEWP7/jb/0l07vMNkA7sDYq+czf0XnYZgZSGKh06wFVz8xkhaPTdsiO4CXuMsoH+w==|DDVwVFHzdfbPQe3ycCx82eYVHDW97V/eWTPsNpHX/+U=",
|
||||
EncryptionKey = "2.f/U45I7KF+JKfnvOArUyaw==|zNhhS2q2WwBl6SqLWMkxrXC8EX91Ra9LJExywkJhsRbxubRLt7fK+YWc8T1LUaDmMwJ3G8buSPGzyacKX0lnUR33dW6DIaLNgRZ/ekb/zkg=|qFoIZWwS0foiiIOyikFRwQKmmmI2HeyHcOVklJnIILI=",
|
||||
Date = DateTime.UtcNow.AddMonths(-1)
|
||||
},
|
||||
new OrganizationReportSummaryModel
|
||||
{
|
||||
OrganizationId = Guid.Parse("cf6cb873-4916-4b2b-aef0-b20d00e7f3e2"),
|
||||
EncryptedData = "2.WYauwooJUEY3kZsDPphmrA==|oguYW6h10A4GxK4KkRS0X32qSTekU2CkGqNDNGfisUgvJzsyoVTafO9sVcdPdg4BUM7YNkPMjYiKEc5jMHkIgLzbnM27jcGvMJrrccSrLHiWL6/mEiqQkV3TlfiZF9i3wqj1ITsYRzM454uNle6Wrg==|uR67aFYb1i5LSidWib0iTf8091l8GY5olHkVXse3CAw=",
|
||||
EncryptionKey = "2.ZyV9+9A2cxNaf8dfzfbnlA==|hhorBpVkcrrhTtNmd6SNHYI8gPNokGLOC22Vx8Qa/AotDAcyuYWw56zsawMnzpAdJGEJFtszKM2+VUVOcroCTMWHpy8yNf/kZA6uPk3Lz3s=|ASzVeJf+K1ZB8NXuypamRBGRuRq0GUHZBEy5r/O7ORY=",
|
||||
Date = DateTime.UtcNow.AddMonths(-1)
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Settings;
|
||||
using Enums = Bit.Core.Enums;
|
||||
|
||||
@@ -35,7 +36,7 @@ public class PremiumRequestModel : IValidatableObject
|
||||
{
|
||||
yield return new ValidationResult("Payment token or license is required.");
|
||||
}
|
||||
if (Country == "US" && string.IsNullOrWhiteSpace(PostalCode))
|
||||
if (Country == Constants.CountryAbbreviations.UnitedStates && string.IsNullOrWhiteSpace(PostalCode))
|
||||
{
|
||||
yield return new ValidationResult("Zip / postal code is required.",
|
||||
new string[] { nameof(PostalCode) });
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#nullable disable
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core;
|
||||
|
||||
namespace Bit.Api.Models.Request.Accounts;
|
||||
|
||||
@@ -13,7 +14,7 @@ public class TaxInfoUpdateRequestModel : IValidatableObject
|
||||
|
||||
public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||
{
|
||||
if (Country == "US" && string.IsNullOrWhiteSpace(PostalCode))
|
||||
if (Country == Constants.CountryAbbreviations.UnitedStates && string.IsNullOrWhiteSpace(PostalCode))
|
||||
{
|
||||
yield return new ValidationResult("Zip / postal code is required.",
|
||||
new string[] { nameof(PostalCode) });
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.NotificationHub;
|
||||
using Bit.Core.Platform.PushRegistration;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
namespace Bit.Api.Models.Request;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.AdminConsole.Models.Business;
|
||||
|
||||
namespace Bit.Api.Models.Request.Organizations;
|
||||
|
||||
@@ -10,12 +9,11 @@ public class OrganizationCollectionManagementUpdateRequestModel
|
||||
public bool LimitItemDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
|
||||
public virtual Organization ToOrganization(Organization existingOrganization, IFeatureService featureService)
|
||||
public OrganizationCollectionManagementSettings ToSettings() => new()
|
||||
{
|
||||
existingOrganization.LimitCollectionCreation = LimitCollectionCreation;
|
||||
existingOrganization.LimitCollectionDeletion = LimitCollectionDeletion;
|
||||
existingOrganization.LimitItemDeletion = LimitItemDeletion;
|
||||
existingOrganization.AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems;
|
||||
return existingOrganization;
|
||||
}
|
||||
LimitCollectionCreation = LimitCollectionCreation,
|
||||
LimitCollectionDeletion = LimitCollectionDeletion,
|
||||
LimitItemDeletion = LimitItemDeletion,
|
||||
AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Bit.Core;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.Services;
|
||||
@@ -46,8 +45,7 @@ public class ConfigResponseModel : ResponseModel
|
||||
Sso = globalSettings.BaseServiceUri.Sso
|
||||
};
|
||||
FeatureStates = featureService.GetAll();
|
||||
var webPushEnabled = FeatureStates.TryGetValue(FeatureFlagKeys.WebPush, out var webPushEnabledValue) ? (bool)webPushEnabledValue : false;
|
||||
Push = PushSettings.Build(webPushEnabled, globalSettings);
|
||||
Push = PushSettings.Build(globalSettings);
|
||||
Settings = new ServerSettingsResponseModel
|
||||
{
|
||||
DisableUserRegistration = globalSettings.DisableUserRegistration
|
||||
@@ -76,9 +74,9 @@ public class PushSettings
|
||||
public PushTechnologyType PushTechnology { get; private init; }
|
||||
public string VapidPublicKey { get; private init; }
|
||||
|
||||
public static PushSettings Build(bool webPushEnabled, IGlobalSettings globalSettings)
|
||||
public static PushSettings Build(IGlobalSettings globalSettings)
|
||||
{
|
||||
var vapidPublicKey = webPushEnabled ? globalSettings.WebPush.VapidPublicKey : null;
|
||||
var vapidPublicKey = globalSettings.WebPush.VapidPublicKey;
|
||||
var pushTechnology = vapidPublicKey != null ? PushTechnologyType.WebPush : PushTechnologyType.SignalR;
|
||||
return new()
|
||||
{
|
||||
|
||||
@@ -6,9 +6,9 @@ using System.Text.Json;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.Api;
|
||||
using Bit.Core.NotificationHub;
|
||||
using Bit.Core.Platform.Push;
|
||||
using Bit.Core.Platform.Push.Internal;
|
||||
using Bit.Core.Platform.PushRegistration;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.SecretsManager.Models.Request;
|
||||
using Bit.Api.SecretsManager.Models.Response;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||
using Bit.Core.SecretsManager.Commands.Projects.Interfaces;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
|
||||
@@ -4,10 +4,10 @@
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.SecretsManager.Models.Request;
|
||||
using Bit.Api.SecretsManager.Models.Response;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.SecretsManager.AuthorizationRequirements;
|
||||
using Bit.Core.SecretsManager.Commands.Secrets.Interfaces;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
using Bit.Api.SecretsManager.Models.Response;
|
||||
using Bit.Core.Auth.Identity;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.SecretsManager.Commands.Trash.Interfaces;
|
||||
using Bit.Core.SecretsManager.Entities;
|
||||
using Bit.Core.SecretsManager.Repositories;
|
||||
|
||||
@@ -13,7 +13,6 @@ using Bit.Api.KeyManagement.Validators;
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.SharedWeb.Health;
|
||||
using Microsoft.IdentityModel.Logging;
|
||||
using Microsoft.OpenApi.Models;
|
||||
@@ -33,6 +32,9 @@ using Bit.Core.Tools.ImportFeatures;
|
||||
using Bit.Core.Auth.Models.Api.Request;
|
||||
using Bit.Core.Dirt.Reports.ReportFeatures;
|
||||
using Bit.Core.Tools.SendFeatures;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.Auth.Identity;
|
||||
|
||||
|
||||
#if !OSS
|
||||
using Bit.Commercial.Core.SecretsManager;
|
||||
@@ -144,6 +146,12 @@ public class Startup
|
||||
(c.Value.Contains(ApiScopes.Api) || c.Value.Contains(ApiScopes.ApiSecrets))
|
||||
));
|
||||
});
|
||||
config.AddPolicy(Policies.Send, configurePolicy: policy =>
|
||||
{
|
||||
policy.RequireAuthenticatedUser();
|
||||
policy.RequireClaim(JwtClaimTypes.Scope, ApiScopes.ApiSendAccess);
|
||||
policy.RequireClaim(Claims.SendAccessClaims.SendId);
|
||||
});
|
||||
});
|
||||
|
||||
services.AddScoped<AuthenticatorTokenProvider>();
|
||||
|
||||
@@ -63,7 +63,7 @@ public class ImportCiphersController : Controller
|
||||
}
|
||||
|
||||
[HttpPost("import-organization")]
|
||||
public async Task PostImport([FromQuery] string organizationId,
|
||||
public async Task PostImportOrganization([FromQuery] string organizationId,
|
||||
[FromBody] ImportOrganizationCiphersRequestModel model)
|
||||
{
|
||||
if (!_globalSettings.SelfHosted &&
|
||||
|
||||
@@ -193,7 +193,7 @@ public class SendsController : Controller
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<SendResponseModel>> Get()
|
||||
public async Task<ListResponseModel<SendResponseModel>> GetAll()
|
||||
{
|
||||
var sends = await _sendOwnerQuery.GetOwned(User);
|
||||
var responses = sends.Select(s => new SendResponseModel(s));
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
using Bit.Api.AdminConsole.Authorization;
|
||||
using Bit.Api.Tools.Authorization;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Authorization;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.Auth.IdentityServer;
|
||||
using Bit.Core.PhishingDomainFeatures;
|
||||
using Bit.Core.PhishingDomainFeatures.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
@@ -82,15 +80,7 @@ public static class ServiceCollectionExtensions
|
||||
config.DescribeAllParametersInCamelCase();
|
||||
// config.UseReferencedDefinitionsForEnums();
|
||||
|
||||
config.SchemaFilter<EnumSchemaFilter>();
|
||||
config.SchemaFilter<EncryptedStringSchemaFilter>();
|
||||
|
||||
// These two filters require debug symbols/git, so only add them in development mode
|
||||
if (environment.IsDevelopment())
|
||||
{
|
||||
config.DocumentFilter<GitCommitDocumentFilter>();
|
||||
config.OperationFilter<SourceFileLineOperationFilter>();
|
||||
}
|
||||
config.InitializeSwaggerFilters(environment);
|
||||
|
||||
var apiFilePath = Path.Combine(AppContext.BaseDirectory, "Api.xml");
|
||||
config.IncludeXmlComments(apiFilePath, true);
|
||||
@@ -117,14 +107,12 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
public static void AddAuthorizationHandlers(this IServiceCollection services)
|
||||
{
|
||||
services.AddScoped<IAuthorizationHandler, BulkCollectionAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, CollectionAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, GroupAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, VaultExportAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, SecurityTaskAuthorizationHandler>();
|
||||
services.AddScoped<IAuthorizationHandler, SecurityTaskOrganizationAuthorizationHandler>();
|
||||
|
||||
services.AddScoped<IAuthorizationHandler, OrganizationRequirementHandler>();
|
||||
// Admin Console authorization handlers
|
||||
services.AddAdminConsoleAuthorizationHandlers();
|
||||
}
|
||||
|
||||
public static void AddPhishingDomainServices(this IServiceCollection services, GlobalSettings globalSettings)
|
||||
|
||||
@@ -20,6 +20,7 @@ using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Services;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Authorization.Permissions;
|
||||
using Bit.Core.Vault.Commands.Interfaces;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Core.Vault.Models.Data;
|
||||
using Bit.Core.Vault.Queries;
|
||||
@@ -48,6 +49,9 @@ public class CiphersController : Controller
|
||||
private readonly IOrganizationCiphersQuery _organizationCiphersQuery;
|
||||
private readonly IApplicationCacheService _applicationCacheService;
|
||||
private readonly ICollectionRepository _collectionRepository;
|
||||
private readonly IArchiveCiphersCommand _archiveCiphersCommand;
|
||||
private readonly IUnarchiveCiphersCommand _unarchiveCiphersCommand;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public CiphersController(
|
||||
ICipherRepository cipherRepository,
|
||||
@@ -61,7 +65,10 @@ public class CiphersController : Controller
|
||||
GlobalSettings globalSettings,
|
||||
IOrganizationCiphersQuery organizationCiphersQuery,
|
||||
IApplicationCacheService applicationCacheService,
|
||||
ICollectionRepository collectionRepository)
|
||||
ICollectionRepository collectionRepository,
|
||||
IArchiveCiphersCommand archiveCiphersCommand,
|
||||
IUnarchiveCiphersCommand unarchiveCiphersCommand,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_cipherRepository = cipherRepository;
|
||||
_collectionCipherRepository = collectionCipherRepository;
|
||||
@@ -75,6 +82,9 @@ public class CiphersController : Controller
|
||||
_organizationCiphersQuery = organizationCiphersQuery;
|
||||
_applicationCacheService = applicationCacheService;
|
||||
_collectionRepository = collectionRepository;
|
||||
_archiveCiphersCommand = archiveCiphersCommand;
|
||||
_unarchiveCiphersCommand = unarchiveCiphersCommand;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
[HttpGet("{id}")]
|
||||
@@ -108,7 +118,6 @@ public class CiphersController : Controller
|
||||
return new CipherMiniDetailsResponseModel(cipher, _globalSettings, collectionCiphersGroupDict, cipher.OrganizationUseTotp);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/full-details")]
|
||||
[HttpGet("{id}/details")]
|
||||
public async Task<CipherDetailsResponseModel> GetDetails(Guid id)
|
||||
{
|
||||
@@ -124,8 +133,15 @@ public class CiphersController : Controller
|
||||
return new CipherDetailsResponseModel(cipher, user, organizationAbilities, _globalSettings, collectionCiphers);
|
||||
}
|
||||
|
||||
[HttpGet("{id}/full-details")]
|
||||
[Obsolete("This endpoint is deprecated. Use GET details method instead.")]
|
||||
public async Task<CipherDetailsResponseModel> GetFullDetails(Guid id)
|
||||
{
|
||||
return await GetDetails(id);
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<CipherDetailsResponseModel>> Get()
|
||||
public async Task<ListResponseModel<CipherDetailsResponseModel>> GetAll()
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
var hasOrgs = _currentContext.Organizations.Count != 0;
|
||||
@@ -232,7 +248,6 @@ public class CiphersController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[HttpPost("{id}")]
|
||||
public async Task<CipherResponseModel> Put(Guid id, [FromBody] CipherRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
@@ -273,8 +288,14 @@ public class CiphersController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("{id}")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT method instead.")]
|
||||
public async Task<CipherResponseModel> PostPut(Guid id, [FromBody] CipherRequestModel model)
|
||||
{
|
||||
return await Put(id, model);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/admin")]
|
||||
[HttpPost("{id}/admin")]
|
||||
public async Task<CipherMiniResponseModel> PutAdmin(Guid id, [FromBody] CipherRequestModel model)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
@@ -307,6 +328,13 @@ public class CiphersController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("{id}/admin")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT method instead.")]
|
||||
public async Task<CipherMiniResponseModel> PostPutAdmin(Guid id, [FromBody] CipherRequestModel model)
|
||||
{
|
||||
return await PutAdmin(id, model);
|
||||
}
|
||||
|
||||
[HttpGet("organization-details")]
|
||||
public async Task<ListResponseModel<CipherMiniDetailsResponseModel>> GetOrganizationCiphers(Guid organizationId)
|
||||
{
|
||||
@@ -314,8 +342,11 @@ public class CiphersController : Controller
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
var allOrganizationCiphers = await _organizationCiphersQuery.GetAllOrganizationCiphers(organizationId);
|
||||
var allOrganizationCiphers = _featureService.IsEnabled(FeatureFlagKeys.CreateDefaultLocation)
|
||||
?
|
||||
await _organizationCiphersQuery.GetAllOrganizationCiphersExcludingDefaultUserCollections(organizationId)
|
||||
:
|
||||
await _organizationCiphersQuery.GetAllOrganizationCiphers(organizationId);
|
||||
|
||||
var allOrganizationCipherResponses =
|
||||
allOrganizationCiphers.Select(c =>
|
||||
@@ -678,7 +709,6 @@ public class CiphersController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("{id}/partial")]
|
||||
[HttpPost("{id}/partial")]
|
||||
public async Task<CipherResponseModel> PutPartial(Guid id, [FromBody] CipherPartialRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
@@ -694,8 +724,14 @@ public class CiphersController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("{id}/partial")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT method instead.")]
|
||||
public async Task<CipherResponseModel> PostPartial(Guid id, [FromBody] CipherPartialRequestModel model)
|
||||
{
|
||||
return await PutPartial(id, model);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/share")]
|
||||
[HttpPost("{id}/share")]
|
||||
public async Task<CipherResponseModel> PutShare(Guid id, [FromBody] CipherShareRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
@@ -731,8 +767,14 @@ public class CiphersController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("{id}/share")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT method instead.")]
|
||||
public async Task<CipherResponseModel> PostShare(Guid id, [FromBody] CipherShareRequestModel model)
|
||||
{
|
||||
return await PutShare(id, model);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/collections")]
|
||||
[HttpPost("{id}/collections")]
|
||||
public async Task<CipherDetailsResponseModel> PutCollections(Guid id, [FromBody] CipherCollectionsRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
@@ -757,8 +799,14 @@ public class CiphersController : Controller
|
||||
collectionCiphers);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/collections")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT method instead.")]
|
||||
public async Task<CipherDetailsResponseModel> PostCollections(Guid id, [FromBody] CipherCollectionsRequestModel model)
|
||||
{
|
||||
return await PutCollections(id, model);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/collections_v2")]
|
||||
[HttpPost("{id}/collections_v2")]
|
||||
public async Task<OptionalCipherDetailsResponseModel> PutCollections_vNext(Guid id, [FromBody] CipherCollectionsRequestModel model)
|
||||
{
|
||||
var user = await _userService.GetUserByPrincipalAsync(User);
|
||||
@@ -791,8 +839,14 @@ public class CiphersController : Controller
|
||||
return response;
|
||||
}
|
||||
|
||||
[HttpPost("{id}/collections_v2")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT method instead.")]
|
||||
public async Task<OptionalCipherDetailsResponseModel> PostCollections_vNext(Guid id, [FromBody] CipherCollectionsRequestModel model)
|
||||
{
|
||||
return await PutCollections_vNext(id, model);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/collections-admin")]
|
||||
[HttpPost("{id}/collections-admin")]
|
||||
public async Task<CipherMiniDetailsResponseModel> PutCollectionsAdmin(string id, [FromBody] CipherCollectionsRequestModel model)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
@@ -821,6 +875,13 @@ public class CiphersController : Controller
|
||||
return new CipherMiniDetailsResponseModel(cipher, _globalSettings, collectionCiphersGroupDict, cipher.OrganizationUseTotp);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/collections-admin")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT method instead.")]
|
||||
public async Task<CipherMiniDetailsResponseModel> PostCollectionsAdmin(string id, [FromBody] CipherCollectionsRequestModel model)
|
||||
{
|
||||
return await PutCollectionsAdmin(id, model);
|
||||
}
|
||||
|
||||
[HttpPost("bulk-collections")]
|
||||
public async Task PostBulkCollections([FromBody] CipherBulkUpdateCollectionsRequestModel model)
|
||||
{
|
||||
@@ -840,8 +901,48 @@ public class CiphersController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPut("{id}/archive")]
|
||||
[RequireFeature(FeatureFlagKeys.ArchiveVaultItems)]
|
||||
public async Task<CipherMiniResponseModel> PutArchive(Guid id)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
|
||||
var archivedCipherOrganizationDetails = await _archiveCiphersCommand.ArchiveManyAsync([id], userId);
|
||||
|
||||
if (archivedCipherOrganizationDetails.Count == 0)
|
||||
{
|
||||
throw new BadRequestException("Cipher was not archived. Ensure the provided ID is correct and you have permission to archive it.");
|
||||
}
|
||||
|
||||
return new CipherMiniResponseModel(archivedCipherOrganizationDetails.First(), _globalSettings, archivedCipherOrganizationDetails.First().OrganizationUseTotp);
|
||||
}
|
||||
|
||||
[HttpPut("archive")]
|
||||
[RequireFeature(FeatureFlagKeys.ArchiveVaultItems)]
|
||||
public async Task<ListResponseModel<CipherMiniResponseModel>> PutArchiveMany([FromBody] CipherBulkArchiveRequestModel model)
|
||||
{
|
||||
if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)
|
||||
{
|
||||
throw new BadRequestException("You can only archive up to 500 items at a time.");
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
|
||||
var cipherIdsToArchive = new HashSet<Guid>(model.Ids);
|
||||
|
||||
var archivedCiphers = await _archiveCiphersCommand.ArchiveManyAsync(cipherIdsToArchive, userId);
|
||||
|
||||
if (archivedCiphers.Count == 0)
|
||||
{
|
||||
throw new BadRequestException("No ciphers were archived. Ensure the provided IDs are correct and you have permission to archive them.");
|
||||
}
|
||||
|
||||
var responses = archivedCiphers.Select(c => new CipherMiniResponseModel(c, _globalSettings, c.OrganizationUseTotp));
|
||||
|
||||
return new ListResponseModel<CipherMiniResponseModel>(responses);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpPost("{id}/delete")]
|
||||
public async Task Delete(Guid id)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
@@ -854,8 +955,14 @@ public class CiphersController : Controller
|
||||
await _cipherService.DeleteAsync(cipher, userId);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE method instead.")]
|
||||
public async Task PostDelete(Guid id)
|
||||
{
|
||||
await Delete(id);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}/admin")]
|
||||
[HttpPost("{id}/delete-admin")]
|
||||
public async Task DeleteAdmin(Guid id)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
@@ -869,8 +976,14 @@ public class CiphersController : Controller
|
||||
await _cipherService.DeleteAsync(cipher, userId, true);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/delete-admin")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE method instead.")]
|
||||
public async Task PostDeleteAdmin(Guid id)
|
||||
{
|
||||
await DeleteAdmin(id);
|
||||
}
|
||||
|
||||
[HttpDelete("")]
|
||||
[HttpPost("delete")]
|
||||
public async Task DeleteMany([FromBody] CipherBulkDeleteRequestModel model)
|
||||
{
|
||||
if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)
|
||||
@@ -883,8 +996,14 @@ public class CiphersController : Controller
|
||||
await _cipherService.DeleteManyAsync(model.Ids.Select(i => new Guid(i)), userId);
|
||||
}
|
||||
|
||||
[HttpPost("delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE method instead.")]
|
||||
public async Task PostDeleteMany([FromBody] CipherBulkDeleteRequestModel model)
|
||||
{
|
||||
await DeleteMany(model);
|
||||
}
|
||||
|
||||
[HttpDelete("admin")]
|
||||
[HttpPost("delete-admin")]
|
||||
public async Task DeleteManyAdmin([FromBody] CipherBulkDeleteRequestModel model)
|
||||
{
|
||||
if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)
|
||||
@@ -910,6 +1029,13 @@ public class CiphersController : Controller
|
||||
await _cipherService.DeleteManyAsync(cipherIds, userId, new Guid(model.OrganizationId), true);
|
||||
}
|
||||
|
||||
[HttpPost("delete-admin")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE method instead.")]
|
||||
public async Task PostDeleteManyAdmin([FromBody] CipherBulkDeleteRequestModel model)
|
||||
{
|
||||
await DeleteManyAdmin(model);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/delete")]
|
||||
public async Task PutDelete(Guid id)
|
||||
{
|
||||
@@ -973,6 +1099,47 @@ public class CiphersController : Controller
|
||||
await _cipherService.SoftDeleteManyAsync(cipherIds, userId, new Guid(model.OrganizationId), true);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/unarchive")]
|
||||
[RequireFeature(FeatureFlagKeys.ArchiveVaultItems)]
|
||||
public async Task<CipherMiniResponseModel> PutUnarchive(Guid id)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
|
||||
var unarchivedCipherDetails = await _unarchiveCiphersCommand.UnarchiveManyAsync([id], userId);
|
||||
|
||||
if (unarchivedCipherDetails.Count == 0)
|
||||
{
|
||||
throw new BadRequestException("Cipher was not unarchived. Ensure the provided ID is correct and you have permission to archive it.");
|
||||
}
|
||||
|
||||
return new CipherMiniResponseModel(unarchivedCipherDetails.First(), _globalSettings, unarchivedCipherDetails.First().OrganizationUseTotp);
|
||||
}
|
||||
|
||||
[HttpPut("unarchive")]
|
||||
[RequireFeature(FeatureFlagKeys.ArchiveVaultItems)]
|
||||
public async Task<ListResponseModel<CipherMiniResponseModel>> PutUnarchiveMany([FromBody] CipherBulkUnarchiveRequestModel model)
|
||||
{
|
||||
if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)
|
||||
{
|
||||
throw new BadRequestException("You can only unarchive up to 500 items at a time.");
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
|
||||
var cipherIdsToUnarchive = new HashSet<Guid>(model.Ids);
|
||||
|
||||
var unarchivedCipherOrganizationDetails = await _unarchiveCiphersCommand.UnarchiveManyAsync(cipherIdsToUnarchive, userId);
|
||||
|
||||
if (unarchivedCipherOrganizationDetails.Count == 0)
|
||||
{
|
||||
throw new BadRequestException("Ciphers were not unarchived. Ensure the provided ID is correct and you have permission to archive it.");
|
||||
}
|
||||
|
||||
var responses = unarchivedCipherOrganizationDetails.Select(c => new CipherMiniResponseModel(c, _globalSettings, c.OrganizationUseTotp));
|
||||
|
||||
return new ListResponseModel<CipherMiniResponseModel>(responses);
|
||||
}
|
||||
|
||||
[HttpPut("{id}/restore")]
|
||||
public async Task<CipherResponseModel> PutRestore(Guid id)
|
||||
{
|
||||
@@ -1050,7 +1217,6 @@ public class CiphersController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("move")]
|
||||
[HttpPost("move")]
|
||||
public async Task MoveMany([FromBody] CipherBulkMoveRequestModel model)
|
||||
{
|
||||
if (!_globalSettings.SelfHosted && model.Ids.Count() > 500)
|
||||
@@ -1063,8 +1229,14 @@ public class CiphersController : Controller
|
||||
string.IsNullOrWhiteSpace(model.FolderId) ? (Guid?)null : new Guid(model.FolderId), userId);
|
||||
}
|
||||
|
||||
[HttpPost("move")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT method instead.")]
|
||||
public async Task PostMoveMany([FromBody] CipherBulkMoveRequestModel model)
|
||||
{
|
||||
await MoveMany(model);
|
||||
}
|
||||
|
||||
[HttpPut("share")]
|
||||
[HttpPost("share")]
|
||||
public async Task<ListResponseModel<CipherMiniResponseModel>> PutShareMany([FromBody] CipherBulkShareRequestModel model)
|
||||
{
|
||||
var organizationId = new Guid(model.Ciphers.First().OrganizationId);
|
||||
@@ -1112,6 +1284,13 @@ public class CiphersController : Controller
|
||||
return new ListResponseModel<CipherMiniResponseModel>(response);
|
||||
}
|
||||
|
||||
[HttpPost("share")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT method instead.")]
|
||||
public async Task<ListResponseModel<CipherMiniResponseModel>> PostShareMany([FromBody] CipherBulkShareRequestModel model)
|
||||
{
|
||||
return await PutShareMany(model);
|
||||
}
|
||||
|
||||
[HttpPost("purge")]
|
||||
public async Task PostPurge([FromBody] SecretVerificationRequestModel model, Guid? organizationId = null)
|
||||
{
|
||||
@@ -1230,7 +1409,7 @@ public class CiphersController : Controller
|
||||
[Obsolete("Deprecated Attachments API", false)]
|
||||
[RequestSizeLimit(Constants.FileSize101mb)]
|
||||
[DisableFormValueModelBinding]
|
||||
public async Task<CipherResponseModel> PostAttachment(Guid id)
|
||||
public async Task<CipherResponseModel> PostAttachmentV1(Guid id)
|
||||
{
|
||||
ValidateAttachment();
|
||||
|
||||
@@ -1324,7 +1503,6 @@ public class CiphersController : Controller
|
||||
}
|
||||
|
||||
[HttpDelete("{id}/attachment/{attachmentId}")]
|
||||
[HttpPost("{id}/attachment/{attachmentId}/delete")]
|
||||
public async Task<DeleteAttachmentResponseData> DeleteAttachment(Guid id, string attachmentId)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
@@ -1337,8 +1515,14 @@ public class CiphersController : Controller
|
||||
return await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, false);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/attachment/{attachmentId}/delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE method instead.")]
|
||||
public async Task<DeleteAttachmentResponseData> PostDeleteAttachment(Guid id, string attachmentId)
|
||||
{
|
||||
return await DeleteAttachment(id, attachmentId);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}/attachment/{attachmentId}/admin")]
|
||||
[HttpPost("{id}/attachment/{attachmentId}/delete-admin")]
|
||||
public async Task<DeleteAttachmentResponseData> DeleteAttachmentAdmin(Guid id, string attachmentId)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
@@ -1352,6 +1536,13 @@ public class CiphersController : Controller
|
||||
return await _cipherService.DeleteAttachmentAsync(cipher, attachmentId, userId, true);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/attachment/{attachmentId}/delete-admin")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE method instead.")]
|
||||
public async Task<DeleteAttachmentResponseData> PostDeleteAttachmentAdmin(Guid id, string attachmentId)
|
||||
{
|
||||
return await DeleteAttachmentAdmin(id, attachmentId);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
[HttpPost("attachment/validate/azure")]
|
||||
public async Task<ObjectResult> AzureValidateFile()
|
||||
|
||||
@@ -45,7 +45,7 @@ public class FoldersController : Controller
|
||||
}
|
||||
|
||||
[HttpGet("")]
|
||||
public async Task<ListResponseModel<FolderResponseModel>> Get()
|
||||
public async Task<ListResponseModel<FolderResponseModel>> GetAll()
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
var folders = await _folderRepository.GetManyByUserIdAsync(userId);
|
||||
@@ -63,7 +63,6 @@ public class FoldersController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("{id}")]
|
||||
[HttpPost("{id}")]
|
||||
public async Task<FolderResponseModel> Put(string id, [FromBody] FolderRequestModel model)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
@@ -77,8 +76,14 @@ public class FoldersController : Controller
|
||||
return new FolderResponseModel(folder);
|
||||
}
|
||||
|
||||
[HttpPost("{id}")]
|
||||
[Obsolete("This endpoint is deprecated. Use PUT method instead.")]
|
||||
public async Task<FolderResponseModel> PostPut(string id, [FromBody] FolderRequestModel model)
|
||||
{
|
||||
return await Put(id, model);
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpPost("{id}/delete")]
|
||||
public async Task Delete(string id)
|
||||
{
|
||||
var userId = _userService.GetProperUserId(User).Value;
|
||||
@@ -91,6 +96,13 @@ public class FoldersController : Controller
|
||||
await _cipherService.DeleteFolderAsync(folder);
|
||||
}
|
||||
|
||||
[HttpPost("{id}/delete")]
|
||||
[Obsolete("This endpoint is deprecated. Use DELETE method instead.")]
|
||||
public async Task PostDelete(string id)
|
||||
{
|
||||
await Delete(id);
|
||||
}
|
||||
|
||||
[HttpDelete("all")]
|
||||
public async Task DeleteAll()
|
||||
{
|
||||
|
||||
@@ -46,6 +46,7 @@ public class CipherRequestModel
|
||||
public CipherSecureNoteModel SecureNote { get; set; }
|
||||
public CipherSSHKeyModel SSHKey { get; set; }
|
||||
public DateTime? LastKnownRevisionDate { get; set; } = null;
|
||||
public DateTime? ArchivedDate { get; set; }
|
||||
|
||||
public CipherDetails ToCipherDetails(Guid userId, bool allowOrgIdSet = true)
|
||||
{
|
||||
@@ -99,6 +100,7 @@ public class CipherRequestModel
|
||||
|
||||
existingCipher.Reprompt = Reprompt;
|
||||
existingCipher.Key = Key;
|
||||
existingCipher.ArchivedDate = ArchivedDate;
|
||||
|
||||
var hasAttachments2 = (Attachments2?.Count ?? 0) > 0;
|
||||
var hasAttachments = (Attachments?.Count ?? 0) > 0;
|
||||
@@ -316,6 +318,12 @@ public class CipherCollectionsRequestModel
|
||||
public IEnumerable<string> CollectionIds { get; set; }
|
||||
}
|
||||
|
||||
public class CipherBulkArchiveRequestModel
|
||||
{
|
||||
[Required]
|
||||
public IEnumerable<Guid> Ids { get; set; }
|
||||
}
|
||||
|
||||
public class CipherBulkDeleteRequestModel
|
||||
{
|
||||
[Required]
|
||||
@@ -323,6 +331,12 @@ public class CipherBulkDeleteRequestModel
|
||||
public string OrganizationId { get; set; }
|
||||
}
|
||||
|
||||
public class CipherBulkUnarchiveRequestModel
|
||||
{
|
||||
[Required]
|
||||
public IEnumerable<Guid> Ids { get; set; }
|
||||
}
|
||||
|
||||
public class CipherBulkRestoreRequestModel
|
||||
{
|
||||
[Required]
|
||||
|
||||
@@ -74,6 +74,7 @@ public class CipherMiniResponseModel : ResponseModel
|
||||
DeletedDate = cipher.DeletedDate;
|
||||
Reprompt = cipher.Reprompt.GetValueOrDefault(CipherRepromptType.None);
|
||||
Key = cipher.Key;
|
||||
ArchivedDate = cipher.ArchivedDate;
|
||||
}
|
||||
|
||||
public Guid Id { get; set; }
|
||||
@@ -96,6 +97,7 @@ public class CipherMiniResponseModel : ResponseModel
|
||||
public DateTime? DeletedDate { get; set; }
|
||||
public CipherRepromptType Reprompt { get; set; }
|
||||
public string Key { get; set; }
|
||||
public DateTime? ArchivedDate { get; set; }
|
||||
}
|
||||
|
||||
public class CipherResponseModel : CipherMiniResponseModel
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
<ProjectReference Include="..\Core\Core.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.2" />
|
||||
<PackageReference Include="MarkDig" Version="0.41.3" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="9.0.4" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -34,6 +34,10 @@ public class BillingSettings
|
||||
public virtual string Region { get; set; }
|
||||
public virtual string UserFieldName { get; set; }
|
||||
public virtual string OrgFieldName { get; set; }
|
||||
|
||||
public virtual bool RemoveNewlinesInReplies { get; set; } = false;
|
||||
public virtual string AutoReplyGreeting { get; set; } = string.Empty;
|
||||
public virtual string AutoReplySalutation { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class OnyxSettings
|
||||
|
||||
@@ -13,4 +13,5 @@ public static class HandledStripeWebhook
|
||||
public const string PaymentMethodAttached = "payment_method.attached";
|
||||
public const string CustomerUpdated = "customer.updated";
|
||||
public const string InvoiceFinalized = "invoice.finalized";
|
||||
public const string SetupIntentSucceeded = "setup_intent.succeeded";
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ using Bit.Billing.Models;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Markdig;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
@@ -152,6 +153,12 @@ public class FreshdeskController : Controller
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
// if there is no description, then we don't send anything to onyx
|
||||
if (string.IsNullOrEmpty(model.TicketDescriptionText.Trim()))
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
|
||||
// create the onyx `answer-with-citation` request
|
||||
var onyxRequestModel = new OnyxAnswerWithCitationRequestModel(model.TicketDescriptionText, _billingSettings.Onyx.PersonaId);
|
||||
var onyxRequest = new HttpRequestMessage(HttpMethod.Post,
|
||||
@@ -164,9 +171,12 @@ public class FreshdeskController : Controller
|
||||
// the CallOnyxApi will return a null if we have an error response
|
||||
if (onyxJsonResponse?.Answer == null || !string.IsNullOrEmpty(onyxJsonResponse?.ErrorMsg))
|
||||
{
|
||||
return BadRequest(
|
||||
string.Format("Failed to get a valid response from Onyx API. Response: {0}",
|
||||
JsonSerializer.Serialize(onyxJsonResponse ?? new OnyxAnswerWithCitationResponseModel())));
|
||||
_logger.LogWarning("Error getting answer from Onyx AI. Freshdesk model: {model}\r\n Onyx query {query}\r\nresponse: {response}. ",
|
||||
JsonSerializer.Serialize(model),
|
||||
JsonSerializer.Serialize(onyxRequestModel),
|
||||
JsonSerializer.Serialize(onyxJsonResponse));
|
||||
|
||||
return Ok(); // return ok so we don't retry
|
||||
}
|
||||
|
||||
// add the answer as a note to the ticket
|
||||
@@ -175,6 +185,52 @@ public class FreshdeskController : Controller
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[HttpPost("webhook-onyx-ai-reply")]
|
||||
public async Task<IActionResult> PostWebhookOnyxAiReply([FromQuery, Required] string key,
|
||||
[FromBody, Required] FreshdeskOnyxAiWebhookModel model)
|
||||
{
|
||||
// NOTE:
|
||||
// at this time, this endpoint is a duplicate of `webhook-onyx-ai`
|
||||
// eventually, we will merge both endpoints into one webhook for Freshdesk
|
||||
|
||||
// ensure that the key is from Freshdesk
|
||||
if (!IsValidRequestFromFreshdesk(key) || !ModelState.IsValid)
|
||||
{
|
||||
return new BadRequestResult();
|
||||
}
|
||||
|
||||
// if there is no description, then we don't send anything to onyx
|
||||
if (string.IsNullOrEmpty(model.TicketDescriptionText.Trim()))
|
||||
{
|
||||
return Ok();
|
||||
}
|
||||
|
||||
// create the onyx `answer-with-citation` request
|
||||
var onyxRequestModel = new OnyxAnswerWithCitationRequestModel(model.TicketDescriptionText, _billingSettings.Onyx.PersonaId);
|
||||
var onyxRequest = new HttpRequestMessage(HttpMethod.Post,
|
||||
string.Format("{0}/query/answer-with-citation", _billingSettings.Onyx.BaseUrl))
|
||||
{
|
||||
Content = JsonContent.Create(onyxRequestModel, mediaType: new MediaTypeHeaderValue("application/json")),
|
||||
};
|
||||
var (_, onyxJsonResponse) = await CallOnyxApi<OnyxAnswerWithCitationResponseModel>(onyxRequest);
|
||||
|
||||
// the CallOnyxApi will return a null if we have an error response
|
||||
if (onyxJsonResponse?.Answer == null || !string.IsNullOrEmpty(onyxJsonResponse?.ErrorMsg))
|
||||
{
|
||||
_logger.LogWarning("Error getting answer from Onyx AI. Freshdesk model: {model}\r\n Onyx query {query}\r\nresponse: {response}. ",
|
||||
JsonSerializer.Serialize(model),
|
||||
JsonSerializer.Serialize(onyxRequestModel),
|
||||
JsonSerializer.Serialize(onyxJsonResponse));
|
||||
|
||||
return Ok(); // return ok so we don't retry
|
||||
}
|
||||
|
||||
// add the reply to the ticket
|
||||
await AddReplyToTicketAsync(onyxJsonResponse.Answer, model.TicketId);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
|
||||
private bool IsValidRequestFromFreshdesk(string key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key)
|
||||
@@ -229,6 +285,53 @@ public class FreshdeskController : Controller
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddReplyToTicketAsync(string note, string ticketId)
|
||||
{
|
||||
// if there is no content, then we don't need to add a note
|
||||
if (string.IsNullOrWhiteSpace(note))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// convert note from markdown to html
|
||||
var htmlNote = note;
|
||||
try
|
||||
{
|
||||
var pipeline = new MarkdownPipelineBuilder().UseAdvancedExtensions().Build();
|
||||
htmlNote = Markdig.Markdown.ToHtml(note, pipeline);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error converting markdown to HTML for Freshdesk reply. Ticket Id: {0}. Note: {1}",
|
||||
ticketId, note);
|
||||
htmlNote = note; // fallback to the original note
|
||||
}
|
||||
|
||||
// clear out any new lines that Freshdesk doesn't like
|
||||
if (_billingSettings.FreshDesk.RemoveNewlinesInReplies)
|
||||
{
|
||||
htmlNote = htmlNote.Replace(Environment.NewLine, string.Empty);
|
||||
}
|
||||
|
||||
var replyBody = new FreshdeskReplyRequestModel
|
||||
{
|
||||
Body = $"{_billingSettings.FreshDesk.AutoReplyGreeting}{htmlNote}{_billingSettings.FreshDesk.AutoReplySalutation}",
|
||||
};
|
||||
|
||||
var replyRequest = new HttpRequestMessage(HttpMethod.Post,
|
||||
string.Format("https://bitwarden.freshdesk.com/api/v2/tickets/{0}/reply", ticketId))
|
||||
{
|
||||
Content = JsonContent.Create(replyBody),
|
||||
};
|
||||
|
||||
var addReplyResponse = await CallFreshdeskApiAsync(replyRequest);
|
||||
if (addReplyResponse.StatusCode != System.Net.HttpStatusCode.Created)
|
||||
{
|
||||
_logger.LogError("Error adding reply to Freshdesk ticket. Ticket Id: {0}. Status: {1}",
|
||||
ticketId, addReplyResponse.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> CallFreshdeskApiAsync(HttpRequestMessage request, int retriedCount = 0)
|
||||
{
|
||||
try
|
||||
|
||||
9
src/Billing/Models/FreshdeskReplyRequestModel.cs
Normal file
9
src/Billing/Models/FreshdeskReplyRequestModel.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Bit.Billing.Models;
|
||||
|
||||
public class FreshdeskReplyRequestModel
|
||||
{
|
||||
[JsonPropertyName("body")]
|
||||
public required string Body { get; set; }
|
||||
}
|
||||
11
src/Billing/Services/IPushNotificationAdapter.cs
Normal file
11
src/Billing/Services/IPushNotificationAdapter.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Entities.Provider;
|
||||
|
||||
namespace Bit.Billing.Services;
|
||||
|
||||
public interface IPushNotificationAdapter
|
||||
{
|
||||
Task NotifyBankAccountVerifiedAsync(Organization organization);
|
||||
Task NotifyBankAccountVerifiedAsync(Provider provider);
|
||||
Task NotifyEnabledChangedAsync(Organization organization);
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
// FIXME: Update this file to be null safe and then delete the line below
|
||||
#nullable disable
|
||||
|
||||
using Stripe;
|
||||
using Stripe;
|
||||
|
||||
namespace Bit.Billing.Services;
|
||||
|
||||
@@ -13,12 +10,10 @@ public interface IStripeEventService
|
||||
/// and optionally expands it with the provided <see cref="expand"/> options.
|
||||
/// </summary>
|
||||
/// <param name="stripeEvent">The Stripe webhook event.</param>
|
||||
/// <param name="fresh">Determines whether or not to retrieve a fresh copy of the charge object from Stripe.</param>
|
||||
/// <param name="fresh">Determines whether to retrieve a fresh copy of the charge object from Stripe.</param>
|
||||
/// <param name="expand">Optionally provided to expand the fresh charge object retrieved from Stripe.</param>
|
||||
/// <returns>A Stripe <see cref="Charge"/>.</returns>
|
||||
/// <exception cref="Exception">Thrown when the Stripe event does not contain a charge object.</exception>
|
||||
/// <exception cref="Exception">Thrown when <paramref name="fresh"/> is true and Stripe's API returns a null charge object.</exception>
|
||||
Task<Charge> GetCharge(Event stripeEvent, bool fresh = false, List<string> expand = null);
|
||||
Task<Charge> GetCharge(Event stripeEvent, bool fresh = false, List<string>? expand = null);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the <see cref="Customer"/> object from the Stripe <see cref="Event"/>. When <paramref name="fresh"/> is true,
|
||||
@@ -26,12 +21,10 @@ public interface IStripeEventService
|
||||
/// and optionally expands it with the provided <see cref="expand"/> options.
|
||||
/// </summary>
|
||||
/// <param name="stripeEvent">The Stripe webhook event.</param>
|
||||
/// <param name="fresh">Determines whether or not to retrieve a fresh copy of the customer object from Stripe.</param>
|
||||
/// <param name="fresh">Determines whether to retrieve a fresh copy of the customer object from Stripe.</param>
|
||||
/// <param name="expand">Optionally provided to expand the fresh customer object retrieved from Stripe.</param>
|
||||
/// <returns>A Stripe <see cref="Customer"/>.</returns>
|
||||
/// <exception cref="Exception">Thrown when the Stripe event does not contain a customer object.</exception>
|
||||
/// <exception cref="Exception">Thrown when <paramref name="fresh"/> is true and Stripe's API returns a null customer object.</exception>
|
||||
Task<Customer> GetCustomer(Event stripeEvent, bool fresh = false, List<string> expand = null);
|
||||
Task<Customer> GetCustomer(Event stripeEvent, bool fresh = false, List<string>? expand = null);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the <see cref="Invoice"/> object from the Stripe <see cref="Event"/>. When <paramref name="fresh"/> is true,
|
||||
@@ -39,12 +32,10 @@ public interface IStripeEventService
|
||||
/// and optionally expands it with the provided <see cref="expand"/> options.
|
||||
/// </summary>
|
||||
/// <param name="stripeEvent">The Stripe webhook event.</param>
|
||||
/// <param name="fresh">Determines whether or not to retrieve a fresh copy of the invoice object from Stripe.</param>
|
||||
/// <param name="fresh">Determines whether to retrieve a fresh copy of the invoice object from Stripe.</param>
|
||||
/// <param name="expand">Optionally provided to expand the fresh invoice object retrieved from Stripe.</param>
|
||||
/// <returns>A Stripe <see cref="Invoice"/>.</returns>
|
||||
/// <exception cref="Exception">Thrown when the Stripe event does not contain an invoice object.</exception>
|
||||
/// <exception cref="Exception">Thrown when <paramref name="fresh"/> is true and Stripe's API returns a null invoice object.</exception>
|
||||
Task<Invoice> GetInvoice(Event stripeEvent, bool fresh = false, List<string> expand = null);
|
||||
Task<Invoice> GetInvoice(Event stripeEvent, bool fresh = false, List<string>? expand = null);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the <see cref="PaymentMethod"/> object from the Stripe <see cref="Event"/>. When <paramref name="fresh"/> is true,
|
||||
@@ -52,12 +43,21 @@ public interface IStripeEventService
|
||||
/// and optionally expands it with the provided <see cref="expand"/> options.
|
||||
/// </summary>
|
||||
/// <param name="stripeEvent">The Stripe webhook event.</param>
|
||||
/// <param name="fresh">Determines whether or not to retrieve a fresh copy of the payment method object from Stripe.</param>
|
||||
/// <param name="fresh">Determines whether to retrieve a fresh copy of the payment method object from Stripe.</param>
|
||||
/// <param name="expand">Optionally provided to expand the fresh payment method object retrieved from Stripe.</param>
|
||||
/// <returns>A Stripe <see cref="PaymentMethod"/>.</returns>
|
||||
/// <exception cref="Exception">Thrown when the Stripe event does not contain an payment method object.</exception>
|
||||
/// <exception cref="Exception">Thrown when <paramref name="fresh"/> is true and Stripe's API returns a null payment method object.</exception>
|
||||
Task<PaymentMethod> GetPaymentMethod(Event stripeEvent, bool fresh = false, List<string> expand = null);
|
||||
Task<PaymentMethod> GetPaymentMethod(Event stripeEvent, bool fresh = false, List<string>? expand = null);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the <see cref="SetupIntent"/> object from the Stripe <see cref="Event"/>. When <paramref name="fresh"/> is true,
|
||||
/// uses the setup intent ID extracted from the event to retrieve the most up-to-update setup intent from Stripe's API
|
||||
/// and optionally expands it with the provided <see cref="expand"/> options.
|
||||
/// </summary>
|
||||
/// <param name="stripeEvent">The Stripe webhook event.</param>
|
||||
/// <param name="fresh">Determines whether to retrieve a fresh copy of the setup intent object from Stripe.</param>
|
||||
/// <param name="expand">Optionally provided to expand the fresh setup intent object retrieved from Stripe.</param>
|
||||
/// <returns>A Stripe <see cref="SetupIntent"/>.</returns>
|
||||
Task<SetupIntent> GetSetupIntent(Event stripeEvent, bool fresh = false, List<string>? expand = null);
|
||||
|
||||
/// <summary>
|
||||
/// Extracts the <see cref="Subscription"/> object from the Stripe <see cref="Event"/>. When <paramref name="fresh"/> is true,
|
||||
@@ -65,12 +65,10 @@ public interface IStripeEventService
|
||||
/// and optionally expands it with the provided <see cref="expand"/> options.
|
||||
/// </summary>
|
||||
/// <param name="stripeEvent">The Stripe webhook event.</param>
|
||||
/// <param name="fresh">Determines whether or not to retrieve a fresh copy of the subscription object from Stripe.</param>
|
||||
/// <param name="fresh">Determines whether to retrieve a fresh copy of the subscription object from Stripe.</param>
|
||||
/// <param name="expand">Optionally provided to expand the fresh subscription object retrieved from Stripe.</param>
|
||||
/// <returns>A Stripe <see cref="Subscription"/>.</returns>
|
||||
/// <exception cref="Exception">Thrown when the Stripe event does not contain an subscription object.</exception>
|
||||
/// <exception cref="Exception">Thrown when <paramref name="fresh"/> is true and Stripe's API returns a null subscription object.</exception>
|
||||
Task<Subscription> GetSubscription(Event stripeEvent, bool fresh = false, List<string> expand = null);
|
||||
Task<Subscription> GetSubscription(Event stripeEvent, bool fresh = false, List<string>? expand = null);
|
||||
|
||||
/// <summary>
|
||||
/// Ensures that the customer associated with the Stripe <see cref="Event"/> is in the correct region for this server.
|
||||
|
||||
@@ -38,6 +38,12 @@ public interface IStripeFacade
|
||||
RequestOptions requestOptions = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<SetupIntent> GetSetupIntent(
|
||||
string setupIntentId,
|
||||
SetupIntentGetOptions setupIntentGetOptions = null,
|
||||
RequestOptions requestOptions = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<StripeList<Invoice>> ListInvoices(
|
||||
InvoiceListOptions options = null,
|
||||
RequestOptions requestOptions = null,
|
||||
|
||||
@@ -65,3 +65,5 @@ public interface ICustomerUpdatedHandler : IStripeWebhookHandler;
|
||||
/// Defines the contract for handling Stripe Invoice Finalized events.
|
||||
/// </summary>
|
||||
public interface IInvoiceFinalizedHandler : IStripeWebhookHandler;
|
||||
|
||||
public interface ISetupIntentSucceededHandler : IStripeWebhookHandler;
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
#nullable disable
|
||||
|
||||
using Bit.Billing.Constants;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Enums.Provider;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Services;
|
||||
using Stripe;
|
||||
using Event = Stripe.Event;
|
||||
|
||||
@@ -19,41 +17,22 @@ public class PaymentMethodAttachedHandler : IPaymentMethodAttachedHandler
|
||||
private readonly IStripeEventService _stripeEventService;
|
||||
private readonly IStripeFacade _stripeFacade;
|
||||
private readonly IStripeEventUtilityService _stripeEventUtilityService;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly IProviderRepository _providerRepository;
|
||||
|
||||
public PaymentMethodAttachedHandler(
|
||||
ILogger<PaymentMethodAttachedHandler> logger,
|
||||
public PaymentMethodAttachedHandler(ILogger<PaymentMethodAttachedHandler> logger,
|
||||
IStripeEventService stripeEventService,
|
||||
IStripeFacade stripeFacade,
|
||||
IStripeEventUtilityService stripeEventUtilityService,
|
||||
IFeatureService featureService,
|
||||
IProviderRepository providerRepository)
|
||||
{
|
||||
_logger = logger;
|
||||
_stripeEventService = stripeEventService;
|
||||
_stripeFacade = stripeFacade;
|
||||
_stripeEventUtilityService = stripeEventUtilityService;
|
||||
_featureService = featureService;
|
||||
_providerRepository = providerRepository;
|
||||
}
|
||||
|
||||
public async Task HandleAsync(Event parsedEvent)
|
||||
{
|
||||
var updateMSPToChargeAutomatically =
|
||||
_featureService.IsEnabled(FeatureFlagKeys.PM199566_UpdateMSPToChargeAutomatically);
|
||||
|
||||
if (updateMSPToChargeAutomatically)
|
||||
{
|
||||
await HandleVNextAsync(parsedEvent);
|
||||
}
|
||||
else
|
||||
{
|
||||
await HandleVCurrentAsync(parsedEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleVNextAsync(Event parsedEvent)
|
||||
{
|
||||
var paymentMethod = await _stripeEventService.GetPaymentMethod(parsedEvent, true, ["customer.subscriptions.data.latest_invoice"]);
|
||||
|
||||
@@ -136,42 +115,6 @@ public class PaymentMethodAttachedHandler : IPaymentMethodAttachedHandler
|
||||
}
|
||||
}
|
||||
|
||||
private async Task HandleVCurrentAsync(Event parsedEvent)
|
||||
{
|
||||
var paymentMethod = await _stripeEventService.GetPaymentMethod(parsedEvent);
|
||||
if (paymentMethod is null)
|
||||
{
|
||||
_logger.LogWarning("Attempted to handle the event payment_method.attached but paymentMethod was null");
|
||||
return;
|
||||
}
|
||||
|
||||
var subscriptionListOptions = new SubscriptionListOptions
|
||||
{
|
||||
Customer = paymentMethod.CustomerId,
|
||||
Status = StripeSubscriptionStatus.Unpaid,
|
||||
Expand = ["data.latest_invoice"]
|
||||
};
|
||||
|
||||
StripeList<Subscription> unpaidSubscriptions;
|
||||
try
|
||||
{
|
||||
unpaidSubscriptions = await _stripeFacade.ListSubscriptions(subscriptionListOptions);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.LogError(e,
|
||||
"Attempted to get unpaid invoices for customer {CustomerId} but encountered an error while calling Stripe",
|
||||
paymentMethod.CustomerId);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var unpaidSubscription in unpaidSubscriptions)
|
||||
{
|
||||
await AttemptToPayOpenSubscriptionAsync(unpaidSubscription);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AttemptToPayOpenSubscriptionAsync(Subscription unpaidSubscription)
|
||||
{
|
||||
var latestInvoice = unpaidSubscription.LatestInvoice;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user