1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-28 13:15:12 +01:00

Merge branch 'main' of https://github.com/bitwarden/server into vault/pm-11249/attachment-cipher-date

This commit is contained in:
Nick Krantz 2024-11-06 13:59:49 -06:00
commit a584bf42a1
No known key found for this signature in database
GPG Key ID: FF670021ABCAB82E
17 changed files with 292 additions and 160 deletions

19
.github/renovate.json vendored
View File

@ -29,7 +29,7 @@
"commitMessagePrefix": "[deps] DevOps:" "commitMessagePrefix": "[deps] DevOps:"
}, },
{ {
"matchPackageNames": ["DnsClient", "Quartz"], "matchPackageNames": ["DnsClient"],
"description": "Admin Console owned dependencies", "description": "Admin Console owned dependencies",
"commitMessagePrefix": "[deps] AC:", "commitMessagePrefix": "[deps] AC:",
"reviewers": ["team:team-admin-console-dev"] "reviewers": ["team:team-admin-console-dev"]
@ -42,14 +42,7 @@
}, },
{ {
"matchPackageNames": [ "matchPackageNames": [
"AspNetCoreRateLimit",
"AspNetCoreRateLimit.Redis",
"Azure.Data.Tables",
"Azure.Extensions.AspNetCore.DataProtection.Blobs", "Azure.Extensions.AspNetCore.DataProtection.Blobs",
"Azure.Messaging.EventGrid",
"Azure.Messaging.ServiceBus",
"Azure.Storage.Blobs",
"Azure.Storage.Queues",
"DuoUniversal", "DuoUniversal",
"Fido2.AspNet", "Fido2.AspNet",
"Duende.IdentityServer", "Duende.IdentityServer",
@ -128,8 +121,16 @@
}, },
{ {
"matchPackageNames": [ "matchPackageNames": [
"AspNetCoreRateLimit",
"AspNetCoreRateLimit.Redis",
"Azure.Data.Tables",
"Azure.Messaging.EventGrid",
"Azure.Messaging.ServiceBus",
"Azure.Storage.Blobs",
"Azure.Storage.Queues",
"Microsoft.AspNetCore.Authentication.JwtBearer", "Microsoft.AspNetCore.Authentication.JwtBearer",
"Microsoft.AspNetCore.Http" "Microsoft.AspNetCore.Http",
"Quartz"
], ],
"description": "Platform owned dependencies", "description": "Platform owned dependencies",
"commitMessagePrefix": "[deps] Platform:", "commitMessagePrefix": "[deps] Platform:",

View File

@ -115,12 +115,33 @@ jobs:
path: ${{ matrix.base_path }}/${{ matrix.project_name }}/${{ matrix.project_name }}.zip path: ${{ matrix.base_path }}/${{ matrix.project_name }}/${{ matrix.project_name }}.zip
if-no-files-found: error if-no-files-found: error
check-akv-secrets:
name: Check for AKV secrets
runs-on: ubuntu-22.04
outputs:
available: ${{ steps.check-akv-secrets.outputs.available }}
permissions:
contents: read
steps:
- name: Check
id: check-akv-secrets
run: |
if [ "${{ secrets.AZURE_PROD_KV_CREDENTIALS }}" != '' ]; then
echo "available=true" >> $GITHUB_OUTPUT;
else
echo "available=false" >> $GITHUB_OUTPUT;
fi
build-docker: build-docker:
name: Build Docker images name: Build Docker images
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
permissions: permissions:
security-events: write security-events: write
needs: build-artifacts needs:
- build-artifacts
- check-akv-secrets
if: ${{ needs.check-akv-secrets.outputs.available == 'true' }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:

View File

@ -3,12 +3,13 @@ name: Repository management
on: on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
branch_to_cut: task:
default: "rc" default: "Version Bump"
description: "Branch to cut" description: "Task to execute"
options: options:
- "rc" - "Version Bump"
- "hotfix-rc" - "Version Bump and Cut rc"
- "Version Bump and Cut hotfix-rc"
required: true required: true
type: choice type: choice
target_ref: target_ref:
@ -22,18 +23,51 @@ on:
type: string type: string
jobs: jobs:
setup:
name: Setup
runs-on: ubuntu-24.04
outputs:
branch: ${{ steps.set-branch.outputs.branch }}
token: ${{ steps.app-token.outputs.token }}
steps:
- name: Set branch
id: set-branch
env:
TASK: ${{ inputs.task }}
run: |
if [[ "$TASK" == "Version Bump" ]]; then
BRANCH="none"
elif [[ "$TASK" == "Version Bump and Cut rc" ]]; then
BRANCH="rc"
elif [[ "$TASK" == "Version Bump and Cut hotfix-rc" ]]; then
BRANCH="hotfix-rc"
fi
echo "branch=$BRANCH" >> $GITHUB_OUTPUT
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}
private-key: ${{ secrets.BW_GHAPP_KEY }}
cut_branch: cut_branch:
name: Cut branch name: Cut branch
runs-on: ubuntu-22.04 if: ${{ needs.setup.outputs.branch != 'none' }}
needs: setup
runs-on: ubuntu-24.04
steps: steps:
- name: Check out target ref - name: Check out target ref
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
ref: ${{ inputs.target_ref }} ref: ${{ inputs.target_ref }}
token: ${{ needs.setup.outputs.token }}
- name: Check if ${{ inputs.branch_to_cut }} branch exists - name: Check if ${{ needs.setup.outputs.branch }} branch exists
env: env:
BRANCH_NAME: ${{ inputs.branch_to_cut }} BRANCH_NAME: ${{ needs.setup.outputs.branch }}
run: | run: |
if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then
echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY
@ -42,7 +76,7 @@ jobs:
- name: Cut branch - name: Cut branch
env: env:
BRANCH_NAME: ${{ inputs.branch_to_cut }} BRANCH_NAME: ${{ needs.setup.outputs.branch }}
run: | run: |
git switch --quiet --create $BRANCH_NAME git switch --quiet --create $BRANCH_NAME
git push --quiet --set-upstream origin $BRANCH_NAME git push --quiet --set-upstream origin $BRANCH_NAME
@ -50,8 +84,11 @@ jobs:
bump_version: bump_version:
name: Bump Version name: Bump Version
runs-on: ubuntu-22.04 if: ${{ always() }}
needs: cut_branch runs-on: ubuntu-24.04
needs:
- cut_branch
- setup
outputs: outputs:
version: ${{ steps.set-final-version-output.outputs.version }} version: ${{ steps.set-final-version-output.outputs.version }}
steps: steps:
@ -65,6 +102,12 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
ref: main ref: main
token: ${{ needs.setup.outputs.token }}
- name: Configure Git
run: |
git config --local user.email "actions@github.com"
git config --local user.name "Github Actions"
- name: Install xmllint - name: Install xmllint
run: | run: |
@ -123,133 +166,69 @@ jobs:
- name: Set final version output - name: Set final version output
id: set-final-version-output id: set-final-version-output
env:
VERSION: ${{ inputs.version_number_override }}
run: | run: |
if [[ "${{ steps.bump-version-override.outcome }}" = "success" ]]; then if [[ "${{ steps.bump-version-override.outcome }}" = "success" ]]; then
echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT echo "version=$VERSION" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-version-automatic.outcome }}" = "success" ]]; then elif [[ "${{ steps.bump-version-automatic.outcome }}" = "success" ]]; then
echo "version=${{ steps.calculate-next-version.outputs.version }}" >> $GITHUB_OUTPUT echo "version=${{ steps.calculate-next-version.outputs.version }}" >> $GITHUB_OUTPUT
fi fi
- name: Configure Git
run: |
git config --local user.email "actions@github.com"
git config --local user.name "Github Actions"
- name: Create version branch
id: create-branch
run: |
NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
git switch -c $NAME
echo "name=$NAME" >> $GITHUB_OUTPUT
- name: Commit files - name: Commit files
run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a
- name: Push changes - name: Push changes
run: git push run: git push
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}
private-key: ${{ secrets.BW_GHAPP_KEY }}
owner: ${{ github.repository_owner }}
- name: Create version PR
id: create-pr
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
TITLE: "Bump version to ${{ steps.set-final-version-output.outputs.version }}"
run: |
PR_URL=$(gh pr create --title "$TITLE" \
--base "main" \
--head "$PR_BRANCH" \
--label "version update" \
--label "automated pr" \
--body "
## Type of change
- [ ] Bug fix
- [ ] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [X] Other
## Objective
Automated version bump to ${{ steps.set-final-version-output.outputs.version }}")
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
- name: Approve PR
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr review $PR_NUMBER --approve
- name: Merge PR
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
cherry_pick: cherry_pick:
name: Cherry-Pick Commit(s) name: Cherry-Pick Commit(s)
runs-on: ubuntu-22.04 if: ${{ needs.setup.outputs.branch != 'none' }}
needs: bump_version runs-on: ubuntu-24.04
needs:
- bump_version
- setup
steps: steps:
- name: Check out main branch - name: Check out main branch
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with: with:
ref: main ref: main
token: ${{ needs.setup.outputs.token }}
- name: Install xmllint
run: |
sudo apt-get update
sudo apt-get install -y libxml2-utils
- name: Verify version has been updated
env:
NEW_VERSION: ${{ needs.bump_version.outputs.version }}
run: |
# Wait for version to change.
while : ; do
echo "Waiting for version to be updated..."
git pull --force
CURRENT_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
# If the versions don't match we continue the loop, otherwise we break out of the loop.
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break
sleep 10
done
- name: Get last version commit(s)
id: get-commits
run: |
git switch main
MAIN_COMMIT=$(git log --reverse --pretty=format:"%H" --max-count=1 Directory.Build.props)
echo "main_commit=$MAIN_COMMIT" >> $GITHUB_OUTPUT
if [[ $(git ls-remote --heads origin rc) ]]; then
git switch rc
RC_COMMIT=$(git log --reverse --pretty=format:"%H" --max-count=1 Directory.Build.props)
echo "rc_commit=$RC_COMMIT" >> $GITHUB_OUTPUT
RC_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
echo "rc_version=$RC_VERSION" >> $GITHUB_OUTPUT
fi
- name: Configure Git - name: Configure Git
run: | run: |
git config --local user.email "actions@github.com" git config --local user.email "actions@github.com"
git config --local user.name "Github Actions" git config --local user.name "Github Actions"
- name: Install xmllint
run: |
sudo apt-get update
sudo apt-get install -y libxml2-utils
- name: Perform cherry-pick(s) - name: Perform cherry-pick(s)
env: env:
CUT_BRANCH: ${{ inputs.branch_to_cut }} CUT_BRANCH: ${{ needs.setup.outputs.branch }}
MAIN_COMMIT: ${{ steps.get-commits.outputs.main_commit }}
RC_COMMIT: ${{ steps.get-commits.outputs.rc_commit }}
RC_VERSION: ${{ steps.get-commits.outputs.rc_version }}
run: | run: |
# Function for cherry-picking
cherry_pick () {
local source_branch=$1
local destination_branch=$2
# Get project commit/version from source branch
git switch $source_branch
SOURCE_COMMIT=$(git log --reverse --pretty=format:"%H" --max-count=1 Directory.Build.props)
SOURCE_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
# Get project commit/version from destination branch
git switch $destination_branch
DESTINATION_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
if [[ "$DESTINATION_VERSION" != "$SOURCE_VERSION" ]]; then
git cherry-pick --strategy-option=theirs -x $SOURCE_COMMIT
git push -u origin $destination_branch
fi
# If we are cutting 'hotfix-rc': # If we are cutting 'hotfix-rc':
if [[ "$CUT_BRANCH" == "hotfix-rc" ]]; then if [[ "$CUT_BRANCH" == "hotfix-rc" ]]; then
@ -257,25 +236,16 @@ jobs:
if [[ $(git ls-remote --heads origin rc) ]]; then if [[ $(git ls-remote --heads origin rc) ]]; then
# Chery-pick from 'rc' into 'hotfix-rc' # Chery-pick from 'rc' into 'hotfix-rc'
git switch hotfix-rc cherry_pick rc hotfix-rc
HOTFIX_RC_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
if [[ "$HOTFIX_RC_VERSION" != "$RC_VERSION" ]]; then
git cherry-pick --strategy-option=theirs -x $RC_COMMIT
git push -u origin hotfix-rc
fi
# Cherry-pick from 'main' into 'rc' # Cherry-pick from 'main' into 'rc'
git switch rc cherry_pick main rc
git cherry-pick --strategy-option=theirs -x $MAIN_COMMIT
git push -u origin rc
# If the 'rc' branch does not exist: # If the 'rc' branch does not exist:
else else
# Cherry-pick from 'main' into 'hotfix-rc' # Cherry-pick from 'main' into 'hotfix-rc'
git switch hotfix-rc cherry_pick main hotfix-rc
git cherry-pick --strategy-option=theirs -x $MAIN_COMMIT
git push -u origin hotfix-rc
fi fi
@ -283,9 +253,7 @@ jobs:
elif [[ "$CUT_BRANCH" == "rc" ]]; then elif [[ "$CUT_BRANCH" == "rc" ]]; then
# Cherry-pick from 'main' into 'rc' # Cherry-pick from 'main' into 'rc'
git switch rc cherry_pick main rc
git cherry-pick --strategy-option=theirs -x $MAIN_COMMIT
git push -u origin rc
fi fi

View File

@ -30,9 +30,28 @@ on:
- "test/Infrastructure.IntegrationTest/**" # Any changes to the tests - "test/Infrastructure.IntegrationTest/**" # Any changes to the tests
jobs: jobs:
check-test-secrets:
name: Check for test secrets
runs-on: ubuntu-22.04
outputs:
available: ${{ steps.check-test-secrets.outputs.available }}
permissions:
contents: read
steps:
- name: Check
id: check-test-secrets
run: |
if [ "${{ secrets.CODECOV_TOKEN }}" != '' ]; then
echo "available=true" >> $GITHUB_OUTPUT;
else
echo "available=false" >> $GITHUB_OUTPUT;
fi
test: test:
name: Run tests name: Run tests
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
needs: check-test-secrets
steps: steps:
- name: Check out repo - name: Check out repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
@ -128,7 +147,7 @@ jobs:
- name: Report test results - name: Report test results
uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1 uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5 # v1.9.1
if: always() if: ${{ needs.check-test-secrets.outputs.available == 'true' && !cancelled() }}
with: with:
name: Test Results name: Test Results
path: "**/*-test-results.trx" path: "**/*-test-results.trx"

View File

@ -392,7 +392,9 @@ public class ProviderService : IProviderService
var organization = await _organizationRepository.GetByIdAsync(organizationId); var organization = await _organizationRepository.GetByIdAsync(organizationId);
ThrowOnInvalidPlanType(organization.PlanType); var provider = await _providerRepository.GetByIdAsync(providerId);
ThrowOnInvalidPlanType(provider.Type, organization.PlanType);
if (organization.UseSecretsManager) if (organization.UseSecretsManager)
{ {
@ -407,8 +409,6 @@ public class ProviderService : IProviderService
Key = key, Key = key,
}; };
var provider = await _providerRepository.GetByIdAsync(providerId);
await ApplyProviderPriceRateAsync(organization, provider); await ApplyProviderPriceRateAsync(organization, provider);
await _providerOrganizationRepository.CreateAsync(providerOrganization); await _providerOrganizationRepository.CreateAsync(providerOrganization);
@ -547,7 +547,7 @@ public class ProviderService : IProviderService
var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && provider.IsBillable(); var consolidatedBillingEnabled = _featureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && provider.IsBillable();
ThrowOnInvalidPlanType(organizationSignup.Plan, consolidatedBillingEnabled); ThrowOnInvalidPlanType(provider.Type, organizationSignup.Plan, consolidatedBillingEnabled);
var (organization, _, defaultCollection) = consolidatedBillingEnabled var (organization, _, defaultCollection) = consolidatedBillingEnabled
? await _organizationService.SignupClientAsync(organizationSignup) ? await _organizationService.SignupClientAsync(organizationSignup)
@ -687,11 +687,27 @@ public class ProviderService : IProviderService
return confirmedOwnersIds.Except(providerUserIds).Any(); return confirmedOwnersIds.Except(providerUserIds).Any();
} }
private void ThrowOnInvalidPlanType(PlanType requestedType, bool consolidatedBillingEnabled = false) private void ThrowOnInvalidPlanType(ProviderType providerType, PlanType requestedType, bool consolidatedBillingEnabled = false)
{ {
if (consolidatedBillingEnabled && requestedType is not (PlanType.TeamsMonthly or PlanType.EnterpriseMonthly)) if (consolidatedBillingEnabled)
{ {
throw new BadRequestException($"Providers cannot manage organizations with the plan type {requestedType}. Only Teams (Monthly) and Enterprise (Monthly) are allowed."); switch (providerType)
{
case ProviderType.Msp:
if (requestedType is not (PlanType.TeamsMonthly or PlanType.EnterpriseMonthly))
{
throw new BadRequestException($"Managed Service Providers cannot manage organizations with the plan type {requestedType}. Only Teams (Monthly) and Enterprise (Monthly) are allowed.");
}
break;
case ProviderType.MultiOrganizationEnterprise:
if (requestedType is not (PlanType.EnterpriseMonthly or PlanType.EnterpriseAnnually))
{
throw new BadRequestException($"Multi-organization Enterprise Providers cannot manage organizations with the plan type {requestedType}. Only Enterprise (Monthly) and Enterprise (Annually) are allowed.");
}
break;
default:
throw new BadRequestException($"Unsupported provider type {providerType}.");
}
} }
if (ProviderDisallowedOrganizationTypes.Contains(requestedType)) if (ProviderDisallowedOrganizationTypes.Contains(requestedType))

View File

@ -209,16 +209,9 @@ public class ProviderBillingService(
{ {
ArgumentNullException.ThrowIfNull(provider); ArgumentNullException.ThrowIfNull(provider);
if (provider.Type != ProviderType.Msp) if (!provider.SupportsConsolidatedBilling())
{ {
logger.LogError("Non-MSP provider ({ProviderID}) cannot scale their seats", provider.Id); logger.LogError("Provider ({ProviderID}) cannot scale their seats", provider.Id);
throw new BillingException();
}
if (!planType.SupportsConsolidatedBilling())
{
logger.LogError("Cannot scale provider ({ProviderID}) seats for plan type {PlanType} as it does not support consolidated billing", provider.Id, planType.ToString());
throw new BillingException(); throw new BillingException();
} }
@ -488,6 +481,23 @@ public class ProviderBillingService(
}; };
await stripeAdapter.SubscriptionUpdateAsync(command.GatewaySubscriptionId, updateOptions); await stripeAdapter.SubscriptionUpdateAsync(command.GatewaySubscriptionId, updateOptions);
// 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(plan.ProviderId);
foreach (var providerOrganization in providerOrganizations)
{
var organization = await organizationRepository.GetByIdAsync(providerOrganization.OrganizationId);
if (organization == null)
{
throw new ConflictException($"Organization '{providerOrganization.Id}' not found.");
}
organization.PlanType = command.NewPlan;
organization.Plan = StaticStore.GetPlan(command.NewPlan).Name;
await organizationRepository.ReplaceAsync(organization);
}
} }
public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command) public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command)

View File

@ -93,7 +93,8 @@ public class ProviderBillingController(
subscription, subscription,
providerPlans, providerPlans,
taxInformation, taxInformation,
subscriptionSuspension); subscriptionSuspension,
provider);
return TypedResults.Ok(response); return TypedResults.Ok(response);
} }

