1
0
mirror of https://github.com/bitwarden/server synced 2026-02-07 04:03:26 +00:00

Merge branch 'main' into renovate/yamldotnet-16.x

This commit is contained in:
Amy Galles
2025-10-08 13:46:34 -07:00
committed by GitHub
743 changed files with 80863 additions and 7695 deletions

View File

@@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"swashbuckle.aspnetcore.cli": {
"version": "9.0.2",
"version": "9.0.4",
"commands": ["swagger"]
},
"dotnet-ef": {

13
.github/CODEOWNERS vendored
View File

@@ -4,11 +4,12 @@
#
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
## Docker files have shared ownership ##
**/Dockerfile
**/*.Dockerfile
**/.dockerignore
**/entrypoint.sh
## Docker-related files
**/Dockerfile @bitwarden/team-appsec @bitwarden/dept-bre
**/*.Dockerfile @bitwarden/team-appsec @bitwarden/dept-bre
**/*.dockerignore @bitwarden/team-appsec @bitwarden/dept-bre
**/docker-compose.yml @bitwarden/team-appsec @bitwarden/dept-bre
**/entrypoint.sh @bitwarden/team-appsec @bitwarden/dept-bre
## BRE team owns these workflows ##
.github/workflows/publish.yml @bitwarden/dept-bre
@@ -92,6 +93,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

View File

@@ -484,7 +484,7 @@ jobs:
uses: bitwarden/gh-actions/azure-logout@main
- name: Trigger self-host build
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
script: |
@@ -525,7 +525,7 @@ jobs:
uses: bitwarden/gh-actions/azure-logout@main
- name: Trigger k8s deploy
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
script: |

112
.github/workflows/load-test.yml vendored Normal file
View 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 }}

View File

@@ -22,7 +22,9 @@ on:
required: false
type: string
permissions: {}
permissions:
pull-requests: write
contents: write
jobs:
setup:
@@ -82,7 +84,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 +202,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 }}
@@ -231,5 +233,10 @@ jobs:
move_edd_db_scripts:
name: Move EDD database scripts
needs: cut_branch
permissions:
actions: read
contents: write
id-token: write
pull-requests: write
uses: ./.github/workflows/_move_edd_db_scripts.yml
secrets: inherit

109
.github/workflows/review-code.yml vendored Normal file
View File

@@ -0,0 +1,109 @@
name: Review code
on:
pull_request:
types: [opened, synchronize, reopened]
permissions: {}
jobs:
review:
name: Review
runs-on: ubuntu-24.04
permissions:
contents: read
id-token: write
pull-requests: write
steps:
- name: Check out repo
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Check for Vault team changes
id: check_changes
run: |
# Ensure we have the base branch
git fetch origin ${{ github.base_ref }}
echo "Comparing changes between origin/${{ github.base_ref }} and HEAD"
CHANGED_FILES=$(git diff --name-only origin/${{ github.base_ref }}...HEAD)
if [ -z "$CHANGED_FILES" ]; then
echo "Zero files changed"
echo "vault_team_changes=false" >> $GITHUB_OUTPUT
exit 0
fi
# Handle variations in spacing and multiple teams
VAULT_PATTERNS=$(grep -E "@bitwarden/team-vault-dev(\s|$)" .github/CODEOWNERS 2>/dev/null | awk '{print $1}')
if [ -z "$VAULT_PATTERNS" ]; then
echo "⚠️ No patterns found for @bitwarden/team-vault-dev in CODEOWNERS"
echo "vault_team_changes=false" >> $GITHUB_OUTPUT
exit 0
fi
vault_team_changes=false
for pattern in $VAULT_PATTERNS; do
echo "Checking pattern: $pattern"
# Handle **/directory patterns
if [[ "$pattern" == "**/"* ]]; then
# Remove the **/ prefix
dir_pattern="${pattern#\*\*/}"
# Check if any file contains this directory in its path
if echo "$CHANGED_FILES" | grep -qE "(^|/)${dir_pattern}(/|$)"; then
vault_team_changes=true
echo "✅ Found files matching pattern: $pattern"
echo "$CHANGED_FILES" | grep -E "(^|/)${dir_pattern}(/|$)" | sed 's/^/ - /'
break
fi
else
# Handle other patterns (shouldn't happen based on your CODEOWNERS)
if echo "$CHANGED_FILES" | grep -q "$pattern"; then
vault_team_changes=true
echo "✅ Found files matching pattern: $pattern"
echo "$CHANGED_FILES" | grep "$pattern" | sed 's/^/ - /'
break
fi
fi
done
echo "vault_team_changes=$vault_team_changes" >> $GITHUB_OUTPUT
if [ "$vault_team_changes" = "true" ]; then
echo ""
echo "✅ Vault team changes detected - proceeding with review"
else
echo ""
echo "❌ No Vault team changes detected - skipping review"
fi
- name: Review with Claude Code
if: steps.check_changes.outputs.vault_team_changes == 'true'
uses: anthropics/claude-code-action@a5528eec7426a4f0c9c1ac96018daa53ebd05bc4 # v1.0.7
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
track_progress: true
prompt: |
REPO: ${{ github.repository }}
PR NUMBER: ${{ github.event.pull_request.number }}
TITLE: ${{ github.event.pull_request.title }}
BODY: ${{ github.event.pull_request.body }}
AUTHOR: ${{ github.event.pull_request.user.login }}
Please review this pull request with a focus on:
- Code quality and best practices
- Potential bugs or issues
- Security implications
- Performance considerations
Note: The PR branch is already checked out in the current working directory.
Provide detailed feedback using inline comments for specific issues.
claude_args: |
--allowedTools "mcp__github_inline_comment__create_inline_comment,Bash(gh pr comment:*),Bash(gh pr diff:*),Bash(gh pr view:*)"

72
CLAUDE.md Normal file
View File

@@ -0,0 +1,72 @@
# Bitwarden Server - Claude Code Configuration
## Critical Rules
- **NEVER** edit: `/bin/`, `/obj/`, `/.git/`, `/.vs/`, `/packages/` which are generated files
- **NEVER** use code regions: If complexity suggests regions, refactor for better readability
- **NEVER** compromise zero-knowledge principles: User vault data must remain encrypted and inaccessible to Bitwarden
- **NEVER** log or expose sensitive data: No PII, passwords, keys, or vault data in logs or error messages
- **ALWAYS** use secure communication channels: Enforce confidentiality, integrity, and authenticity
- **ALWAYS** encrypt sensitive data: All vault data must be encrypted at rest, in transit, and in use
- **ALWAYS** prioritize cryptographic integrity and data protection
- **ALWAYS** add unit tests (with mocking) for any new feature development
## Project Context
- **Architecture**: Feature and team-based organization
- **Framework**: .NET 8.0, ASP.NET Core
- **Database**: SQL Server primary, EF Core supports PostgreSQL, MySQL/MariaDB, SQLite
- **Testing**: xUnit, NSubstitute
- **Container**: Docker, Docker Compose, Kubernetes/Helm deployable
## Project Structure
- **Source Code**: `/src/` - Services and core infrastructure
- **Tests**: `/test/` - Test logic aligning with the source structure, albeit with a `.Test` suffix
- **Utilities**: `/util/` - Migration tools, seeders, and setup scripts
- **Dev Tools**: `/dev/` - Local development helpers
- **Configuration**: `appsettings.{Environment}.json`, `/dev/secrets.json` for local development
## Security Requirements
- **Compliance**: SOC 2 Type II, SOC 3, HIPAA, ISO 27001, GDPR, CCPA
- **Principles**: Zero-knowledge, end-to-end encryption, secure defaults
- **Validation**: Input sanitization, parameterized queries, rate limiting
- **Logging**: Structured logs, no PII/sensitive data in logs
## Common Commands
- **Build**: `dotnet build`
- **Test**: `dotnet test`
- **Run locally**: `dotnet run --project src/Api`
- **Database update**: `pwsh dev/migrate.ps1`
- **Generate OpenAPI**: `pwsh dev/generate_openapi_files.ps1`
## Code Review Checklist
- Security impact assessed
- xUnit tests added / updated
- Performance impact considered
- Error handling implemented
- Breaking changes documented
- CI passes: build, test, lint
- Feature flags considered for new features
- CODEOWNERS file respected
### Key Architectural Decisions
- Use .NET nullable reference types (ADR 0024)
- TryAdd dependency injection pattern (ADR 0026)
- Authorization patterns (ADR 0022)
- OpenTelemetry for observability (ADR 0020)
- Log to standard output (ADR 0021)
## References
- [Server architecture](https://contributing.bitwarden.com/architecture/server/)
- [Architectural Decision Records (ADRs)](https://contributing.bitwarden.com/architecture/adr/)
- [Contributing guidelines](https://contributing.bitwarden.com/contributing/)
- [Setup guide](https://contributing.bitwarden.com/getting-started/server/guide/)
- [Code style](https://contributing.bitwarden.com/contributing/code-style/)
- [Bitwarden security whitepaper](https://bitwarden.com/help/bitwarden-security-white-paper/)
- [Bitwarden security definitions](https://contributing.bitwarden.com/architecture/security/definitions)

View File

@@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Version>2025.8.0</Version>
<Version>2025.10.0</Version>
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>

View File

@@ -12,7 +12,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Payment.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Context;
@@ -90,7 +90,7 @@ public class ProviderService : IProviderService
_providerClientOrganizationSignUpCommand = providerClientOrganizationSignUpCommand;
}
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource = null)
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key, TokenizedPaymentMethod paymentMethod, BillingAddress billingAddress)
{
var owner = await _userService.GetUserByIdAsync(ownerUserId);
if (owner == null)
@@ -115,21 +115,7 @@ public class ProviderService : IProviderService
throw new BadRequestException("Invalid owner.");
}
if (taxInfo == null || string.IsNullOrEmpty(taxInfo.BillingAddressCountry) || string.IsNullOrEmpty(taxInfo.BillingAddressPostalCode))
{
throw new BadRequestException("Both address and postal code are required to set up your provider.");
}
if (tokenizedPaymentSource is not
{
Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal,
Token: not null and not ""
})
{
throw new BadRequestException("A payment method is required to set up your provider.");
}
var customer = await _providerBillingService.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
var customer = await _providerBillingService.SetupCustomer(provider, paymentMethod, billingAddress);
provider.GatewayCustomerId = customer.Id;
var subscription = await _providerBillingService.SetupSubscription(provider);
provider.GatewaySubscriptionId = subscription.Id;

View File

@@ -10,6 +10,7 @@ using Stripe.Tax;
namespace Bit.Commercial.Core.Billing.Providers.Queries;
using static Bit.Core.Constants;
using static StripeConstants;
using SuspensionWarning = ProviderWarnings.SuspensionWarning;
using TaxIdWarning = ProviderWarnings.TaxIdWarning;
@@ -61,6 +62,11 @@ public class GetProviderWarningsQuery(
Provider provider,
Customer customer)
{
if (customer.Address?.Country == CountryAbbreviations.UnitedStates)
{
return null;
}
if (!currentContext.ProviderProviderAdmin(provider.Id))
{
return null;
@@ -75,7 +81,7 @@ public class GetProviderWarningsQuery(
.SelectMany(registrations => registrations.Data);
// Find the matching registration for the customer
var registration = registrations.FirstOrDefault(registration => registration.Country == customer.Address.Country);
var registration = registrations.FirstOrDefault(registration => registration.Country == customer.Address?.Country);
// If we're not registered in their country, we don't need a warning
if (registration == null)

View File

@@ -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;
@@ -13,6 +14,7 @@ using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Payment.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Providers.Entities;
using Bit.Core.Billing.Providers.Models;
@@ -20,10 +22,8 @@ using Bit.Core.Billing.Providers.Repositories;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Tax.Models;
using Bit.Core.Billing.Tax.Services;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
@@ -37,6 +37,9 @@ using Subscription = Stripe.Subscription;
namespace Bit.Commercial.Core.Billing.Providers.Services;
using static Constants;
using static StripeConstants;
public class ProviderBillingService(
IBraintreeGateway braintreeGateway,
IEventService eventService,
@@ -50,8 +53,7 @@ public class ProviderBillingService(
IProviderUserRepository providerUserRepository,
ISetupIntentCache setupIntentCache,
IStripeAdapter stripeAdapter,
ISubscriberService subscriberService,
ITaxService taxService)
ISubscriberService subscriberService)
: IProviderBillingService
{
public async Task AddExistingOrganization(
@@ -60,10 +62,7 @@ public class ProviderBillingService(
string key)
{
await stripeAdapter.SubscriptionUpdateAsync(organization.GatewaySubscriptionId,
new SubscriptionUpdateOptions
{
CancelAtPeriodEnd = false
});
new SubscriptionUpdateOptions { CancelAtPeriodEnd = false });
var subscription =
await stripeAdapter.SubscriptionCancelAsync(organization.GatewaySubscriptionId,
@@ -82,7 +81,7 @@ public class ProviderBillingService(
var wasTrialing = subscription.TrialEnd.HasValue && subscription.TrialEnd.Value > now;
if (!wasTrialing && subscription.LatestInvoice.Status == StripeConstants.InvoiceStatus.Draft)
if (!wasTrialing && subscription.LatestInvoice.Status == InvoiceStatus.Draft)
{
await stripeAdapter.InvoiceFinalizeInvoiceAsync(subscription.LatestInvoiceId,
new InvoiceFinalizeOptions { AutoAdvance = true });
@@ -183,16 +182,8 @@ public class ProviderBillingService(
{
Items =
[
new SubscriptionItemOptions
{
Price = newPriceId,
Quantity = oldSubscriptionItem!.Quantity
},
new SubscriptionItemOptions
{
Id = oldSubscriptionItem.Id,
Deleted = true
}
new SubscriptionItemOptions { Price = newPriceId, Quantity = oldSubscriptionItem!.Quantity },
new SubscriptionItemOptions { Id = oldSubscriptionItem.Id, Deleted = true }
]
};
@@ -201,7 +192,8 @@ public class ProviderBillingService(
// Refactor later to ?ChangeClientPlanCommand? (ProviderPlanId, ProviderId, OrganizationId)
// 1. Retrieve PlanType and PlanName for ProviderPlan
// 2. Assign PlanType & PlanName to Organization
var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(providerPlan.ProviderId);
var providerOrganizations =
await providerOrganizationRepository.GetManyDetailsByProviderAsync(providerPlan.ProviderId);
var newPlan = await pricingClient.GetPlanOrThrow(newPlanType);
@@ -212,6 +204,7 @@ public class ProviderBillingService(
{
throw new ConflictException($"Organization '{providerOrganization.Id}' not found.");
}
organization.PlanType = newPlanType;
organization.Plan = newPlan.Name;
await organizationRepository.ReplaceAsync(organization);
@@ -227,15 +220,15 @@ public class ProviderBillingService(
if (!string.IsNullOrEmpty(organization.GatewayCustomerId))
{
logger.LogWarning("Client organization ({ID}) already has a populated {FieldName}", organization.Id, nameof(organization.GatewayCustomerId));
logger.LogWarning("Client organization ({ID}) already has a populated {FieldName}", organization.Id,
nameof(organization.GatewayCustomerId));
return;
}
var providerCustomer = await subscriberService.GetCustomerOrThrow(provider, new CustomerGetOptions
{
Expand = ["tax", "tax_ids"]
});
var providerCustomer =
await subscriberService.GetCustomerOrThrow(provider,
new CustomerGetOptions { Expand = ["tax", "tax_ids"] });
var providerTaxId = providerCustomer.TaxIds.FirstOrDefault();
@@ -268,23 +261,18 @@ public class ProviderBillingService(
}
]
},
Metadata = new Dictionary<string, string>
{
{ "region", globalSettings.BaseServiceUri.CloudRegion }
},
TaxIdData = providerTaxId == null ? null :
[
new CustomerTaxIdDataOptions
{
Type = providerTaxId.Type,
Value = providerTaxId.Value
}
]
Metadata = new Dictionary<string, string> { { "region", globalSettings.BaseServiceUri.CloudRegion } },
TaxIdData = providerTaxId == null
? null
:
[
new CustomerTaxIdDataOptions { Type = providerTaxId.Type, Value = providerTaxId.Value }
]
};
if (providerCustomer.Address is not { Country: "US" })
if (providerCustomer.Address is not { Country: CountryAbbreviations.UnitedStates })
{
customerCreateOptions.TaxExempt = StripeConstants.TaxExempt.Reverse;
customerCreateOptions.TaxExempt = TaxExempt.Reverse;
}
var customer = await stripeAdapter.CustomerCreateAsync(customerCreateOptions);
@@ -346,9 +334,9 @@ public class ProviderBillingService(
.Where(pair => pair.subscription is
{
Status:
StripeConstants.SubscriptionStatus.Active or
StripeConstants.SubscriptionStatus.Trialing or
StripeConstants.SubscriptionStatus.PastDue
SubscriptionStatus.Active or
SubscriptionStatus.Trialing or
SubscriptionStatus.PastDue
}).ToList();
if (active.Count == 0)
@@ -473,37 +461,27 @@ public class ProviderBillingService(
// Below the limit to above the limit
(currentlyAssignedSeatTotal <= seatMinimum && newlyAssignedSeatTotal > seatMinimum) ||
// Above the limit to further above the limit
(currentlyAssignedSeatTotal > seatMinimum && newlyAssignedSeatTotal > seatMinimum && newlyAssignedSeatTotal > currentlyAssignedSeatTotal);
(currentlyAssignedSeatTotal > seatMinimum && newlyAssignedSeatTotal > seatMinimum &&
newlyAssignedSeatTotal > currentlyAssignedSeatTotal);
}
public async Task<Customer> SetupCustomer(
Provider provider,
TaxInfo taxInfo,
TokenizedPaymentSource tokenizedPaymentSource)
TokenizedPaymentMethod paymentMethod,
BillingAddress billingAddress)
{
ArgumentNullException.ThrowIfNull(tokenizedPaymentSource);
if (taxInfo is not
{
BillingAddressCountry: not null and not "",
BillingAddressPostalCode: not null and not ""
})
{
logger.LogError("Cannot create customer for provider ({ProviderID}) without both a country and postal code", provider.Id);
throw new BillingException();
}
var options = new CustomerCreateOptions
{
Address = new AddressOptions
{
Country = taxInfo.BillingAddressCountry,
PostalCode = taxInfo.BillingAddressPostalCode,
Line1 = taxInfo.BillingAddressLine1,
Line2 = taxInfo.BillingAddressLine2,
City = taxInfo.BillingAddressCity,
State = taxInfo.BillingAddressState
Country = billingAddress.Country,
PostalCode = billingAddress.PostalCode,
Line1 = billingAddress.Line1,
Line2 = billingAddress.Line2,
City = billingAddress.City,
State = billingAddress.State
},
Coupon = !string.IsNullOrEmpty(provider.DiscountId) ? provider.DiscountId : null,
Description = provider.DisplayBusinessName(),
Email = provider.BillingEmail,
InvoiceSettings = new CustomerInvoiceSettingsOptions
@@ -519,93 +497,61 @@ public class ProviderBillingService(
}
]
},
Metadata = new Dictionary<string, string>
{
{ "region", globalSettings.BaseServiceUri.CloudRegion }
}
Metadata = new Dictionary<string, string> { { "region", globalSettings.BaseServiceUri.CloudRegion } },
TaxExempt = billingAddress.Country != CountryAbbreviations.UnitedStates ? TaxExempt.Reverse : TaxExempt.None
};
if (taxInfo.BillingAddressCountry is not "US")
if (billingAddress.TaxId != null)
{
options.TaxExempt = StripeConstants.TaxExempt.Reverse;
}
if (!string.IsNullOrEmpty(taxInfo.TaxIdNumber))
{
var taxIdType = taxService.GetStripeTaxCode(
taxInfo.BillingAddressCountry,
taxInfo.TaxIdNumber);
if (taxIdType == null)
{
logger.LogWarning("Could not infer tax ID type in country '{Country}' with tax ID '{TaxID}'.",
taxInfo.BillingAddressCountry,
taxInfo.TaxIdNumber);
throw new BadRequestException("billingTaxIdTypeInferenceError");
}
options.TaxIdData =
[
new CustomerTaxIdDataOptions { Type = taxIdType, Value = taxInfo.TaxIdNumber }
new CustomerTaxIdDataOptions { Type = billingAddress.TaxId.Code, Value = billingAddress.TaxId.Value }
];
if (taxIdType == StripeConstants.TaxIdType.SpanishNIF)
if (billingAddress.TaxId.Code == TaxIdType.SpanishNIF)
{
options.TaxIdData.Add(new CustomerTaxIdDataOptions
{
Type = StripeConstants.TaxIdType.EUVAT,
Value = $"ES{taxInfo.TaxIdNumber}"
Type = TaxIdType.EUVAT,
Value = $"ES{billingAddress.TaxId.Value}"
});
}
}
if (!string.IsNullOrEmpty(provider.DiscountId))
{
options.Coupon = provider.DiscountId;
}
var braintreeCustomerId = "";
if (tokenizedPaymentSource is not
{
Type: PaymentMethodType.BankAccount or PaymentMethodType.Card or PaymentMethodType.PayPal,
Token: not null and not ""
})
{
logger.LogError("Cannot create customer for provider ({ProviderID}) with invalid payment method", provider.Id);
throw new BillingException();
}
var (type, token) = tokenizedPaymentSource;
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
switch (type)
switch (paymentMethod.Type)
{
case PaymentMethodType.BankAccount:
case TokenizablePaymentMethodType.BankAccount:
{
var setupIntent =
(await stripeAdapter.SetupIntentList(new SetupIntentListOptions { PaymentMethod = token }))
(await stripeAdapter.SetupIntentList(new SetupIntentListOptions
{
PaymentMethod = paymentMethod.Token
}))
.FirstOrDefault();
if (setupIntent == null)
{
logger.LogError("Cannot create customer for provider ({ProviderID}) without a setup intent for their bank account", provider.Id);
logger.LogError(
"Cannot create customer for provider ({ProviderID}) without a setup intent for their bank account",
provider.Id);
throw new BillingException();
}
await setupIntentCache.Set(provider.Id, setupIntent.Id);
break;
}
case PaymentMethodType.Card:
case TokenizablePaymentMethodType.Card:
{
options.PaymentMethod = token;
options.InvoiceSettings.DefaultPaymentMethod = token;
options.PaymentMethod = paymentMethod.Token;
options.InvoiceSettings.DefaultPaymentMethod = paymentMethod.Token;
break;
}
case PaymentMethodType.PayPal:
case TokenizablePaymentMethodType.PayPal:
{
braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(provider, token);
braintreeCustomerId = await subscriberService.CreateBraintreeCustomer(provider, paymentMethod.Token);
options.Metadata[BraintreeCustomerIdKey] = braintreeCustomerId;
break;
}
@@ -615,8 +561,7 @@ public class ProviderBillingService(
{
return await stripeAdapter.CustomerCreateAsync(options);
}
catch (StripeException stripeException) when (stripeException.StripeError?.Code ==
StripeConstants.ErrorCodes.TaxIdInvalid)
catch (StripeException stripeException) when (stripeException.StripeError?.Code == ErrorCodes.TaxIdInvalid)
{
await Revert();
throw new BadRequestException(
@@ -631,17 +576,17 @@ public class ProviderBillingService(
async Task Revert()
{
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
switch (tokenizedPaymentSource.Type)
switch (paymentMethod.Type)
{
case PaymentMethodType.BankAccount:
case TokenizablePaymentMethodType.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):
case TokenizablePaymentMethodType.PayPal when !string.IsNullOrEmpty(braintreeCustomerId):
{
await braintreeGateway.Customer.DeleteAsync(braintreeCustomerId);
break;
@@ -660,9 +605,10 @@ public class ProviderBillingService(
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
if (providerPlans == null || providerPlans.Count == 0)
if (providerPlans.Count == 0)
{
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured plans", provider.Id);
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured plans",
provider.Id);
throw new BillingException();
}
@@ -675,7 +621,9 @@ public class ProviderBillingService(
if (!providerPlan.IsConfigured())
{
logger.LogError("Cannot start subscription for provider ({ProviderID}) that has no configured {ProviderName} plan", provider.Id, plan.Name);
logger.LogError(
"Cannot start subscription for provider ({ProviderID}) that has no configured {ProviderName} plan",
provider.Id, plan.Name);
throw new BillingException();
}
@@ -688,19 +636,17 @@ 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
{
Expand = ["payment_method"]
})
? await stripeAdapter.SetupIntentGet(setupIntentId,
new SetupIntentGetOptions { Expand = ["payment_method"] })
: null;
var usePaymentMethod =
!string.IsNullOrEmpty(customer.InvoiceSettings?.DefaultPaymentMethodId) ||
(customer.Metadata?.ContainsKey(BraintreeCustomerIdKey) == true) ||
(setupIntent?.IsUnverifiedBankAccount() == true);
customer.Metadata?.ContainsKey(BraintreeCustomerIdKey) == true ||
setupIntent?.IsUnverifiedBankAccount() == true;
int? trialPeriodDays = provider.Type switch
{
@@ -711,30 +657,28 @@ public class ProviderBillingService(
var subscriptionCreateOptions = new SubscriptionCreateOptions
{
CollectionMethod = usePaymentMethod ?
StripeConstants.CollectionMethod.ChargeAutomatically : StripeConstants.CollectionMethod.SendInvoice,
CollectionMethod =
usePaymentMethod
? CollectionMethod.ChargeAutomatically
: CollectionMethod.SendInvoice,
Customer = customer.Id,
DaysUntilDue = usePaymentMethod ? null : 30,
Items = subscriptionItemOptionsList,
Metadata = new Dictionary<string, string>
{
{ "providerId", provider.Id.ToString() }
},
Metadata = new Dictionary<string, string> { { "providerId", provider.Id.ToString() } },
OffSession = true,
ProrationBehavior = StripeConstants.ProrationBehavior.CreateProrations,
TrialPeriodDays = trialPeriodDays
ProrationBehavior = ProrationBehavior.CreateProrations,
TrialPeriodDays = trialPeriodDays,
AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true }
};
subscriptionCreateOptions.AutomaticTax = new SubscriptionAutomaticTaxOptions { Enabled = true };
try
{
var subscription = await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
if (subscription is
{
Status: StripeConstants.SubscriptionStatus.Active or StripeConstants.SubscriptionStatus.Trialing
Status: SubscriptionStatus.Active or SubscriptionStatus.Trialing
})
{
return subscription;
@@ -748,9 +692,11 @@ public class ProviderBillingService(
throw new BillingException();
}
catch (StripeException stripeException) when (stripeException.StripeError?.Code == StripeConstants.ErrorCodes.CustomerTaxLocationInvalid)
catch (StripeException stripeException) when (stripeException.StripeError?.Code ==
ErrorCodes.CustomerTaxLocationInvalid)
{
throw new BadRequestException("Your location wasn't recognized. Please ensure your country and postal code are valid.");
throw new BadRequestException(
"Your location wasn't recognized. Please ensure your country and postal code are valid.");
}
}
@@ -764,7 +710,7 @@ public class ProviderBillingService(
subscriberService.UpdateTaxInformation(provider, taxInformation));
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
new SubscriptionUpdateOptions { CollectionMethod = StripeConstants.CollectionMethod.ChargeAutomatically });
new SubscriptionUpdateOptions { CollectionMethod = CollectionMethod.ChargeAutomatically });
}
public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command)
@@ -864,13 +810,9 @@ public class ProviderBillingService(
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId, new SubscriptionUpdateOptions
{
Items = [
new SubscriptionItemOptions
{
Id = item.Id,
Price = priceId,
Quantity = newlySubscribedSeats
}
Items =
[
new SubscriptionItemOptions { Id = item.Id, Price = priceId, Quantity = newlySubscribedSeats }
]
});
@@ -893,7 +835,8 @@ public class ProviderBillingService(
var plan = await pricingClient.GetPlanOrThrow(planType);
return providerOrganizations
.Where(providerOrganization => providerOrganization.Plan == plan.Name && providerOrganization.Status == OrganizationStatusType.Managed)
.Where(providerOrganization => providerOrganization.Plan == plan.Name &&
providerOrganization.Status == OrganizationStatusType.Managed)
.Sum(providerOrganization => providerOrganization.Seats ?? 0);
}

View File

@@ -5,7 +5,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CsvHelper" Version="33.0.1" />
<PackageReference Include="CsvHelper" Version="33.1.0" />
</ItemGroup>
</Project>

View File

@@ -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;

View File

@@ -1,10 +1,13 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.SecretsManager.Commands.ServiceAccounts.Interfaces;
using Bit.Core.SecretsManager.Entities;
using Bit.Core.SecretsManager.Repositories;
using Bit.Core.Services;
namespace Bit.Commercial.Core.SecretsManager.Commands.ServiceAccounts;
@@ -13,15 +16,21 @@ public class CreateServiceAccountCommand : ICreateServiceAccountCommand
private readonly IAccessPolicyRepository _accessPolicyRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
private readonly IEventService _eventService;
private readonly ICurrentContext _currentContext;
public CreateServiceAccountCommand(
IAccessPolicyRepository accessPolicyRepository,
IOrganizationUserRepository organizationUserRepository,
IServiceAccountRepository serviceAccountRepository)
IServiceAccountRepository serviceAccountRepository,
IEventService eventService,
ICurrentContext currentContext)
{
_accessPolicyRepository = accessPolicyRepository;
_organizationUserRepository = organizationUserRepository;
_serviceAccountRepository = serviceAccountRepository;
_eventService = eventService;
_currentContext = currentContext;
}
public async Task<ServiceAccount> CreateAsync(ServiceAccount serviceAccount, Guid userId)
@@ -38,6 +47,7 @@ public class CreateServiceAccountCommand : ICreateServiceAccountCommand
Write = true,
};
await _accessPolicyRepository.CreateManyAsync(new List<BaseAccessPolicy> { accessPolicy });
await _eventService.LogServiceAccountPeopleEventAsync(user.Id, accessPolicy, EventType.ServiceAccount_UserAdded, _currentContext.IdentityClientType);
return createdServiceAccount;
}
}

View File

@@ -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.

View File

@@ -10,7 +10,7 @@
<!-- This is a transitive dependency to Sustainsys.Saml2.AspNetCore2 -->
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Sustainsys.Saml2.AspNetCore2" Version="2.10.0" />
<PackageReference Include="Sustainsys.Saml2.AspNetCore2" Version="2.11.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -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);

View File

@@ -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": {

View File

@@ -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"
}
}

View File

@@ -9,7 +9,7 @@ using Bit.Core.AdminConsole.Models.Data.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.Organizations;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Payment.Models;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Context;
@@ -41,7 +41,7 @@ public class ProviderServiceTests
public async Task CompleteSetupAsync_UserIdIsInvalid_Throws(SutProvider<ProviderService> sutProvider)
{
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.CompleteSetupAsync(default, default, default, default, null));
() => sutProvider.Sut.CompleteSetupAsync(default, default, default, default, null, null));
Assert.Contains("Invalid owner.", exception.Message);
}
@@ -53,83 +53,12 @@ public class ProviderServiceTests
userService.GetUserByIdAsync(user.Id).Returns(user);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.CompleteSetupAsync(provider, user.Id, default, default, null));
() => sutProvider.Sut.CompleteSetupAsync(provider, user.Id, default, default, null, null));
Assert.Contains("Invalid token.", exception.Message);
}
[Theory, BitAutoData]
public async Task CompleteSetupAsync_InvalidTaxInfo_ThrowsBadRequestException(
User user,
Provider provider,
string key,
TaxInfo taxInfo,
TokenizedPaymentSource tokenizedPaymentSource,
[ProviderUser] ProviderUser providerUser,
SutProvider<ProviderService> sutProvider)
{
providerUser.ProviderId = provider.Id;
providerUser.UserId = user.Id;
var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByIdAsync(user.Id).Returns(user);
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser);
var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName");
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
.Returns(protector);
sutProvider.Create();
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
taxInfo.BillingAddressCountry = null;
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource));
Assert.Equal("Both address and postal code are required to set up your provider.", exception.Message);
}
[Theory, BitAutoData]
public async Task CompleteSetupAsync_InvalidTokenizedPaymentSource_ThrowsBadRequestException(
User user,
Provider provider,
string key,
TaxInfo taxInfo,
TokenizedPaymentSource tokenizedPaymentSource,
[ProviderUser] ProviderUser providerUser,
SutProvider<ProviderService> sutProvider)
{
providerUser.ProviderId = provider.Id;
providerUser.UserId = user.Id;
var userService = sutProvider.GetDependency<IUserService>();
userService.GetUserByIdAsync(user.Id).Returns(user);
var providerUserRepository = sutProvider.GetDependency<IProviderUserRepository>();
providerUserRepository.GetByProviderUserAsync(provider.Id, user.Id).Returns(providerUser);
var dataProtectionProvider = DataProtectionProvider.Create("ApplicationName");
var protector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
sutProvider.GetDependency<IDataProtectionProvider>().CreateProtector("ProviderServiceDataProtector")
.Returns(protector);
sutProvider.Create();
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
tokenizedPaymentSource = tokenizedPaymentSource with { Type = PaymentMethodType.BitPay };
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource));
Assert.Equal("A payment method is required to set up your provider.", exception.Message);
}
[Theory, BitAutoData]
public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, TaxInfo taxInfo, TokenizedPaymentSource tokenizedPaymentSource,
public async Task CompleteSetupAsync_Success(User user, Provider provider, string key, TokenizedPaymentMethod tokenizedPaymentMethod, BillingAddress billingAddress,
[ProviderUser] ProviderUser providerUser,
SutProvider<ProviderService> sutProvider)
{
@@ -149,7 +78,7 @@ public class ProviderServiceTests
var providerBillingService = sutProvider.GetDependency<IProviderBillingService>();
var customer = new Customer { Id = "customer_id" };
providerBillingService.SetupCustomer(provider, taxInfo, tokenizedPaymentSource).Returns(customer);
providerBillingService.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress).Returns(customer);
var subscription = new Subscription { Id = "subscription_id" };
providerBillingService.SetupSubscription(provider).Returns(subscription);
@@ -158,7 +87,7 @@ public class ProviderServiceTests
var token = protector.Protect($"ProviderSetupInvite {provider.Id} {user.Email} {CoreHelpers.ToEpocMilliseconds(DateTime.UtcNow)}");
await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, taxInfo, tokenizedPaymentSource);
await sutProvider.Sut.CompleteSetupAsync(provider, user.Id, token, key, tokenizedPaymentMethod, billingAddress);
await sutProvider.GetDependency<IProviderRepository>().Received().UpsertAsync(Arg.Is<Provider>(
p =>

View File

@@ -58,7 +58,7 @@ public class GetProviderWarningsQueryTests
Customer = new Customer
{
TaxIds = new StripeList<TaxId> { Data = [] },
Address = new Address { Country = "US" }
Address = new Address { Country = "CA" }
}
});
@@ -90,7 +90,7 @@ public class GetProviderWarningsQueryTests
Customer = new Customer
{
TaxIds = new StripeList<TaxId> { Data = [] },
Address = new Address { Country = "US" }
Address = new Address { Country = "CA" }
}
});
@@ -124,7 +124,7 @@ public class GetProviderWarningsQueryTests
Customer = new Customer
{
TaxIds = new StripeList<TaxId> { Data = [] },
Address = new Address { Country = "US" }
Address = new Address { Country = "CA" }
}
});
@@ -158,7 +158,7 @@ public class GetProviderWarningsQueryTests
Customer = new Customer
{
TaxIds = new StripeList<TaxId> { Data = [] },
Address = new Address { Country = "US" }
Address = new Address { Country = "CA" }
}
});
@@ -191,7 +191,7 @@ public class GetProviderWarningsQueryTests
Customer = new Customer
{
TaxIds = new StripeList<TaxId> { Data = [] },
Address = new Address { Country = "US" }
Address = new Address { Country = "CA" }
}
});
@@ -219,7 +219,7 @@ public class GetProviderWarningsQueryTests
Customer = new Customer
{
TaxIds = new StripeList<TaxId> { Data = [] },
Address = new Address { Country = "US" }
Address = new Address { Country = "CA" }
}
});
@@ -227,7 +227,7 @@ public class GetProviderWarningsQueryTests
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = [new Registration { Country = "CA" }]
Data = [new Registration { Country = "GB" }]
});
var response = await sutProvider.Sut.Run(provider);
@@ -252,7 +252,7 @@ public class GetProviderWarningsQueryTests
Customer = new Customer
{
TaxIds = new StripeList<TaxId> { Data = [] },
Address = new Address { Country = "US" }
Address = new Address { Country = "CA" }
}
});
@@ -260,7 +260,7 @@ public class GetProviderWarningsQueryTests
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = [new Registration { Country = "US" }]
Data = [new Registration { Country = "CA" }]
});
var response = await sutProvider.Sut.Run(provider);
@@ -291,7 +291,7 @@ public class GetProviderWarningsQueryTests
{
Data = [new TaxId { Verification = null }]
},
Address = new Address { Country = "US" }
Address = new Address { Country = "CA" }
}
});
@@ -299,7 +299,7 @@ public class GetProviderWarningsQueryTests
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = [new Registration { Country = "US" }]
Data = [new Registration { Country = "CA" }]
});
var response = await sutProvider.Sut.Run(provider);
@@ -333,7 +333,7 @@ public class GetProviderWarningsQueryTests
}
}]
},
Address = new Address { Country = "US" }
Address = new Address { Country = "CA" }
}
});
@@ -341,7 +341,7 @@ public class GetProviderWarningsQueryTests
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = [new Registration { Country = "US" }]
Data = [new Registration { Country = "CA" }]
});
var response = await sutProvider.Sut.Run(provider);
@@ -378,7 +378,7 @@ public class GetProviderWarningsQueryTests
}
}]
},
Address = new Address { Country = "US" }
Address = new Address { Country = "CA" }
}
});
@@ -386,7 +386,7 @@ public class GetProviderWarningsQueryTests
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = [new Registration { Country = "US" }]
Data = [new Registration { Country = "CA" }]
});
var response = await sutProvider.Sut.Run(provider);
@@ -423,7 +423,7 @@ public class GetProviderWarningsQueryTests
}
}]
},
Address = new Address { Country = "US" }
Address = new Address { Country = "CA" }
}
});
@@ -431,7 +431,7 @@ public class GetProviderWarningsQueryTests
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = [new Registration { Country = "US" }]
Data = [new Registration { Country = "CA" }]
});
var response = await sutProvider.Sut.Run(provider);
@@ -498,6 +498,44 @@ public class GetProviderWarningsQueryTests
Status = SubscriptionStatus.Unpaid,
CancelAt = cancelAt,
Customer = new Customer
{
TaxIds = new StripeList<TaxId> { Data = [] },
Address = new Address { Country = "CA" }
}
});
sutProvider.GetDependency<ICurrentContext>().ProviderProviderAdmin(provider.Id).Returns(true);
sutProvider.GetDependency<IStripeAdapter>().TaxRegistrationsListAsync(Arg.Any<RegistrationListOptions>())
.Returns(new StripeList<Registration>
{
Data = [new Registration { Country = "CA" }]
});
var response = await sutProvider.Sut.Run(provider);
Assert.True(response is
{
Suspension.Resolution: "add_payment_method",
TaxId.Type: "tax_id_missing"
});
Assert.Equal(cancelAt, response.Suspension.SubscriptionCancelsAt);
}
[Theory, BitAutoData]
public async Task Run_USCustomer_NoTaxIdWarning(
Provider provider,
SutProvider<GetProviderWarningsQuery> sutProvider)
{
provider.Enabled = true;
sutProvider.GetDependency<ISubscriberService>()
.GetSubscription(provider, Arg.Is<SubscriptionGetOptions>(options =>
options.Expand.SequenceEqual(_requiredExpansions)
))
.Returns(new Subscription
{
Status = SubscriptionStatus.Active,
Customer = new Customer
{
TaxIds = new StripeList<TaxId> { Data = [] },
Address = new Address { Country = "US" }
@@ -513,11 +551,6 @@ public class GetProviderWarningsQueryTests
var response = await sutProvider.Sut.Run(provider);
Assert.True(response is
{
Suspension.Resolution: "add_payment_method",
TaxId.Type: "tax_id_missing"
});
Assert.Equal(cancelAt, response.Suspension.SubscriptionCancelsAt);
Assert.Null(response!.TaxId);
}
}

View File

@@ -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;

View File

@@ -1,5 +1,4 @@
using System.Globalization;
using System.Net;
using Bit.Commercial.Core.Billing.Providers.Models;
using Bit.Commercial.Core.Billing.Providers.Services;
using Bit.Core.AdminConsole.Entities;
@@ -10,17 +9,16 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Payment.Models;
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;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
@@ -894,170 +892,97 @@ public class ProviderBillingServiceTests
#region SetupCustomer
[Theory, BitAutoData]
public async Task SetupCustomer_MissingCountry_ContactSupport(
public async Task SetupCustomer_NullPaymentMethod_ThrowsNullReferenceException(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo,
TokenizedPaymentSource tokenizedPaymentSource)
BillingAddress billingAddress)
{
taxInfo.BillingAddressCountry = null;
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
await sutProvider.GetDependency<IStripeAdapter>()
.DidNotReceiveWithAnyArgs()
.CustomerGetAsync(Arg.Any<string>(), Arg.Any<CustomerGetOptions>());
}
[Theory, BitAutoData]
public async Task SetupCustomer_MissingPostalCode_ContactSupport(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo,
TokenizedPaymentSource tokenizedPaymentSource)
{
taxInfo.BillingAddressCountry = null;
await ThrowsBillingExceptionAsync(() => sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
await sutProvider.GetDependency<IStripeAdapter>()
.DidNotReceiveWithAnyArgs()
.CustomerGetAsync(Arg.Any<string>(), Arg.Any<CustomerGetOptions>());
}
[Theory, BitAutoData]
public async Task SetupCustomer_NullPaymentSource_ThrowsArgumentNullException(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo)
{
await Assert.ThrowsAsync<ArgumentNullException>(() =>
sutProvider.Sut.SetupCustomer(provider, taxInfo, null));
}
[Theory, BitAutoData]
public async Task SetupCustomer_InvalidRequiredPaymentMethod_ThrowsBillingException(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo,
TokenizedPaymentSource tokenizedPaymentSource)
{
provider.Name = "MSP";
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns(taxInfo.TaxIdType);
taxInfo.BillingAddressCountry = "AD";
tokenizedPaymentSource = tokenizedPaymentSource with { Type = PaymentMethodType.BitPay };
await ThrowsBillingExceptionAsync(() =>
sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
await Assert.ThrowsAsync<NullReferenceException>(() =>
sutProvider.Sut.SetupCustomer(provider, null, billingAddress));
}
[Theory, BitAutoData]
public async Task SetupCustomer_WithBankAccount_Error_Reverts(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo)
BillingAddress billingAddress)
{
provider.Name = "MSP";
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns(taxInfo.TaxIdType);
taxInfo.BillingAddressCountry = "AD";
billingAddress.Country = "AD";
billingAddress.TaxId = new TaxID("es_nif", "12345678Z");
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.BankAccount, "token");
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.BankAccount, Token = "token" };
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options =>
options.PaymentMethod == tokenizedPaymentSource.Token)).Returns([
options.PaymentMethod == tokenizedPaymentMethod.Token)).Returns([
new SetupIntent { Id = "setup_intent_id" }
]);
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
o.Address.Country == taxInfo.BillingAddressCountry &&
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
o.Address.City == taxInfo.BillingAddressCity &&
o.Address.State == taxInfo.BillingAddressState &&
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
o.Address.Country == billingAddress.Country &&
o.Address.PostalCode == billingAddress.PostalCode &&
o.Address.Line1 == billingAddress.Line1 &&
o.Address.Line2 == billingAddress.Line2 &&
o.Address.City == billingAddress.City &&
o.Address.State == billingAddress.State &&
o.Description == provider.DisplayBusinessName() &&
o.Email == provider.BillingEmail &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&
o.Metadata["region"] == "" &&
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&
o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value))
.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));
sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress));
await sutProvider.GetDependency<ISetupIntentCache>().Received(1).Set(provider.Id, "setup_intent_id");
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]
public async Task SetupCustomer_WithPayPal_Error_Reverts(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo)
BillingAddress billingAddress)
{
provider.Name = "MSP";
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns(taxInfo.TaxIdType);
taxInfo.BillingAddressCountry = "AD";
billingAddress.Country = "AD";
billingAddress.TaxId = new TaxID("es_nif", "12345678Z");
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.PayPal, Token = "token" };
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.PayPal, "token");
sutProvider.GetDependency<ISubscriberService>().CreateBraintreeCustomer(provider, tokenizedPaymentSource.Token)
sutProvider.GetDependency<ISubscriberService>().CreateBraintreeCustomer(provider, tokenizedPaymentMethod.Token)
.Returns("braintree_customer_id");
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
o.Address.Country == taxInfo.BillingAddressCountry &&
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
o.Address.City == taxInfo.BillingAddressCity &&
o.Address.State == taxInfo.BillingAddressState &&
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
o.Address.Country == billingAddress.Country &&
o.Address.PostalCode == billingAddress.PostalCode &&
o.Address.Line1 == billingAddress.Line1 &&
o.Address.Line2 == billingAddress.Line2 &&
o.Address.City == billingAddress.City &&
o.Address.State == billingAddress.State &&
o.Description == provider.DisplayBusinessName() &&
o.Email == provider.BillingEmail &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&
o.Metadata["region"] == "" &&
o.Metadata["btCustomerId"] == "braintree_customer_id" &&
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&
o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value))
.Throws<StripeException>();
await Assert.ThrowsAsync<StripeException>(() =>
sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress));
await sutProvider.GetDependency<IBraintreeGateway>().Customer.Received(1).DeleteAsync("braintree_customer_id");
}
@@ -1066,17 +991,11 @@ public class ProviderBillingServiceTests
public async Task SetupCustomer_WithBankAccount_Success(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo)
BillingAddress billingAddress)
{
provider.Name = "MSP";
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns(taxInfo.TaxIdType);
taxInfo.BillingAddressCountry = "AD";
billingAddress.Country = "AD";
billingAddress.TaxId = new TaxID("es_nif", "12345678Z");
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
@@ -1086,31 +1005,30 @@ public class ProviderBillingServiceTests
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
};
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.BankAccount, "token");
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.BankAccount, Token = "token" };
stripeAdapter.SetupIntentList(Arg.Is<SetupIntentListOptions>(options =>
options.PaymentMethod == tokenizedPaymentSource.Token)).Returns([
options.PaymentMethod == tokenizedPaymentMethod.Token)).Returns([
new SetupIntent { Id = "setup_intent_id" }
]);
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
o.Address.Country == taxInfo.BillingAddressCountry &&
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
o.Address.City == taxInfo.BillingAddressCity &&
o.Address.State == taxInfo.BillingAddressState &&
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
o.Address.Country == billingAddress.Country &&
o.Address.PostalCode == billingAddress.PostalCode &&
o.Address.Line1 == billingAddress.Line1 &&
o.Address.Line2 == billingAddress.Line2 &&
o.Address.City == billingAddress.City &&
o.Address.State == billingAddress.State &&
o.Description == provider.DisplayBusinessName() &&
o.Email == provider.BillingEmail &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&
o.Metadata["region"] == "" &&
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&
o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value))
.Returns(expected);
var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
var actual = await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress);
Assert.Equivalent(expected, actual);
@@ -1121,17 +1039,11 @@ public class ProviderBillingServiceTests
public async Task SetupCustomer_WithPayPal_Success(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo)
BillingAddress billingAddress)
{
provider.Name = "MSP";
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns(taxInfo.TaxIdType);
taxInfo.BillingAddressCountry = "AD";
billingAddress.Country = "AD";
billingAddress.TaxId = new TaxID("es_nif", "12345678Z");
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
@@ -1141,30 +1053,29 @@ public class ProviderBillingServiceTests
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
};
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.PayPal, "token");
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.PayPal, Token = "token" };
sutProvider.GetDependency<ISubscriberService>().CreateBraintreeCustomer(provider, tokenizedPaymentSource.Token)
sutProvider.GetDependency<ISubscriberService>().CreateBraintreeCustomer(provider, tokenizedPaymentMethod.Token)
.Returns("braintree_customer_id");
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
o.Address.Country == taxInfo.BillingAddressCountry &&
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
o.Address.City == taxInfo.BillingAddressCity &&
o.Address.State == taxInfo.BillingAddressState &&
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
o.Address.Country == billingAddress.Country &&
o.Address.PostalCode == billingAddress.PostalCode &&
o.Address.Line1 == billingAddress.Line1 &&
o.Address.Line2 == billingAddress.Line2 &&
o.Address.City == billingAddress.City &&
o.Address.State == billingAddress.State &&
o.Description == provider.DisplayBusinessName() &&
o.Email == provider.BillingEmail &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&
o.Metadata["region"] == "" &&
o.Metadata["btCustomerId"] == "braintree_customer_id" &&
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&
o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value))
.Returns(expected);
var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
var actual = await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress);
Assert.Equivalent(expected, actual);
}
@@ -1173,17 +1084,11 @@ public class ProviderBillingServiceTests
public async Task SetupCustomer_WithCard_Success(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo)
BillingAddress billingAddress)
{
provider.Name = "MSP";
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns(taxInfo.TaxIdType);
taxInfo.BillingAddressCountry = "AD";
billingAddress.Country = "AD";
billingAddress.TaxId = new TaxID("es_nif", "12345678Z");
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
@@ -1193,28 +1098,26 @@ public class ProviderBillingServiceTests
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
};
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.Card, "token");
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = "token" };
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
o.Address.Country == taxInfo.BillingAddressCountry &&
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
o.Address.City == taxInfo.BillingAddressCity &&
o.Address.State == taxInfo.BillingAddressState &&
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
o.Address.Country == billingAddress.Country &&
o.Address.PostalCode == billingAddress.PostalCode &&
o.Address.Line1 == billingAddress.Line1 &&
o.Address.Line2 == billingAddress.Line2 &&
o.Address.City == billingAddress.City &&
o.Address.State == billingAddress.State &&
o.Description == provider.DisplayBusinessName() &&
o.Email == provider.BillingEmail &&
o.PaymentMethod == tokenizedPaymentSource.Token &&
o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentSource.Token &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentMethod.Token &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&
o.Metadata["region"] == "" &&
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber))
o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&
o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value))
.Returns(expected);
var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
var actual = await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress);
Assert.Equivalent(expected, actual);
}
@@ -1223,17 +1126,11 @@ public class ProviderBillingServiceTests
public async Task SetupCustomer_WithCard_ReverseCharge_Success(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo)
BillingAddress billingAddress)
{
provider.Name = "MSP";
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns(taxInfo.TaxIdType);
taxInfo.BillingAddressCountry = "AD";
billingAddress.Country = "FR"; // Non-US country to trigger reverse charge
billingAddress.TaxId = new TaxID("fr_siren", "123456789");
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
@@ -1243,55 +1140,51 @@ public class ProviderBillingServiceTests
Tax = new CustomerTax { AutomaticTax = StripeConstants.AutomaticTaxStatus.Supported }
};
var tokenizedPaymentSource = new TokenizedPaymentSource(PaymentMethodType.Card, "token");
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = "token" };
stripeAdapter.CustomerCreateAsync(Arg.Is<CustomerCreateOptions>(o =>
o.Address.Country == taxInfo.BillingAddressCountry &&
o.Address.PostalCode == taxInfo.BillingAddressPostalCode &&
o.Address.Line1 == taxInfo.BillingAddressLine1 &&
o.Address.Line2 == taxInfo.BillingAddressLine2 &&
o.Address.City == taxInfo.BillingAddressCity &&
o.Address.State == taxInfo.BillingAddressState &&
o.Description == WebUtility.HtmlDecode(provider.BusinessName) &&
o.Address.Country == billingAddress.Country &&
o.Address.PostalCode == billingAddress.PostalCode &&
o.Address.Line1 == billingAddress.Line1 &&
o.Address.Line2 == billingAddress.Line2 &&
o.Address.City == billingAddress.City &&
o.Address.State == billingAddress.State &&
o.Description == provider.DisplayBusinessName() &&
o.Email == provider.BillingEmail &&
o.PaymentMethod == tokenizedPaymentSource.Token &&
o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentSource.Token &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == "Provider" &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == "MSP" &&
o.InvoiceSettings.DefaultPaymentMethod == tokenizedPaymentMethod.Token &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Name == provider.SubscriberType() &&
o.InvoiceSettings.CustomFields.FirstOrDefault().Value == provider.DisplayName() &&
o.Metadata["region"] == "" &&
o.TaxIdData.FirstOrDefault().Type == taxInfo.TaxIdType &&
o.TaxIdData.FirstOrDefault().Value == taxInfo.TaxIdNumber &&
o.TaxIdData.FirstOrDefault().Type == billingAddress.TaxId.Code &&
o.TaxIdData.FirstOrDefault().Value == billingAddress.TaxId.Value &&
o.TaxExempt == StripeConstants.TaxExempt.Reverse))
.Returns(expected);
var actual = await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource);
var actual = await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress);
Assert.Equivalent(expected, actual);
}
[Theory, BitAutoData]
public async Task SetupCustomer_Throws_BadRequestException_WhenTaxIdIsInvalid(
public async Task SetupCustomer_WithInvalidTaxId_ThrowsBadRequestException(
SutProvider<ProviderBillingService> sutProvider,
Provider provider,
TaxInfo taxInfo,
TokenizedPaymentSource tokenizedPaymentSource)
BillingAddress billingAddress)
{
provider.Name = "MSP";
billingAddress.Country = "AD";
billingAddress.TaxId = new TaxID("es_nif", "invalid_tax_id");
taxInfo.BillingAddressCountry = "AD";
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
var tokenizedPaymentMethod = new TokenizedPaymentMethod { Type = TokenizablePaymentMethodType.Card, Token = "token" };
sutProvider.GetDependency<ITaxService>()
.GetStripeTaxCode(Arg.Is<string>(
p => p == taxInfo.BillingAddressCountry),
Arg.Is<string>(p => p == taxInfo.TaxIdNumber))
.Returns((string)null);
stripeAdapter.CustomerCreateAsync(Arg.Any<CustomerCreateOptions>())
.Throws(new StripeException("Invalid tax ID") { StripeError = new StripeError { Code = "tax_id_invalid" } });
var actual = await Assert.ThrowsAsync<BadRequestException>(async () =>
await sutProvider.Sut.SetupCustomer(provider, taxInfo, tokenizedPaymentSource));
await sutProvider.Sut.SetupCustomer(provider, tokenizedPaymentMethod, billingAddress));
Assert.IsType<BadRequestException>(actual);
Assert.Equal("billingTaxIdTypeInferenceError", actual.Message);
Assert.Equal("Your tax ID wasn't recognized for your selected country. Please ensure your country and tax ID are valid.", actual.Message);
}
#endregion
@@ -1643,7 +1536,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