View File

@ -12,7 +12,7 @@ public class CreateClientOrganizationRequestBody
[Required(ErrorMessage = "'ownerEmail' must be provided")] [Required(ErrorMessage = "'ownerEmail' must be provided")]
public string OwnerEmail { get; set; } public string OwnerEmail { get; set; }
[EnumMatches<PlanType>(PlanType.TeamsMonthly, PlanType.EnterpriseMonthly, ErrorMessage = "'planType' must be Teams (Monthly) or Enterprise (Monthly)")] [EnumMatches<PlanType>(PlanType.TeamsMonthly, PlanType.EnterpriseMonthly, PlanType.EnterpriseAnnually, ErrorMessage = "'planType' must be Teams (Monthly), Enterprise (Monthly) or Enterprise (Annually)")]
public PlanType PlanType { get; set; } public PlanType PlanType { get; set; }
[Range(1, int.MaxValue, ErrorMessage = "'seats' must be greater than 0")] [Range(1, int.MaxValue, ErrorMessage = "'seats' must be greater than 0")]

View File

@ -1,4 +1,7 @@
using Bit.Core.Billing.Entities; using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.Billing.Entities;
using Bit.Core.Billing.Enums;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Stripe; using Stripe;
@ -14,7 +17,8 @@ public record ProviderSubscriptionResponse(
decimal AccountCredit, decimal AccountCredit,
TaxInformation TaxInformation, TaxInformation TaxInformation,
DateTime? CancelAt, DateTime? CancelAt,
SubscriptionSuspension Suspension) SubscriptionSuspension Suspension,
ProviderType ProviderType)
{ {
private const string _annualCadence = "Annual"; private const string _annualCadence = "Annual";
private const string _monthlyCadence = "Monthly"; private const string _monthlyCadence = "Monthly";
@ -23,7 +27,8 @@ public record ProviderSubscriptionResponse(
Subscription subscription, Subscription subscription,
ICollection<ProviderPlan> providerPlans, ICollection<ProviderPlan> providerPlans,
TaxInformation taxInformation, TaxInformation taxInformation,
SubscriptionSuspension subscriptionSuspension) SubscriptionSuspension subscriptionSuspension,
Provider provider)
{ {
var providerPlanResponses = providerPlans var providerPlanResponses = providerPlans
.Where(providerPlan => providerPlan.IsConfigured()) .Where(providerPlan => providerPlan.IsConfigured())
@ -35,6 +40,8 @@ public record ProviderSubscriptionResponse(
var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence; var cadence = plan.IsAnnual ? _annualCadence : _monthlyCadence;
return new ProviderPlanResponse( return new ProviderPlanResponse(
plan.Name, plan.Name,
plan.Type,
plan.ProductTier,
configuredProviderPlan.SeatMinimum, configuredProviderPlan.SeatMinimum,
configuredProviderPlan.PurchasedSeats, configuredProviderPlan.PurchasedSeats,
configuredProviderPlan.AssignedSeats, configuredProviderPlan.AssignedSeats,
@ -53,12 +60,15 @@ public record ProviderSubscriptionResponse(
accountCredit, accountCredit,
taxInformation, taxInformation,
subscription.CancelAt, subscription.CancelAt,
subscriptionSuspension); subscriptionSuspension,
provider.Type);
} }
} }
public record ProviderPlanResponse( public record ProviderPlanResponse(
string PlanName, string PlanName,
PlanType Type,
ProductTierType ProductTier,
int SeatMinimum, int SeatMinimum,
int PurchasedSeats, int PurchasedSeats,
int AssignedSeats, int AssignedSeats,

View File

@ -1,7 +1,9 @@
using Bit.Api.Vault.Models.Response; using Bit.Api.Vault.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums.Provider; using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
@ -10,6 +12,7 @@ using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tools.Repositories; using Bit.Core.Tools.Repositories;
using Bit.Core.Vault.Models.Data;
using Bit.Core.Vault.Repositories; using Bit.Core.Vault.Repositories;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -30,6 +33,8 @@ public class SyncController : Controller
private readonly IPolicyRepository _policyRepository; private readonly IPolicyRepository _policyRepository;
private readonly ISendRepository _sendRepository; private readonly ISendRepository _sendRepository;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly ICurrentContext _currentContext;
private readonly Version _sshKeyCipherMinimumVersion = new(Constants.SSHKeyCipherMinimumVersion);
private readonly IFeatureService _featureService; private readonly IFeatureService _featureService;
public SyncController( public SyncController(
@ -43,6 +48,7 @@ public class SyncController : Controller
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
ISendRepository sendRepository, ISendRepository sendRepository,
GlobalSettings globalSettings, GlobalSettings globalSettings,
ICurrentContext currentContext,
IFeatureService featureService) IFeatureService featureService)
{ {
_userService = userService; _userService = userService;
@ -55,6 +61,7 @@ public class SyncController : Controller
_policyRepository = policyRepository; _policyRepository = policyRepository;
_sendRepository = sendRepository; _sendRepository = sendRepository;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_currentContext = currentContext;
_featureService = featureService; _featureService = featureService;
} }
@ -77,7 +84,8 @@ public class SyncController : Controller
var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled); var hasEnabledOrgs = organizationUserDetails.Any(o => o.Enabled);
var folders = await _folderRepository.GetManyByUserIdAsync(user.Id); var folders = await _folderRepository.GetManyByUserIdAsync(user.Id);
var ciphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, withOrganizations: hasEnabledOrgs); var allCiphers = await _cipherRepository.GetManyByUserIdAsync(user.Id, withOrganizations: hasEnabledOrgs);
var ciphers = FilterSSHKeys(allCiphers);
var sends = await _sendRepository.GetManyByUserIdAsync(user.Id); var sends = await _sendRepository.GetManyByUserIdAsync(user.Id);
IEnumerable<CollectionDetails> collections = null; IEnumerable<CollectionDetails> collections = null;
@ -101,4 +109,16 @@ public class SyncController : Controller
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends); folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends);
return response; return response;
} }
private ICollection<CipherDetails> FilterSSHKeys(ICollection<CipherDetails> ciphers)
{
if (_currentContext.ClientVersion >= _sshKeyCipherMinimumVersion)
{
return ciphers;
}
else
{
return ciphers.Where(c => c.Type != Core.Vault.Enums.CipherType.SSHKey).ToList();
}
}
} }