View File

@@ -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;

View File

@@ -53,6 +53,7 @@ services:
- ./.data/postgres/log:/var/log/postgresql
profiles:
- postgres
- ef
mysql:
image: mysql:8.0
@@ -69,6 +70,7 @@ services:
- mysql_dev_data:/var/lib/mysql
profiles:
- mysql
- ef
mariadb:
image: mariadb:10
@@ -76,13 +78,13 @@ services:
- 4306:3306
environment:
MARIADB_USER: maria
MARIADB_PASSWORD: ${MARIADB_ROOT_PASSWORD}
MARIADB_DATABASE: vault_dev
MARIADB_RANDOM_ROOT_PASSWORD: "true"
volumes:
- mariadb_dev_data:/var/lib/mysql
profiles:
- mariadb
- ef
idp:
image: kenchan0130/simplesamlphp:1.19.8
@@ -99,7 +101,7 @@ services:
- idp
rabbitmq:
image: rabbitmq:4.1.0-management
image: rabbitmq:4.1.3-management
container_name: rabbitmq
ports:
- "5672:5672"
@@ -153,5 +155,6 @@ volumes:
mssql_dev_data:
postgres_dev_data:
mysql_dev_data:
mariadb_dev_data:
rabbitmq_data:
redis_data:

View File

@@ -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
}

View File

@@ -70,7 +70,7 @@ Foreach ($item in @(
@($mysql, "MySQL", "MySqlMigrations", "mySql", 2),
# MariaDB shares the MySQL connection string in the server config so they are mutually exclusive in that context.
# However they can still be run independently for integration tests.
@($mariadb, "MariaDB", "MySqlMigrations", "mySql", 3)
@($mariadb, "MariaDB", "MySqlMigrations", "mySql", 4)
)) {
if (!$item[0] -and !$all) {
continue

View File

@@ -33,6 +33,8 @@
"id": "<your Installation Id>",
"key": "<your Installation Key>"
},
"licenseDirectory": "<full path to license directory>"
"licenseDirectory": "<full path to license directory>",
"enableNewDeviceVerification": true,
"enableEmailVerification": true
}
}

View File

@@ -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"
}
}
}
]
}
]
}

View File

@@ -7,7 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
<PackageReference Include="BenchmarkDotNet" Version="0.15.3" />
</ItemGroup>
<ItemGroup>

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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,

View File

@@ -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>

View File

@@ -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">

View File

@@ -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": {

View File

@@ -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"
}
}

View File

@@ -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>(),
]);
}
}

View File

@@ -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;

View File

@@ -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;

View 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);
}
}