View File

@ -0,0 +1,26 @@
using Bit.Core.Utilities;
using Bit.Core.Vault.Models.Data;
namespace Bit.Api.Vault.Models;
public class CipherSSHKeyModel
{
public CipherSSHKeyModel() { }
public CipherSSHKeyModel(CipherSSHKeyData data)
{
PrivateKey = data.PrivateKey;
PublicKey = data.PublicKey;
KeyFingerprint = data.KeyFingerprint;
}
[EncryptedString]
[EncryptedStringLength(5000)]
public string PrivateKey { get; set; }
[EncryptedString]
[EncryptedStringLength(5000)]
public string PublicKey { get; set; }
[EncryptedString]
[EncryptedStringLength(1000)]
public string KeyFingerprint { get; set; }
}

View File

@ -37,6 +37,7 @@ public class CipherRequestModel
public CipherCardModel Card { get; set; } public CipherCardModel Card { get; set; }
public CipherIdentityModel Identity { get; set; } public CipherIdentityModel Identity { get; set; }
public CipherSecureNoteModel SecureNote { get; set; } public CipherSecureNoteModel SecureNote { get; set; }
public CipherSSHKeyModel SSHKey { get; set; }
public DateTime? LastKnownRevisionDate { get; set; } = null; public DateTime? LastKnownRevisionDate { get; set; } = null;
public CipherDetails ToCipherDetails(Guid userId, bool allowOrgIdSet = true) public CipherDetails ToCipherDetails(Guid userId, bool allowOrgIdSet = true)
@ -82,6 +83,9 @@ public class CipherRequestModel
case CipherType.SecureNote: case CipherType.SecureNote:
existingCipher.Data = JsonSerializer.Serialize(ToCipherSecureNoteData(), JsonHelpers.IgnoreWritingNull); existingCipher.Data = JsonSerializer.Serialize(ToCipherSecureNoteData(), JsonHelpers.IgnoreWritingNull);
break; break;
case CipherType.SSHKey:
existingCipher.Data = JsonSerializer.Serialize(ToCipherSSHKeyData(), JsonHelpers.IgnoreWritingNull);
break;
default: default:
throw new ArgumentException("Unsupported type: " + nameof(Type) + "."); throw new ArgumentException("Unsupported type: " + nameof(Type) + ".");
} }
@ -230,6 +234,21 @@ public class CipherRequestModel
Type = SecureNote.Type, Type = SecureNote.Type,
}; };
} }
private CipherSSHKeyData ToCipherSSHKeyData()
{
return new CipherSSHKeyData
{
Name = Name,
Notes = Notes,
Fields = Fields?.Select(f => f.ToCipherFieldData()),
PasswordHistory = PasswordHistory?.Select(ph => ph.ToCipherPasswordHistoryData()),
PrivateKey = SSHKey.PrivateKey,
PublicKey = SSHKey.PublicKey,
KeyFingerprint = SSHKey.KeyFingerprint,
};
}
} }
public class CipherWithIdRequestModel : CipherRequestModel public class CipherWithIdRequestModel : CipherRequestModel