View File

@@ -30,6 +30,8 @@ public class EventsController : Controller
private readonly ICurrentContext _currentContext;
private readonly ISecretRepository _secretRepository;
private readonly IProjectRepository _projectRepository;
private readonly IServiceAccountRepository _serviceAccountRepository;
public EventsController(
IUserService userService,
@@ -39,7 +41,8 @@ public class EventsController : Controller
IEventRepository eventRepository,
ICurrentContext currentContext,
ISecretRepository secretRepository,
IProjectRepository projectRepository)
IProjectRepository projectRepository,
IServiceAccountRepository serviceAccountRepository)
{
_userService = userService;
_cipherRepository = cipherRepository;
@@ -49,6 +52,7 @@ public class EventsController : Controller
_currentContext = currentContext;
_secretRepository = secretRepository;
_projectRepository = projectRepository;
_serviceAccountRepository = serviceAccountRepository;
}
[HttpGet("")]
@@ -184,6 +188,57 @@ public class EventsController : Controller
return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);
}
[HttpGet("~/organization/{orgId}/service-account/{id}/events")]
public async Task<ListResponseModel<EventResponseModel>> GetServiceAccounts(
Guid orgId,
Guid id,
[FromQuery] DateTime? start = null,
[FromQuery] DateTime? end = null,
[FromQuery] string continuationToken = null)
{
if (id == Guid.Empty || orgId == Guid.Empty)
{
throw new NotFoundException();
}
var serviceAccount = await GetServiceAccount(id, orgId);
var org = _currentContext.GetOrganization(orgId);
if (org == null || !await _currentContext.AccessEventLogs(org.Id))
{
throw new NotFoundException();
}
var (fromDate, toDate) = ApiHelpers.GetDateRange(start, end);
var result = await _eventRepository.GetManyByOrganizationServiceAccountAsync(
serviceAccount.OrganizationId,
serviceAccount.Id,
fromDate,
toDate,
new PageOptions { ContinuationToken = continuationToken });
var responses = result.Data.Select(e => new EventResponseModel(e));
return new ListResponseModel<EventResponseModel>(responses, result.ContinuationToken);
}
[ApiExplorerSettings(IgnoreApi = true)]
private async Task<ServiceAccount> GetServiceAccount(Guid serviceAccountId, Guid orgId)
{
var serviceAccount = await _serviceAccountRepository.GetByIdAsync(serviceAccountId);
if (serviceAccount != null)
{
return serviceAccount;
}
var fallbackServiceAccount = new ServiceAccount
{
Id = serviceAccountId,
OrganizationId = orgId
};
return fallbackServiceAccount;
}
[HttpGet("~/organizations/{orgId}/users/{id}/events")]
public async Task<ListResponseModel<EventResponseModel>> GetOrganizationUser(string orgId, string id,
[FromQuery] DateTime? start = null, [FromQuery] DateTime? end = null, [FromQuery] string continuationToken = null)

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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(

View File

@@ -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);

View File

@@ -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);

View File

@@ -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.DeleteClaimedAccount;
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;
@@ -167,7 +169,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 +362,6 @@ public class OrganizationUsersController : Controller
}
[HttpPut("{id}")]
[HttpPost("{id}")]
[Authorize<ManageUsersRequirement>]
public async Task Put(Guid orgId, Guid id, [FromBody] OrganizationUserUpdateRequestModel model)
{
@@ -436,6 +437,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 +501,6 @@ public class OrganizationUsersController : Controller
}
[HttpDelete("{id}")]
[HttpPost("{id}/remove")]
[Authorize<ManageUsersRequirement>]
public async Task Remove(Guid orgId, Guid id)
{
@@ -500,8 +508,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,38 +526,70 @@ public class OrganizationUsersController : Controller
new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage)));
}
[HttpDelete("{id}/delete-account")]
[HttpPost("{id}/delete-account")]
[HttpPost("remove")]
[Obsolete("This endpoint is deprecated. Use DELETE method instead")]
[Authorize<ManageUsersRequirement>]
public async Task DeleteAccount(Guid orgId, Guid id)
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> PostBulkRemove(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
{
var currentUser = await _userService.GetUserByPrincipalAsync(User);
if (currentUser == null)
return await BulkRemove(orgId, model);
}
[HttpDelete("{id}/delete-account")]
[Authorize<ManageUsersRequirement>]
public async Task<IResult> DeleteAccount(Guid orgId, Guid id)
{
var currentUserId = _userService.GetProperUserId(User);
if (currentUserId == null)
{
throw new UnauthorizedAccessException();
return TypedResults.Unauthorized();
}
await _deleteClaimedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
var commandResult = await _deleteClaimedOrganizationUserAccountCommand.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
);
}
[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);
}
[HttpDelete("delete-account")]
[HttpPost("delete-account")]
[Authorize<ManageUsersRequirement>]
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
{
var currentUser = await _userService.GetUserByPrincipalAsync(User);
if (currentUser == null)
var currentUserId = _userService.GetProperUserId(User);
if (currentUserId == null)
{
throw new UnauthorizedAccessException();
}
var results = await _deleteClaimedOrganizationUserAccountCommand.DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id);
var result = await _deleteClaimedOrganizationUserAccountCommand.DeleteManyUsersAsync(orgId, model.Ids, currentUserId.Value);
return new ListResponseModel<OrganizationUserBulkResponseModel>(results.Select(r =>
new OrganizationUserBulkResponseModel(r.OrganizationUserId, r.ErrorMessage)));
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);
}
[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);
}
[HttpPatch("{id}/revoke")]
[HttpPut("{id}/revoke")]
[Authorize<ManageUsersRequirement>]
public async Task RevokeAsync(Guid orgId, Guid id)
@@ -550,7 +597,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 +612,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 +627,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 +642,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 +682,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,

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -7,7 +7,6 @@ using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Context;
using Bit.Core.Exceptions;
using Bit.Core.Models.Business;
using Bit.Core.Services;
using Bit.Core.Settings;
using Microsoft.AspNetCore.Authorization;
@@ -53,7 +52,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 +69,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)
{
@@ -87,22 +92,12 @@ public class ProvidersController : Controller
var userId = _userService.GetProperUserId(User).Value;
var taxInfo = new TaxInfo
{
BillingAddressCountry = model.TaxInfo.Country,
BillingAddressPostalCode = model.TaxInfo.PostalCode,
TaxIdNumber = model.TaxInfo.TaxId,
BillingAddressLine1 = model.TaxInfo.Line1,
BillingAddressLine2 = model.TaxInfo.Line2,
BillingAddressCity = model.TaxInfo.City,
BillingAddressState = model.TaxInfo.State
};
var tokenizedPaymentSource = model.PaymentSource?.ToDomain();
var paymentMethod = model.PaymentMethod.ToDomain();
var billingAddress = model.BillingAddress.ToDomain();
var response =
await _providerService.CompleteSetupAsync(model.ToProvider(provider), userId, model.Token, model.Key,
taxInfo, tokenizedPaymentSource);
paymentMethod, billingAddress);
return new ProviderResponseModel(response);
}
@@ -120,7 +115,6 @@ public class ProvidersController : Controller
}
[HttpDelete("{id}")]
[HttpPost("{id}/delete")]
public async Task Delete(Guid id)
{
if (!_currentContext.ProviderProviderAdmin(id))
@@ -142,4 +136,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);
}
}

View File

@@ -1,7 +1,4 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
using System.Text.Json;
using System.Text.Json;
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
@@ -18,25 +15,58 @@ using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.AdminConsole.Controllers;
[RequireFeature(FeatureFlagKeys.EventBasedOrganizationIntegrations)]
[Route("organizations/{organizationId:guid}/integrations/slack")]
[Route("organizations")]
[Authorize("Application")]
public class SlackIntegrationController(
ICurrentContext currentContext,
IOrganizationIntegrationRepository integrationRepository,
ISlackService slackService) : Controller
ISlackService slackService,
TimeProvider timeProvider) : Controller
{
[HttpGet("redirect")]
[HttpGet("{organizationId:guid}/integrations/slack/redirect")]
public async Task<IActionResult> RedirectAsync(Guid organizationId)
{
if (!await currentContext.OrganizationOwner(organizationId))
{
throw new NotFoundException();
}
string callbackUrl = Url.RouteUrl(
nameof(CreateAsync),
new { organizationId },
currentContext.HttpContext.Request.Scheme);
var redirectUrl = slackService.GetRedirectUrl(callbackUrl);
string? callbackUrl = Url.RouteUrl(
routeName: nameof(CreateAsync),
values: null,
protocol: currentContext.HttpContext.Request.Scheme,
host: currentContext.HttpContext.Request.Host.ToUriComponent()
);
if (string.IsNullOrEmpty(callbackUrl))
{
throw new BadRequestException("Unable to build callback Url");
}
var integrations = await integrationRepository.GetManyByOrganizationAsync(organizationId);
var integration = integrations.FirstOrDefault(i => i.Type == IntegrationType.Slack);
if (integration is null)
{
// No slack integration exists, create Initiated version
integration = await integrationRepository.CreateAsync(new OrganizationIntegration
{
OrganizationId = organizationId,
Type = IntegrationType.Slack,
Configuration = null,
});
}
else if (integration.Configuration is not null)
{
// A Completed (fully configured) Slack integration already exists, throw to prevent overriding
throw new BadRequestException("There already exists a Slack integration for this organization");
} // An Initiated slack integration exits, re-use it and kick off a new OAuth flow
var state = IntegrationOAuthState.FromIntegration(integration, timeProvider);
var redirectUrl = slackService.GetRedirectUrl(
callbackUrl: callbackUrl,
state: state.ToString()
);
if (string.IsNullOrEmpty(redirectUrl))
{
@@ -46,23 +76,42 @@ public class SlackIntegrationController(
return Redirect(redirectUrl);
}
[HttpGet("create", Name = nameof(CreateAsync))]
public async Task<IActionResult> CreateAsync(Guid organizationId, [FromQuery] string code)
[HttpGet("integrations/slack/create", Name = nameof(CreateAsync))]
[AllowAnonymous]
public async Task<IActionResult> CreateAsync([FromQuery] string code, [FromQuery] string state)
{
if (!await currentContext.OrganizationOwner(organizationId))
var oAuthState = IntegrationOAuthState.FromString(state: state, timeProvider: timeProvider);
if (oAuthState is null)
{
throw new NotFoundException();
}
if (string.IsNullOrEmpty(code))
// Fetch existing Initiated record
var integration = await integrationRepository.GetByIdAsync(oAuthState.IntegrationId);
if (integration is null ||
integration.Type != IntegrationType.Slack ||
integration.Configuration is not null)
{
throw new BadRequestException("Missing code from Slack.");
throw new NotFoundException();
}
string callbackUrl = Url.RouteUrl(
nameof(CreateAsync),
new { organizationId },
currentContext.HttpContext.Request.Scheme);
// Verify Organization matches hash
if (!oAuthState.ValidateOrg(integration.OrganizationId))
{
throw new NotFoundException();
}
// Fetch token from Slack and store to DB
string? callbackUrl = Url.RouteUrl(
routeName: nameof(CreateAsync),
values: null,
protocol: currentContext.HttpContext.Request.Scheme,
host: currentContext.HttpContext.Request.Host.ToUriComponent()
);
if (string.IsNullOrEmpty(callbackUrl))
{
throw new BadRequestException("Unable to build callback Url");
}
var token = await slackService.ObtainTokenViaOAuth(code, callbackUrl);
if (string.IsNullOrEmpty(token))
@@ -70,14 +119,10 @@ public class SlackIntegrationController(
throw new BadRequestException("Invalid response from Slack.");
}
var integration = await integrationRepository.CreateAsync(new OrganizationIntegration
{
OrganizationId = organizationId,
Type = IntegrationType.Slack,
Configuration = JsonSerializer.Serialize(new SlackIntegration(token)),
});
var location = $"/organizations/{organizationId}/integrations/{integration.Id}";
integration.Configuration = JsonSerializer.Serialize(new SlackIntegration(token));
await integrationRepository.UpsertAsync(integration);
var location = $"/organizations/{integration.OrganizationId}/integrations/{integration.Id}";
return Created(location, new OrganizationIntegrationResponseModel(integration));
}
}

View File

@@ -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.",

View File

@@ -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;

View File

@@ -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)]);
}

View File

@@ -3,8 +3,7 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Bit.Api.Billing.Models.Requests;
using Bit.Api.Models.Request;
using Bit.Api.Billing.Models.Requests.Payment;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.Utilities;
@@ -28,8 +27,9 @@ public class ProviderSetupRequestModel
[Required]
public string Key { get; set; }
[Required]
public ExpandedTaxInfoUpdateRequestModel TaxInfo { get; set; }
public TokenizedPaymentSourceRequestBody PaymentSource { get; set; }
public MinimalTokenizedPaymentMethodRequest PaymentMethod { get; set; }
[Required]
public BillingAddressRequest BillingAddress { get; set; }
public virtual Provider ToProvider(Provider provider)
{

View 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();
}
}
}

View File

@@ -35,6 +35,7 @@ public class EventResponseModel : ResponseModel
SecretId = ev.SecretId;
ProjectId = ev.ProjectId;
ServiceAccountId = ev.ServiceAccountId;
GrantedServiceAccountId = ev.GrantedServiceAccountId;
}
public EventType Type { get; set; }
@@ -58,4 +59,5 @@ public class EventResponseModel : ResponseModel
public Guid? SecretId { get; set; }
public Guid? ProjectId { get; set; }
public Guid? ServiceAccountId { get; set; }
public Guid? GrantedServiceAccountId { get; set; }
}

View File

@@ -2,8 +2,6 @@
using Bit.Core.Enums;
using Bit.Core.Models.Api;
#nullable enable
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
public class OrganizationIntegrationResponseModel : ResponseModel
@@ -21,4 +19,29 @@ public class OrganizationIntegrationResponseModel : ResponseModel
public Guid Id { get; set; }
public IntegrationType Type { get; set; }
public string? Configuration { get; set; }
public OrganizationIntegrationStatus Status => Type switch
{
// Not yet implemented, shouldn't be present, NotApplicable
IntegrationType.CloudBillingSync => OrganizationIntegrationStatus.NotApplicable,
IntegrationType.Scim => OrganizationIntegrationStatus.NotApplicable,
// Webhook is allowed to be null. If it's present, it's Completed
IntegrationType.Webhook => OrganizationIntegrationStatus.Completed,
// If present and the configuration is null, OAuth has been initiated, and we are
// waiting on the return call
IntegrationType.Slack => string.IsNullOrWhiteSpace(Configuration)
? OrganizationIntegrationStatus.Initiated
: OrganizationIntegrationStatus.Completed,
// HEC and Datadog should only be allowed to be created non-null.
// If they are null, they are Invalid
IntegrationType.Hec => string.IsNullOrWhiteSpace(Configuration)
? OrganizationIntegrationStatus.Invalid
: OrganizationIntegrationStatus.Completed,
IntegrationType.Datadog => string.IsNullOrWhiteSpace(Configuration)
? OrganizationIntegrationStatus.Invalid
: OrganizationIntegrationStatus.Completed,
};
}

View File

@@ -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;

View File

@@ -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)
{

View File

@@ -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; }
}

View File

@@ -31,7 +31,7 @@ public class MemberCreateRequestModel : MemberUpdateRequestModel
{
Emails = new[] { Email },
Type = Type.Value,
Collections = Collections?.Select(c => c.ToCollectionAccessSelection()).ToList(),
Collections = Collections?.Select(c => c.ToCollectionAccessSelection())?.ToList() ?? [],
Groups = Groups
};

View File

@@ -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>

View File

@@ -9,6 +9,7 @@ using Bit.Core;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Models.Api.Request.Accounts;
using Bit.Core.Auth.Services;
using Bit.Core.Auth.UserFeatures.TdeOffboardingPassword.Interfaces;
@@ -16,6 +17,7 @@ using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Auth.UserFeatures.UserMasterPassword.Interfaces;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.KeyManagement.Kdf;
using Bit.Core.Models.Api.Response;
using Bit.Core.Repositories;
using Bit.Core.Services;
@@ -26,7 +28,7 @@ using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Auth.Controllers;
[Route("accounts")]
[Authorize("Application")]
[Authorize(Policies.Application)]
public class AccountsController : Controller
{
private readonly IOrganizationService _organizationService;
@@ -39,7 +41,7 @@ public class AccountsController : Controller
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IFeatureService _featureService;
private readonly ITwoFactorEmailService _twoFactorEmailService;
private readonly IChangeKdfCommand _changeKdfCommand;
public AccountsController(
IOrganizationService organizationService,
@@ -51,7 +53,8 @@ public class AccountsController : Controller
ITdeOffboardingPasswordCommand tdeOffboardingPasswordCommand,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IFeatureService featureService,
ITwoFactorEmailService twoFactorEmailService
ITwoFactorEmailService twoFactorEmailService,
IChangeKdfCommand changeKdfCommand
)
{
_organizationService = organizationService;
@@ -64,7 +67,7 @@ public class AccountsController : Controller
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_featureService = featureService;
_twoFactorEmailService = twoFactorEmailService;
_changeKdfCommand = changeKdfCommand;
}
@@ -256,7 +259,7 @@ public class AccountsController : Controller
}
[HttpPost("kdf")]
public async Task PostKdf([FromBody] KdfRequestModel model)
public async Task PostKdf([FromBody] PasswordRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
if (user == null)
@@ -264,8 +267,12 @@ public class AccountsController : Controller
throw new UnauthorizedAccessException();
}
var result = await _userService.ChangeKdfAsync(user, model.MasterPasswordHash,
model.NewMasterPasswordHash, model.Key, model.Kdf.Value, model.KdfIterations.Value, model.KdfMemory, model.KdfParallelism);
if (model.AuthenticationData == null || model.UnlockData == null)
{
throw new BadRequestException("AuthenticationData and UnlockData must be provided.");
}
var result = await _changeKdfCommand.ChangeKdfAsync(user, model.MasterPasswordHash, model.AuthenticationData.ToData(), model.UnlockData.ToData());
if (result.Succeeded)
{
return;
@@ -344,7 +351,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 +369,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 +394,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 +449,6 @@ public class AccountsController : Controller
}
[HttpDelete]
[HttpPost("delete")]
public async Task Delete([FromBody] SecretVerificationRequestModel model)
{
var user = await _userService.GetUserByPrincipalAsync(User);
@@ -467,6 +485,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 +663,6 @@ public class AccountsController : Controller
await _twoFactorEmailService.SendNewDeviceVerificationEmailAsync(user);
}
[HttpPost("verify-devices")]
[HttpPut("verify-devices")]
public async Task SetUserVerifyDevicesAsync([FromBody] SetVerifyDevicesRequestModel request)
{
@@ -654,6 +678,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);

View File