View File

@ -48,6 +48,12 @@ public class CipherMiniResponseModel : ResponseModel
cipherData = identityData; cipherData = identityData;
Identity = new CipherIdentityModel(identityData); Identity = new CipherIdentityModel(identityData);
break; break;
case CipherType.SSHKey:
var sshKeyData = JsonSerializer.Deserialize<CipherSSHKeyData>(cipher.Data);
Data = sshKeyData;
cipherData = sshKeyData;
SSHKey = new CipherSSHKeyModel(sshKeyData);
break;
default: default:
throw new ArgumentException("Unsupported " + nameof(Type) + "."); throw new ArgumentException("Unsupported " + nameof(Type) + ".");
} }
@ -76,6 +82,7 @@ public class CipherMiniResponseModel : ResponseModel
public CipherCardModel Card { get; set; } public CipherCardModel Card { get; set; }
public CipherIdentityModel Identity { get; set; } public CipherIdentityModel Identity { get; set; }
public CipherSecureNoteModel SecureNote { get; set; } public CipherSecureNoteModel SecureNote { get; set; }
public CipherSSHKeyModel SSHKey { get; set; }
public IEnumerable<CipherFieldModel> Fields { get; set; } public IEnumerable<CipherFieldModel> Fields { get; set; }
public IEnumerable<CipherPasswordHistoryModel> PasswordHistory { get; set; } public IEnumerable<CipherPasswordHistoryModel> PasswordHistory { get; set; }
public IEnumerable<AttachmentResponseModel> Attachments { get; set; } public IEnumerable<AttachmentResponseModel> Attachments { get; set; }

View File

@ -43,5 +43,5 @@ public static class BillingExtensions
}; };
public static bool SupportsConsolidatedBilling(this PlanType planType) public static bool SupportsConsolidatedBilling(this PlanType planType)
=> planType is PlanType.TeamsMonthly or PlanType.EnterpriseMonthly; => planType is PlanType.TeamsMonthly or PlanType.EnterpriseMonthly or PlanType.EnterpriseAnnually;
} }

View File

@ -22,6 +22,7 @@ public static class Constants
public const int OrganizationSelfHostSubscriptionGracePeriodDays = 60; public const int OrganizationSelfHostSubscriptionGracePeriodDays = 60;
public const string Fido2KeyCipherMinimumVersion = "2023.10.0"; public const string Fido2KeyCipherMinimumVersion = "2023.10.0";
public const string SSHKeyCipherMinimumVersion = "2024.12.0";
/// <summary> /// <summary>
/// Used by IdentityServer to identify our own provider. /// Used by IdentityServer to identify our own provider.
@ -100,7 +101,6 @@ public static class AuthenticationSchemes
public static class FeatureFlagKeys public static class FeatureFlagKeys
{ {
public const string DisplayEuEnvironment = "display-eu-environment";
public const string BrowserFilelessImport = "browser-fileless-import"; public const string BrowserFilelessImport = "browser-fileless-import";
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair"; public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection"; public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
@ -123,6 +123,8 @@ public static class FeatureFlagKeys
public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements"; public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements";
public const string ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner"; public const string ProviderClientVaultPrivacyBanner = "ac-2833-provider-client-vault-privacy-banner";
public const string DeviceTrustLogging = "pm-8285-device-trust-logging"; public const string DeviceTrustLogging = "pm-8285-device-trust-logging";
public const string SSHKeyItemVaultItem = "ssh-key-vault-item";
public const string SSHAgent = "ssh-agent";
public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token"; public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token";
public const string EnableUpgradePasswordManagerSub = "AC-2708-upgrade-password-manager-sub"; public const string EnableUpgradePasswordManagerSub = "AC-2708-upgrade-password-manager-sub";
public const string IdpAutoSubmitLogin = "idp-auto-submit-login"; public const string IdpAutoSubmitLogin = "idp-auto-submit-login";
@ -148,6 +150,7 @@ public static class FeatureFlagKeys
public const string LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split"; public const string LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split";
public const string GeneratorToolsModernization = "generator-tools-modernization"; public const string GeneratorToolsModernization = "generator-tools-modernization";
public const string NewDeviceVerification = "new-device-verification"; public const string NewDeviceVerification = "new-device-verification";
public const string RiskInsightsCriticalApplication = "pm-14466-risk-insights-critical-application";
public static List<string> GetAllKeys() public static List<string> GetAllKeys()
{ {

View File

@ -8,4 +8,5 @@ public enum CipherType : byte
SecureNote = 2, SecureNote = 2,
Card = 3, Card = 3,
Identity = 4, Identity = 4,
SSHKey = 5,
} }

View File

@ -0,0 +1,10 @@
namespace Bit.Core.Vault.Models.Data;
public class CipherSSHKeyData : CipherData
{
public CipherSSHKeyData() { }
public string PrivateKey { get; set; }
public string PublicKey { get; set; }
public string KeyFingerprint { get; set; }
}