@@ -3,22 +3,21 @@
using Bit.Api.Auth.Models.Response;
using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Models.Api.Request.AuthRequest;
using Bit.Core.Auth.Services;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Auth.Controllers;
[Route("auth-requests")]
[Authorize("Application")]
[Authorize(Policies.Application)]
public class AuthRequestsController(
IUserService userService,
IAuthRequestRepository authRequestRepository,
@@ -31,7 +30,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);
@@ -54,7 +53,6 @@ public class AuthRequestsController(
}
[HttpGet("pending")]
[RequireFeature(FeatureFlagKeys.BrowserExtensionLoginApproval)]
public async Task<ListResponseModel<PendingAuthRequestResponseModel>> GetPendingAuthRequestsAsync()
{
var userId = _userService.GetProperUserId(User).Value;
@@ -102,7 +100,37 @@ public class AuthRequestsController(
public async Task<AuthRequestResponseModel> Put(Guid id, [FromBody] AuthRequestUpdateRequestModel model)
{
var userId = _userService.GetProperUserId(User).Value;
// If the Approving Device is attempting to approve a request, validate the approval
if (model.RequestApproved == true)
{
await ValidateApprovalOfMostRecentAuthRequest(id, userId);
}
var authRequest = await _authRequestService.UpdateAuthRequestAsync(id, userId, model);
return new AuthRequestResponseModel(authRequest, _globalSettings.BaseServiceUri.Vault);
}
private async Task ValidateApprovalOfMostRecentAuthRequest(Guid id, Guid userId)
{
// Get the current auth request to find the device identifier
var currentAuthRequest = await _authRequestService.GetAuthRequestAsync(id, userId);
if (currentAuthRequest == null)
{
throw new NotFoundException();
}
// Get all pending auth requests for this user (returns most recent per device)
var pendingRequests = await _authRequestRepository.GetManyPendingAuthRequestByUserId(userId);
// Find the most recent request for the same device
var mostRecentForDevice = pendingRequests
.FirstOrDefault(pendingRequest => pendingRequest.RequestDeviceIdentifier == currentAuthRequest.RequestDeviceIdentifier);
var isMostRecentRequestForDevice = mostRecentForDevice?.Id == id;
if (!isMostRecentRequestForDevice)
{
throw new BadRequestException("This request is no longer valid. Make sure to approve the most recent request.");
}
}
}

View File

@@ -18,7 +18,7 @@ using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Auth.Controllers;
[Route("emergency-access")]
[Authorize("Application")]
[Authorize(Core.Auth.Identity.Policies.Application)]
public class EmergencyAccessController : Controller
{
private readonly IUserService _userService;
@@ -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);

View File

@@ -7,6 +7,7 @@ using Bit.Api.Auth.Models.Response.TwoFactor;
using Bit.Api.Models.Request;
using Bit.Api.Models.Response;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Identity.TokenProviders;
using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces;
using Bit.Core.Auth.Models.Business.Tokenables;
@@ -26,7 +27,7 @@ using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Auth.Controllers;
[Route("two-factor")]
[Authorize("Web")]
[Authorize(Policies.Web)]
public class TwoFactorController : Controller
{
private readonly IUserService _userService;
@@ -110,7 +111,6 @@ public class TwoFactorController : Controller
}
[HttpPut("authenticator")]
[HttpPost("authenticator")]
public async Task<TwoFactorAuthenticatorResponseModel> PutAuthenticator(
[FromBody] UpdateTwoFactorAuthenticatorRequestModel model)
{
@@ -133,6 +133,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 +165,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 +181,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 +197,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 +212,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 +237,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 +262,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 +288,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 +303,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 +382,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 +399,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 +415,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 +445,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 +461,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()

View File

@@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Identity;
using Bit.Core.Auth.Models.Api.Response.Accounts;
using Bit.Core.Auth.Models.Business.Tokenables;
using Bit.Core.Auth.Repositories;
@@ -20,7 +21,7 @@ using Microsoft.AspNetCore.Mvc;
namespace Bit.Api.Auth.Controllers;
[Route("webauthn")]
[Authorize("Web")]
[Authorize(Policies.Web)]
public class WebAuthnController : Controller
{
private readonly IUserService _userService;

View File

@@ -1,25 +0,0 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
using Bit.Core.Utilities;
namespace Bit.Api.Auth.Models.Request.Accounts;
public class KdfRequestModel : PasswordRequestModel, IValidatableObject
{
[Required]
public KdfType? Kdf { get; set; }
[Required]
public int? KdfIterations { get; set; }
public int? KdfMemory { get; set; }
public int? KdfParallelism { get; set; }
public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Kdf.HasValue && KdfIterations.HasValue)
{
return KdfSettingsValidator.Validate(Kdf.Value, KdfIterations.Value, KdfMemory, KdfParallelism);
}
return Enumerable.Empty<ValidationResult>();
}
}

View File

@@ -7,7 +7,7 @@ using Bit.Core.Utilities;
namespace Bit.Api.Auth.Models.Request.Accounts;
public class MasterPasswordUnlockDataModel : IValidatableObject
public class MasterPasswordUnlockAndAuthenticationDataModel : IValidatableObject
{
public required KdfType KdfType { get; set; }
public required int KdfIterations { get; set; }
@@ -45,9 +45,9 @@ public class MasterPasswordUnlockDataModel : IValidatableObject
}
}
public MasterPasswordUnlockData ToUnlockData()
public MasterPasswordUnlockAndAuthenticationData ToUnlockData()
{
var data = new MasterPasswordUnlockData
var data = new MasterPasswordUnlockAndAuthenticationData
{
KdfType = KdfType,
KdfIterations = KdfIterations,

View File

@@ -1,7 +1,7 @@
// FIXME: Update this file to be null safe and then delete the line below
#nullable disable
#nullable enable
using System.ComponentModel.DataAnnotations;
using Bit.Api.KeyManagement.Models.Requests;
namespace Bit.Api.Auth.Models.Request.Accounts;
@@ -9,9 +9,13 @@ public class PasswordRequestModel : SecretVerificationRequestModel
{
[Required]
[StringLength(300)]
public string NewMasterPasswordHash { get; set; }
public required string NewMasterPasswordHash { get; set; }
[StringLength(50)]
public string MasterPasswordHint { get; set; }
public string? MasterPasswordHint { get; set; }
[Required]
public string Key { get; set; }
public required string Key { get; set; }
// Note: These will eventually become required, but not all consumers are moved over yet.
public MasterPasswordAuthenticationDataRequestModel? AuthenticationData { get; set; }
public MasterPasswordUnlockDataRequestModel? UnlockData { get; set; }
}

View File

@@ -121,7 +121,7 @@ public class SsoConfigurationDataRequest : IValidatableObject
new[] { nameof(IdpEntityId) });
}
if (!Uri.IsWellFormedUriString(IdpEntityId, UriKind.Absolute) && string.IsNullOrWhiteSpace(IdpSingleSignOnServiceUrl))
if (string.IsNullOrWhiteSpace(IdpSingleSignOnServiceUrl))
{
yield return new ValidationResult(i18nService.GetLocalizedHtmlString("IdpSingleSignOnServiceUrlValidationError"),
new[] { nameof(IdpSingleSignOnServiceUrl) });
@@ -139,6 +139,7 @@ public class SsoConfigurationDataRequest : IValidatableObject
new[] { nameof(IdpSingleLogoutServiceUrl) });
}
// TODO: On server, make public certificate required for SAML2 SSO: https://bitwarden.atlassian.net/browse/PM-26028
if (!string.IsNullOrWhiteSpace(IdpX509PublicCert))
{
// Validate the certificate is in a valid format

View File

@@ -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)}";
}
}

View File

@@ -1,16 +1,8 @@
#nullable enable
using System.Diagnostics;
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Billing.Models.Requests;
using Bit.Api.Billing.Models.Requests;
using Bit.Api.Billing.Models.Responses;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models;
using Bit.Core.Billing.Organizations.Models;
using Bit.Core.Billing.Organizations.Services;
using Bit.Core.Billing.Pricing;
using Bit.Core.Billing.Providers.Services;
using Bit.Core.Billing.Services;
using Bit.Core.Billing.Tax.Models;
using Bit.Core.Context;
using Bit.Core.Repositories;
using Bit.Core.Services;
@@ -28,10 +20,8 @@ public class OrganizationBillingController(
IOrganizationBillingService organizationBillingService,
IOrganizationRepository organizationRepository,
IPaymentService paymentService,
IPricingClient pricingClient,
ISubscriberService subscriberService,
IPaymentHistoryService paymentHistoryService,
IUserService userService) : BaseBillingController
IPaymentHistoryService paymentHistoryService) : BaseBillingController
{
[HttpGet("metadata")]
public async Task<IResult> GetMetadataAsync([FromRoute] Guid organizationId)
@@ -264,71 +254,6 @@ public class OrganizationBillingController(
return TypedResults.Ok();
}
[HttpPost("restart-subscription")]
public async Task<IResult> RestartSubscriptionAsync([FromRoute] Guid organizationId,
[FromBody] OrganizationCreateRequestModel model)
{
var user = await userService.GetUserByPrincipalAsync(User);
if (user == null)
{
throw new UnauthorizedAccessException();
}
if (!await currentContext.EditPaymentMethods(organizationId))
{
return Error.Unauthorized();
}
var organization = await organizationRepository.GetByIdAsync(organizationId);
if (organization == null)
{
return Error.NotFound();
}
var existingPlan = organization.PlanType;
var organizationSignup = model.ToOrganizationSignup(user);
var sale = OrganizationSale.From(organization, organizationSignup);
var plan = await pricingClient.GetPlanOrThrow(model.PlanType);
sale.Organization.PlanType = plan.Type;
sale.Organization.Plan = plan.Name;
sale.SubscriptionSetup.SkipTrial = true;
if (existingPlan == PlanType.Free && organization.GatewaySubscriptionId is not null)
{
sale.Organization.UseTotp = plan.HasTotp;
sale.Organization.UseGroups = plan.HasGroups;
sale.Organization.UseDirectory = plan.HasDirectory;
sale.Organization.SelfHost = plan.HasSelfHost;
sale.Organization.UsersGetPremium = plan.UsersGetPremium;
sale.Organization.UseEvents = plan.HasEvents;
sale.Organization.Use2fa = plan.Has2fa;
sale.Organization.UseApi = plan.HasApi;
sale.Organization.UsePolicies = plan.HasPolicies;
sale.Organization.UseSso = plan.HasSso;
sale.Organization.UseResetPassword = plan.HasResetPassword;
sale.Organization.UseKeyConnector = plan.HasKeyConnector;
sale.Organization.UseScim = plan.HasScim;
sale.Organization.UseCustomPermissions = plan.HasCustomPermissions;
sale.Organization.UseOrganizationDomains = plan.HasOrganizationDomains;
sale.Organization.MaxCollections = plan.PasswordManager.MaxCollections;
}
if (organizationSignup.PaymentMethodType == null || string.IsNullOrEmpty(organizationSignup.PaymentToken))
{
return Error.BadRequest("A payment method is required to restart the subscription.");
}
var org = await organizationRepository.GetByIdAsync(organizationId);
Debug.Assert(org is not null, "This organization has already been found via this same ID, this should be fine.");
var paymentSource = new TokenizedPaymentSource(organizationSignup.PaymentMethodType.Value, organizationSignup.PaymentToken);
var taxInformation = TaxInformation.From(organizationSignup.TaxInfo);
await organizationBillingService.Finalize(sale);
var updatedOrg = await organizationRepository.GetByIdAsync(organizationId);
if (updatedOrg != null)
{
await organizationBillingService.UpdatePaymentMethod(updatedOrg, paymentSource, taxInformation);
}
return TypedResults.Ok();
}
[HttpPost("setup-business-unit")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<IResult> SetupBusinessUnitAsync(

View File

@@ -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)
{

View File

@@ -211,18 +211,6 @@ public class OrganizationsController(
return new PaymentResponseModel { Success = true, PaymentIntentClientSecret = result };
}
[HttpPost("{id:guid}/verify-bank")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task PostVerifyBank(Guid id, [FromBody] OrganizationVerifyBankRequestModel model)
{
if (!await currentContext.EditSubscription(id))
{
throw new NotFoundException();
}
await organizationService.VerifyBankAsync(id, model.Amount1.Value, model.Amount2.Value);
}
[HttpPost("{id}/cancel")]
public async Task PostCancel(Guid id, [FromBody] SubscriptionCancellationRequestModel request)
{

View File

@@ -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;

View File

@@ -1,33 +1,73 @@
using Bit.Api.Billing.Models.Requests;
using Bit.Core.Billing.Tax.Commands;
using Bit.Api.Billing.Attributes;
using Bit.Api.Billing.Models.Requests.Tax;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Organizations.Commands;
using Bit.Core.Billing.Premium.Commands;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ModelBinding;
namespace Bit.Api.Billing.Controllers;
[Authorize("Application")]
[Route("tax")]
[Route("billing/tax")]
public class TaxController(
IPreviewTaxAmountCommand previewTaxAmountCommand) : BaseBillingController
IPreviewOrganizationTaxCommand previewOrganizationTaxCommand,
IPreviewPremiumTaxCommand previewPremiumTaxCommand) : BaseBillingController
{
[HttpPost("preview-amount/organization-trial")]
public async Task<IResult> PreviewTaxAmountForOrganizationTrialAsync(
[FromBody] PreviewTaxAmountForOrganizationTrialRequestBody requestBody)
[HttpPost("organizations/subscriptions/purchase")]
public async Task<IResult> PreviewOrganizationSubscriptionPurchaseTaxAsync(
[FromBody] PreviewOrganizationSubscriptionPurchaseTaxRequest request)
{
var parameters = new OrganizationTrialParameters
var (purchase, billingAddress) = request.ToDomain();
var result = await previewOrganizationTaxCommand.Run(purchase, billingAddress);
return Handle(result.Map(pair => new
{
PlanType = requestBody.PlanType,
ProductType = requestBody.ProductType,
TaxInformation = new OrganizationTrialParameters.TaxInformationDTO
{
Country = requestBody.TaxInformation.Country,
PostalCode = requestBody.TaxInformation.PostalCode,
TaxId = requestBody.TaxInformation.TaxId
}
};
pair.Tax,
pair.Total
}));
}
var result = await previewTaxAmountCommand.Run(parameters);
[HttpPost("organizations/{organizationId:guid}/subscription/plan-change")]
[InjectOrganization]
public async Task<IResult> PreviewOrganizationSubscriptionPlanChangeTaxAsync(
[BindNever] Organization organization,
[FromBody] PreviewOrganizationSubscriptionPlanChangeTaxRequest request)
{
var (planChange, billingAddress) = request.ToDomain();
var result = await previewOrganizationTaxCommand.Run(organization, planChange, billingAddress);
return Handle(result.Map(pair => new
{
pair.Tax,
pair.Total
}));
}
return Handle(result);
[HttpPut("organizations/{organizationId:guid}/subscription/update")]
[InjectOrganization]
public async Task<IResult> PreviewOrganizationSubscriptionUpdateTaxAsync(
[BindNever] Organization organization,
[FromBody] PreviewOrganizationSubscriptionUpdateTaxRequest request)
{
var update = request.ToDomain();
var result = await previewOrganizationTaxCommand.Run(organization, update);
return Handle(result.Map(pair => new
{
pair.Tax,
pair.Total
}));
}
[HttpPost("premium/subscriptions/purchase")]
public async Task<IResult> PreviewPremiumSubscriptionPurchaseTaxAsync(
[FromBody] PreviewPremiumSubscriptionPurchaseTaxRequest request)
{
var (purchase, billingAddress) = request.ToDomain();
var result = await previewPremiumTaxCommand.Run(purchase, billingAddress);
return Handle(result.Map(pair => new
{
pair.Tax,
pair.Total
}));
}
}

View File

@@ -1,8 +1,10 @@
#nullable enable
using Bit.Api.Billing.Attributes;
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 +18,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 +64,17 @@ public class AccountBillingVNextController(
var result = await updatePaymentMethodCommand.Run(user, paymentMethod, billingAddress);
return Handle(result);
}
[HttpPost("subscription")]
[RequireFeature(FeatureFlagKeys.PM24996ImplementUpgradeFromFreeDialog)]
[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);
}
}

View File

@@ -2,11 +2,14 @@
using Bit.Api.AdminConsole.Authorization.Requirements;
using Bit.Api.Billing.Attributes;
using Bit.Api.Billing.Models.Requests.Payment;
using Bit.Api.Billing.Models.Requests.Subscriptions;
using Bit.Api.Billing.Models.Requirements;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Commands;
using Bit.Core.Billing.Organizations.Queries;
using Bit.Core.Billing.Payment.Commands;
using Bit.Core.Billing.Payment.Queries;
using Bit.Core.Billing.Subscriptions.Commands;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@@ -24,9 +27,9 @@ public class OrganizationBillingVNextController(
IGetCreditQuery getCreditQuery,
IGetOrganizationWarningsQuery getOrganizationWarningsQuery,
IGetPaymentMethodQuery getPaymentMethodQuery,
IRestartSubscriptionCommand restartSubscriptionCommand,
IUpdateBillingAddressCommand updateBillingAddressCommand,
IUpdatePaymentMethodCommand updatePaymentMethodCommand,
IVerifyBankAccountCommand verifyBankAccountCommand) : BaseBillingController
IUpdatePaymentMethodCommand updatePaymentMethodCommand) : BaseBillingController
{
[Authorize<ManageOrganizationBillingRequirement>]
[HttpGet("address")]
@@ -97,13 +100,16 @@ public class OrganizationBillingVNextController(
}
[Authorize<ManageOrganizationBillingRequirement>]
[HttpPost("payment-method/verify-bank-account")]
[HttpPost("subscription/restart")]
[InjectOrganization]
public async Task<IResult> VerifyBankAccountAsync(
public async Task<IResult> RestartSubscriptionAsync(
[BindNever] Organization organization,
[FromBody] VerifyBankAccountRequest request)
[FromBody] RestartSubscriptionRequest request)
{
var result = await verifyBankAccountCommand.Run(organization, request.DescriptorCode);
var (paymentMethod, billingAddress) = request.ToDomain();
var result = await updatePaymentMethodCommand.Run(organization, paymentMethod, null)
.AndThenAsync(_ => updateBillingAddressCommand.Run(organization, billingAddress))
.AndThenAsync(_ => restartSubscriptionCommand.Run(organization));
return Handle(result);
}

View File

@@ -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(

View File

@@ -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.PM24996ImplementUpgradeFromFreeDialog)]
[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);
}
}

View File

@@ -0,0 +1,31 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Organizations.Models;
namespace Bit.Api.Billing.Models.Requests.Organizations;
public record OrganizationSubscriptionPlanChangeRequest : IValidatableObject
{
[Required]
[JsonConverter(typeof(JsonStringEnumConverter))]
public ProductTierType Tier { get; set; }
[Required]
[JsonConverter(typeof(JsonStringEnumConverter))]
public PlanCadenceType Cadence { get; set; }
public OrganizationSubscriptionPlanChange ToDomain() => new()
{
Tier = Tier,
Cadence = Cadence
};
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Tier == ProductTierType.Families && Cadence == PlanCadenceType.Monthly)
{
yield return new ValidationResult("Monthly billing cadence is not available for the Families plan.");
}
}
}

View File

@@ -0,0 +1,84 @@
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Serialization;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Organizations.Models;
namespace Bit.Api.Billing.Models.Requests.Organizations;
public record OrganizationSubscriptionPurchaseRequest : IValidatableObject
{
[Required]
[JsonConverter(typeof(JsonStringEnumConverter))]
public ProductTierType Tier { get; set; }
[Required]
[JsonConverter(typeof(JsonStringEnumConverter))]
public PlanCadenceType Cadence { get; set; }
[Required]
public required PasswordManagerPurchaseSelections PasswordManager { get; set; }
public SecretsManagerPurchaseSelections? SecretsManager { get; set; }
public OrganizationSubscriptionPurchase ToDomain() => new()
{
Tier = Tier,
Cadence = Cadence,
PasswordManager = new OrganizationSubscriptionPurchase.PasswordManagerSelections
{
Seats = PasswordManager.Seats,
AdditionalStorage = PasswordManager.AdditionalStorage,
Sponsored = PasswordManager.Sponsored
},
SecretsManager = SecretsManager != null ? new OrganizationSubscriptionPurchase.SecretsManagerSelections
{
Seats = SecretsManager.Seats,
AdditionalServiceAccounts = SecretsManager.AdditionalServiceAccounts,
Standalone = SecretsManager.Standalone
} : null
};
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if (Tier != ProductTierType.Families)
{
yield break;
}
if (Cadence == PlanCadenceType.Monthly)
{
yield return new ValidationResult("Monthly cadence is not available on the Families plan.");
}
if (SecretsManager != null)
{
yield return new ValidationResult("Secrets Manager is not available on the Families plan.");
}
}
public record PasswordManagerPurchaseSelections
{
[Required]
[Range(1, 100000, ErrorMessage = "Password Manager seats must be between 1 and 100,000")]
public int Seats { get; set; }
[Required]
[Range(0, 99, ErrorMessage = "Additional storage must be between 0 and 99 GB")]
public int AdditionalStorage { get; set; }
public bool Sponsored { get; set; } = false;
}
public record SecretsManagerPurchaseSelections
{
[Required]
[Range(1, 100000, ErrorMessage = "Secrets Manager seats must be between 1 and 100,000")]
public int Seats { get; set; }
[Required]
[Range(0, 100000, ErrorMessage = "Additional service accounts must be between 0 and 100,000")]
public int AdditionalServiceAccounts { get; set; }
public bool Standalone { get; set; } = false;
}
}

View File

@@ -0,0 +1,48 @@
using System.ComponentModel.DataAnnotations;
using Bit.Core.Billing.Organizations.Models;
namespace Bit.Api.Billing.Models.Requests.Organizations;
public record OrganizationSubscriptionUpdateRequest
{
public PasswordManagerUpdateSelections? PasswordManager { get; set; }
public SecretsManagerUpdateSelections? SecretsManager { get; set; }
public OrganizationSubscriptionUpdate ToDomain() => new()
{
PasswordManager =
PasswordManager != null
? new OrganizationSubscriptionUpdate.PasswordManagerSelections
{
Seats = PasswordManager.Seats,
AdditionalStorage = PasswordManager.AdditionalStorage
}
: null,
SecretsManager =
SecretsManager != null
? new OrganizationSubscriptionUpdate.SecretsManagerSelections
{
Seats = SecretsManager.Seats,
AdditionalServiceAccounts = SecretsManager.AdditionalServiceAccounts
}
: null
};
public record PasswordManagerUpdateSelections
{
[Range(1, 100000, ErrorMessage = "Password Manager seats must be between 1 and 100,000")]
public int? Seats { get; set; }
[Range(0, 99, ErrorMessage = "Additional storage must be between 0 and 99 GB")]
public int? AdditionalStorage { get; set; }
}
public record SecretsManagerUpdateSelections
{
[Range(0, 100000, ErrorMessage = "Secrets Manager seats must be between 0 and 100,000")]
public int? Seats { get; set; }
[Range(0, 100000, ErrorMessage = "Additional service accounts must be between 0 and 100,000")]
public int? AdditionalServiceAccounts { get; set; }
}
}

View File

@@ -1,5 +1,4 @@
#nullable enable
using Bit.Core.Billing.Payment.Models;
using Bit.Core.Billing.Payment.Models;
namespace Bit.Api.Billing.Models.Requests.Payment;

View File

@@ -1,5 +1,4 @@
#nullable enable
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
namespace Bit.Api.Billing.Models.Requests.Payment;

View File

@@ -1,5 +1,4 @@
#nullable enable
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using Bit.Core.Billing.Payment.Models;
namespace Bit.Api.Billing.Models.Requests.Payment;

View File

@@ -1,5 +1,4 @@
#nullable enable
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations;
using Bit.Core.Billing.Payment.Models;
namespace Bit.Api.Billing.Models.Requests.Payment;

View File

@@ -0,0 +1,21 @@
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() => new()
{
Type = TokenizablePaymentMethodTypeExtensions.From(Type),
Token = Token
};
}

View File

@@ -1,39 +1,15 @@
#nullable enable
using System.ComponentModel.DataAnnotations;
using Bit.Api.Utilities;
using Bit.Core.Billing.Payment.Models;
using Bit.Core.Billing.Payment.Models;
namespace Bit.Api.Billing.Models.Requests.Payment;
public class TokenizedPaymentMethodRequest
public class TokenizedPaymentMethodRequest : MinimalTokenizedPaymentMethodRequest
{
[Required]
[StringMatches("bankAccount", "card", "payPal",
ErrorMessage = "Payment method type must be one of: bankAccount, card, payPal")]
public required string Type { get; set; }
[Required]
public required string Token { get; set; }
public MinimalBillingAddressRequest? BillingAddress { get; set; }
public (TokenizedPaymentMethod, BillingAddress?) ToDomain()
public new (TokenizedPaymentMethod, BillingAddress?) ToDomain()
{
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)}")
},
Token = Token
};
var paymentMethod = base.ToDomain();
var billingAddress = BillingAddress?.ToDomain();
return (paymentMethod, billingAddress);
}
}

View File

@@ -0,0 +1,25 @@
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);
}
}

View File

@@ -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; }
}

View File

@@ -1,27 +0,0 @@
#nullable enable
using System.ComponentModel.DataAnnotations;
using Bit.Core.Billing.Enums;
namespace Bit.Api.Billing.Models.Requests;
public class PreviewTaxAmountForOrganizationTrialRequestBody
{
[Required]
public PlanType PlanType { get; set; }
[Required]
public ProductType ProductType { get; set; }
[Required] public TaxInformationDTO TaxInformation { get; set; } = null!;
public class TaxInformationDTO
{
[Required]
public string Country { get; set; } = null!;
[Required]
public string PostalCode { get; set; } = null!;
public string? TaxId { get; set; }
}
}

View File

@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
using Bit.Api.Billing.Models.Requests.Payment;
using Bit.Core.Billing.Payment.Models;
namespace Bit.Api.Billing.Models.Requests.Subscriptions;
public class RestartSubscriptionRequest
{
[Required]
public required MinimalTokenizedPaymentMethodRequest PaymentMethod { get; set; }
[Required]
public required CheckoutBillingAddressRequest BillingAddress { get; set; }
public (TokenizedPaymentMethod, BillingAddress) ToDomain()
=> (PaymentMethod.ToDomain(), BillingAddress.ToDomain());
}

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