mirror of
https://github.com/bitwarden/server.git
synced 2024-11-22 12:15:36 +01:00
Merge branch 'refs/heads/main' into km/pm-10600
This commit is contained in:
commit
67aa2eb6d9
@ -3,7 +3,7 @@
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"swashbuckle.aspnetcore.cli": {
|
||||
"version": "6.8.1",
|
||||
"version": "6.9.0",
|
||||
"commands": ["swagger"]
|
||||
},
|
||||
"dotnet-ef": {
|
||||
|
@ -29,7 +29,7 @@ jobs:
|
||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||
|
||||
- name: Check out branch
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
token: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
|
||||
@ -53,7 +53,7 @@ jobs:
|
||||
if: ${{ needs.setup.outputs.copy_finalization_scripts == 'true' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
@ -107,7 +107,7 @@ jobs:
|
||||
devops-alerts-slack-webhook-url"
|
||||
|
||||
- name: Import GPG keys
|
||||
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0
|
||||
uses: crazy-max/ghaction-import-gpg@cb9bde2e2525e640591a934b1fd28eef1dcaf5e5 # v6.2.0
|
||||
with:
|
||||
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
|
||||
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
|
||||
|
24
.github/workflows/build.yml
vendored
24
.github/workflows/build.yml
vendored
@ -18,10 +18,10 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
|
||||
- name: Verify format
|
||||
run: dotnet format --verify-no-changes
|
||||
@ -67,13 +67,13 @@ jobs:
|
||||
node: true
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4
|
||||
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
|
||||
with:
|
||||
cache: "npm"
|
||||
cache-dependency-path: "**/package-lock.json"
|
||||
@ -172,7 +172,7 @@ jobs:
|
||||
dotnet: true
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Check branch to publish
|
||||
env:
|
||||
@ -274,14 +274,14 @@ jobs:
|
||||
|
||||
- name: Scan Docker image
|
||||
id: container-scan
|
||||
uses: anchore/scan-action@49e50b215b647c5ec97abb66f69af73c46a4ca08 # v5.0.1
|
||||
uses: anchore/scan-action@5ed195cc06065322983cae4bb31e2a751feb86fd # v5.2.0
|
||||
with:
|
||||
image: ${{ steps.image-tags.outputs.primary_tag }}
|
||||
fail-build: false
|
||||
output-format: sarif
|
||||
|
||||
- name: Upload Grype results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
|
||||
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||
with:
|
||||
sarif_file: ${{ steps.container-scan.outputs.sarif }}
|
||||
|
||||
@ -291,10 +291,10 @@ jobs:
|
||||
needs: build-docker
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
|
||||
- name: Log in to Azure - production subscription
|
||||
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
|
||||
@ -466,10 +466,10 @@ jobs:
|
||||
- win-x64
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
|
@ -12,7 +12,7 @@ jobs:
|
||||
config-exists: ${{ steps.validate-config.outputs.config-exists }}
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Validate config exists in path
|
||||
id: validate-config
|
||||
|
2
.github/workflows/cleanup-rc-branch.yml
vendored
2
.github/workflows/cleanup-rc-branch.yml
vendored
@ -23,7 +23,7 @@ jobs:
|
||||
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||
|
||||
- name: Checkout main
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: main
|
||||
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||
|
2
.github/workflows/code-references.yml
vendored
2
.github/workflows/code-references.yml
vendored
@ -33,7 +33,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Collect
|
||||
id: collect
|
||||
|
2
.github/workflows/protect-files.yml
vendored
2
.github/workflows/protect-files.yml
vendored
@ -28,7 +28,7 @@ jobs:
|
||||
label: "DB-migrations-changed"
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
|
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@ -98,7 +98,7 @@ jobs:
|
||||
echo "Github Release Option: $RELEASE_OPTION"
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up project name
|
||||
id: setup
|
||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -36,7 +36,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Check release version
|
||||
id: version
|
||||
|
6
.github/workflows/repository-management.yml
vendored
6
.github/workflows/repository-management.yml
vendored
@ -27,7 +27,7 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out target ref
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ inputs.target_ref }}
|
||||
|
||||
@ -62,7 +62,7 @@ jobs:
|
||||
version: ${{ inputs.version_number_override }}
|
||||
|
||||
- name: Check out branch
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: main
|
||||
|
||||
@ -198,7 +198,7 @@ jobs:
|
||||
needs: bump_version
|
||||
steps:
|
||||
- name: Check out main branch
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: main
|
||||
|
||||
|
10
.github/workflows/scan.yml
vendored
10
.github/workflows/scan.yml
vendored
@ -26,7 +26,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
@ -46,7 +46,7 @@ jobs:
|
||||
--output-path . ${{ env.INCREMENTAL }}
|
||||
|
||||
- name: Upload Checkmarx results to GitHub
|
||||
uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
|
||||
uses: github/codeql-action/upload-sarif@662472033e021d55d94146f66f6058822b0b39fd # v3.27.0
|
||||
with:
|
||||
sarif_file: cx_result.sarif
|
||||
|
||||
@ -60,19 +60,19 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@b36c23c0d998641eff861008f374ee103c25ac73 # v4.4.0
|
||||
uses: actions/setup-java@8df1039502a15bceb9433410b1a100fbe190c53b # v4.5.0
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: "zulu"
|
||||
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
|
||||
- name: Install SonarCloud scanner
|
||||
run: dotnet tool install dotnet-sonarscanner -g
|
||||
|
8
.github/workflows/test-database.yml
vendored
8
.github/workflows/test-database.yml
vendored
@ -35,10 +35,10 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
|
||||
- name: Restore tools
|
||||
run: dotnet tool restore
|
||||
@ -146,10 +146,10 @@ jobs:
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
|
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@ -46,10 +46,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Check out repo
|
||||
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Set up .NET
|
||||
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
|
||||
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
|
||||
|
||||
- name: Print environment
|
||||
run: |
|
||||
|
@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<Version>2024.10.1</Version>
|
||||
<Version>2024.11.0</Version>
|
||||
|
||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
@ -11,6 +11,7 @@ using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
@ -437,144 +438,142 @@ public class ProviderBillingService(
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UpdateSeatMinimums(
|
||||
Provider provider,
|
||||
int enterpriseSeatMinimum,
|
||||
int teamsSeatMinimum)
|
||||
public async Task ChangePlan(ChangeProviderPlanCommand command)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(provider);
|
||||
var plan = await providerPlanRepository.GetByIdAsync(command.ProviderPlanId);
|
||||
|
||||
if (enterpriseSeatMinimum < 0 || teamsSeatMinimum < 0)
|
||||
if (plan == null)
|
||||
{
|
||||
throw new BadRequestException("Provider plan not found.");
|
||||
}
|
||||
|
||||
if (plan.PlanType == command.NewPlan)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var oldPlanConfiguration = StaticStore.GetPlan(plan.PlanType);
|
||||
|
||||
plan.PlanType = command.NewPlan;
|
||||
await providerPlanRepository.ReplaceAsync(plan);
|
||||
|
||||
Subscription subscription;
|
||||
try
|
||||
{
|
||||
subscription = await stripeAdapter.ProviderSubscriptionGetAsync(command.GatewaySubscriptionId, plan.ProviderId);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
throw new ConflictException("Subscription not found.");
|
||||
}
|
||||
|
||||
var oldSubscriptionItem = subscription.Items.SingleOrDefault(x =>
|
||||
x.Price.Id == oldPlanConfiguration.PasswordManager.StripeProviderPortalSeatPlanId);
|
||||
|
||||
var updateOptions = new SubscriptionUpdateOptions
|
||||
{
|
||||
Items =
|
||||
[
|
||||
new SubscriptionItemOptions
|
||||
{
|
||||
Price = StaticStore.GetPlan(command.NewPlan).PasswordManager.StripeProviderPortalSeatPlanId,
|
||||
Quantity = oldSubscriptionItem!.Quantity
|
||||
},
|
||||
new SubscriptionItemOptions
|
||||
{
|
||||
Id = oldSubscriptionItem.Id,
|
||||
Deleted = true
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
await stripeAdapter.SubscriptionUpdateAsync(command.GatewaySubscriptionId, updateOptions);
|
||||
}
|
||||
|
||||
public async Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command)
|
||||
{
|
||||
if (command.Configuration.Any(x => x.SeatsMinimum < 0))
|
||||
{
|
||||
throw new BadRequestException("Provider seat minimums must be at least 0.");
|
||||
}
|
||||
|
||||
var subscription = await stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId);
|
||||
Subscription subscription;
|
||||
try
|
||||
{
|
||||
subscription = await stripeAdapter.ProviderSubscriptionGetAsync(command.GatewaySubscriptionId, command.Id);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
throw new ConflictException("Subscription not found.");
|
||||
}
|
||||
|
||||
var subscriptionItemOptionsList = new List<SubscriptionItemOptions>();
|
||||
|
||||
var providerPlans = await providerPlanRepository.GetByProviderId(provider.Id);
|
||||
var providerPlans = await providerPlanRepository.GetByProviderId(command.Id);
|
||||
|
||||
var enterpriseProviderPlan =
|
||||
providerPlans.Single(providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly);
|
||||
|
||||
if (enterpriseProviderPlan.SeatMinimum != enterpriseSeatMinimum)
|
||||
foreach (var newPlanConfiguration in command.Configuration)
|
||||
{
|
||||
var enterprisePriceId = StaticStore.GetPlan(PlanType.EnterpriseMonthly).PasswordManager
|
||||
.StripeProviderPortalSeatPlanId;
|
||||
var providerPlan =
|
||||
providerPlans.Single(providerPlan => providerPlan.PlanType == newPlanConfiguration.Plan);
|
||||
|
||||
var enterpriseSubscriptionItem = subscription.Items.First(item => item.Price.Id == enterprisePriceId);
|
||||
|
||||
if (enterpriseProviderPlan.PurchasedSeats == 0)
|
||||
if (providerPlan.SeatMinimum != newPlanConfiguration.SeatsMinimum)
|
||||
{
|
||||
if (enterpriseProviderPlan.AllocatedSeats > enterpriseSeatMinimum)
|
||||
{
|
||||
enterpriseProviderPlan.PurchasedSeats =
|
||||
enterpriseProviderPlan.AllocatedSeats - enterpriseSeatMinimum;
|
||||
var priceId = StaticStore.GetPlan(newPlanConfiguration.Plan).PasswordManager
|
||||
.StripeProviderPortalSeatPlanId;
|
||||
var subscriptionItem = subscription.Items.First(item => item.Price.Id == priceId);
|
||||
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
if (providerPlan.PurchasedSeats == 0)
|
||||
{
|
||||
if (providerPlan.AllocatedSeats > newPlanConfiguration.SeatsMinimum)
|
||||
{
|
||||
Id = enterpriseSubscriptionItem.Id,
|
||||
Price = enterprisePriceId,
|
||||
Quantity = enterpriseProviderPlan.AllocatedSeats
|
||||
});
|
||||
providerPlan.PurchasedSeats = providerPlan.AllocatedSeats - newPlanConfiguration.SeatsMinimum;
|
||||
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Id = subscriptionItem.Id,
|
||||
Price = priceId,
|
||||
Quantity = providerPlan.AllocatedSeats
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Id = subscriptionItem.Id,
|
||||
Price = priceId,
|
||||
Quantity = newPlanConfiguration.SeatsMinimum
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
var totalSeats = providerPlan.SeatMinimum + providerPlan.PurchasedSeats;
|
||||
|
||||
if (newPlanConfiguration.SeatsMinimum <= totalSeats)
|
||||
{
|
||||
Id = enterpriseSubscriptionItem.Id,
|
||||
Price = enterprisePriceId,
|
||||
Quantity = enterpriseSeatMinimum
|
||||
});
|
||||
providerPlan.PurchasedSeats = totalSeats - newPlanConfiguration.SeatsMinimum;
|
||||
}
|
||||
else
|
||||
{
|
||||
providerPlan.PurchasedSeats = 0;
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Id = subscriptionItem.Id,
|
||||
Price = priceId,
|
||||
Quantity = newPlanConfiguration.SeatsMinimum
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
providerPlan.SeatMinimum = newPlanConfiguration.SeatsMinimum;
|
||||
|
||||
await providerPlanRepository.ReplaceAsync(providerPlan);
|
||||
}
|
||||
else
|
||||
{
|
||||
var totalEnterpriseSeats = enterpriseProviderPlan.SeatMinimum + enterpriseProviderPlan.PurchasedSeats;
|
||||
|
||||
if (enterpriseSeatMinimum <= totalEnterpriseSeats)
|
||||
{
|
||||
enterpriseProviderPlan.PurchasedSeats = totalEnterpriseSeats - enterpriseSeatMinimum;
|
||||
}
|
||||
else
|
||||
{
|
||||
enterpriseProviderPlan.PurchasedSeats = 0;
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Id = enterpriseSubscriptionItem.Id,
|
||||
Price = enterprisePriceId,
|
||||
Quantity = enterpriseSeatMinimum
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
enterpriseProviderPlan.SeatMinimum = enterpriseSeatMinimum;
|
||||
|
||||
await providerPlanRepository.ReplaceAsync(enterpriseProviderPlan);
|
||||
}
|
||||
|
||||
var teamsProviderPlan =
|
||||
providerPlans.Single(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly);
|
||||
|
||||
if (teamsProviderPlan.SeatMinimum != teamsSeatMinimum)
|
||||
{
|
||||
var teamsPriceId = StaticStore.GetPlan(PlanType.TeamsMonthly).PasswordManager
|
||||
.StripeProviderPortalSeatPlanId;
|
||||
|
||||
var teamsSubscriptionItem = subscription.Items.First(item => item.Price.Id == teamsPriceId);
|
||||
|
||||
if (teamsProviderPlan.PurchasedSeats == 0)
|
||||
{
|
||||
if (teamsProviderPlan.AllocatedSeats > teamsSeatMinimum)
|
||||
{
|
||||
teamsProviderPlan.PurchasedSeats = teamsProviderPlan.AllocatedSeats - teamsSeatMinimum;
|
||||
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Id = teamsSubscriptionItem.Id,
|
||||
Price = teamsPriceId,
|
||||
Quantity = teamsProviderPlan.AllocatedSeats
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Id = teamsSubscriptionItem.Id,
|
||||
Price = teamsPriceId,
|
||||
Quantity = teamsSeatMinimum
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var totalTeamsSeats = teamsProviderPlan.SeatMinimum + teamsProviderPlan.PurchasedSeats;
|
||||
|
||||
if (teamsSeatMinimum <= totalTeamsSeats)
|
||||
{
|
||||
teamsProviderPlan.PurchasedSeats = totalTeamsSeats - teamsSeatMinimum;
|
||||
}
|
||||
else
|
||||
{
|
||||
teamsProviderPlan.PurchasedSeats = 0;
|
||||
subscriptionItemOptionsList.Add(new SubscriptionItemOptions
|
||||
{
|
||||
Id = teamsSubscriptionItem.Id,
|
||||
Price = teamsPriceId,
|
||||
Quantity = teamsSeatMinimum
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
teamsProviderPlan.SeatMinimum = teamsSeatMinimum;
|
||||
|
||||
await providerPlanRepository.ReplaceAsync(teamsProviderPlan);
|
||||
}
|
||||
|
||||
if (subscriptionItemOptionsList.Count > 0)
|
||||
{
|
||||
await stripeAdapter.SubscriptionUpdateAsync(provider.GatewaySubscriptionId,
|
||||
await stripeAdapter.SubscriptionUpdateAsync(command.GatewaySubscriptionId,
|
||||
new SubscriptionUpdateOptions { Items = subscriptionItemOptionsList });
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ using Bit.Core.Billing.Entities;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -1011,26 +1012,192 @@ public class ProviderBillingServiceTests
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateSeatMinimums
|
||||
#region ChangePlan
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateSeatMinimums_NullProvider_ThrowsArgumentNullException(
|
||||
SutProvider<ProviderBillingService> sutProvider) =>
|
||||
await Assert.ThrowsAsync<ArgumentNullException>(() => sutProvider.Sut.UpdateSeatMinimums(null, 0, 0));
|
||||
public async Task ChangePlan_NullProviderPlan_ThrowsBadRequestException(
|
||||
ChangeProviderPlanCommand command,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||
providerPlanRepository.GetByIdAsync(Arg.Any<Guid>()).Returns((ProviderPlan)null);
|
||||
|
||||
// Act
|
||||
var actual = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.ChangePlan(command));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Provider plan not found.", actual.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ChangePlan_ProviderNotFound_DoesNothing(
|
||||
ChangeProviderPlanCommand command,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
var existingPlan = new ProviderPlan
|
||||
{
|
||||
Id = command.ProviderPlanId,
|
||||
PlanType = command.NewPlan,
|
||||
PurchasedSeats = 0,
|
||||
AllocatedSeats = 0,
|
||||
SeatMinimum = 0
|
||||
};
|
||||
providerPlanRepository
|
||||
.GetByIdAsync(Arg.Is<Guid>(p => p == command.ProviderPlanId))
|
||||
.Returns(existingPlan);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ChangePlan(command);
|
||||
|
||||
// Assert
|
||||
await providerPlanRepository.Received(0).ReplaceAsync(Arg.Any<ProviderPlan>());
|
||||
await stripeAdapter.Received(0).SubscriptionUpdateAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ChangePlan_SameProviderPlan_DoesNothing(
|
||||
ChangeProviderPlanCommand command,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
var existingPlan = new ProviderPlan
|
||||
{
|
||||
Id = command.ProviderPlanId,
|
||||
PlanType = command.NewPlan,
|
||||
PurchasedSeats = 0,
|
||||
AllocatedSeats = 0,
|
||||
SeatMinimum = 0
|
||||
};
|
||||
providerPlanRepository
|
||||
.GetByIdAsync(Arg.Is<Guid>(p => p == command.ProviderPlanId))
|
||||
.Returns(existingPlan);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ChangePlan(command);
|
||||
|
||||
// Assert
|
||||
await providerPlanRepository.Received(0).ReplaceAsync(Arg.Any<ProviderPlan>());
|
||||
await stripeAdapter.Received(0).SubscriptionUpdateAsync(Arg.Any<string>(), Arg.Any<SubscriptionUpdateOptions>());
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task ChangePlan_UpdatesSubscriptionCorrectly(
|
||||
Guid providerPlanId,
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||
var existingPlan = new ProviderPlan
|
||||
{
|
||||
Id = providerPlanId,
|
||||
ProviderId = provider.Id,
|
||||
PlanType = PlanType.EnterpriseAnnually,
|
||||
PurchasedSeats = 2,
|
||||
AllocatedSeats = 10,
|
||||
SeatMinimum = 8
|
||||
};
|
||||
providerPlanRepository
|
||||
.GetByIdAsync(Arg.Is<Guid>(p => p == providerPlanId))
|
||||
.Returns(existingPlan);
|
||||
|
||||
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||
providerRepository.GetByIdAsync(Arg.Is(existingPlan.ProviderId)).Returns(provider);
|
||||
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
stripeAdapter.ProviderSubscriptionGetAsync(
|
||||
Arg.Is(provider.GatewaySubscriptionId),
|
||||
Arg.Is(provider.Id))
|
||||
.Returns(new Subscription
|
||||
{
|
||||
Id = provider.GatewaySubscriptionId,
|
||||
Items = new StripeList<SubscriptionItem>
|
||||
{
|
||||
Data =
|
||||
[
|
||||
new SubscriptionItem
|
||||
{
|
||||
Id = "si_ent_annual",
|
||||
Price = new Price
|
||||
{
|
||||
Id = StaticStore.GetPlan(PlanType.EnterpriseAnnually).PasswordManager
|
||||
.StripeProviderPortalSeatPlanId
|
||||
},
|
||||
Quantity = 10
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
var command =
|
||||
new ChangeProviderPlanCommand(providerPlanId, PlanType.EnterpriseMonthly, provider.GatewaySubscriptionId);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.ChangePlan(command);
|
||||
|
||||
// Assert
|
||||
await providerPlanRepository.Received(1)
|
||||
.ReplaceAsync(Arg.Is<ProviderPlan>(p => p.PlanType == PlanType.EnterpriseMonthly));
|
||||
|
||||
await stripeAdapter.Received(1)
|
||||
.SubscriptionUpdateAsync(
|
||||
Arg.Is(provider.GatewaySubscriptionId),
|
||||
Arg.Is<SubscriptionUpdateOptions>(p =>
|
||||
p.Items.Count(si => si.Id == "si_ent_annual" && si.Deleted == true) == 1));
|
||||
|
||||
var newPlanCfg = StaticStore.GetPlan(command.NewPlan);
|
||||
await stripeAdapter.Received(1)
|
||||
.SubscriptionUpdateAsync(
|
||||
Arg.Is(provider.GatewaySubscriptionId),
|
||||
Arg.Is<SubscriptionUpdateOptions>(p =>
|
||||
p.Items.Count(si =>
|
||||
si.Price == newPlanCfg.PasswordManager.StripeProviderPortalSeatPlanId &&
|
||||
si.Deleted == default &&
|
||||
si.Quantity == 10) == 1));
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region UpdateSeatMinimums
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateSeatMinimums_NegativeSeatMinimum_ThrowsBadRequestException(
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider) =>
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSeatMinimums(provider, -10, 100));
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||
var command = new UpdateProviderSeatMinimumsCommand(
|
||||
provider.Id,
|
||||
provider.GatewaySubscriptionId,
|
||||
[
|
||||
(PlanType.TeamsMonthly, -10),
|
||||
(PlanType.EnterpriseMonthly, 50)
|
||||
]);
|
||||
|
||||
// Act
|
||||
var actual = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.UpdateSeatMinimums(command));
|
||||
|
||||
// Assert
|
||||
Assert.Equal("Provider seat minimums must be at least 0.", actual.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task UpdateSeatMinimums_NoPurchasedSeats_AllocatedHigherThanIncomingMinimum_UpdatesPurchasedSeats_SyncsStripeWithNewSeatMinimum(
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||
|
||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||
const string teamsLineItemId = "teams_line_item_id";
|
||||
@ -1058,7 +1225,9 @@ public class ProviderBillingServiceTests
|
||||
}
|
||||
};
|
||||
|
||||
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription);
|
||||
stripeAdapter.ProviderSubscriptionGetAsync(
|
||||
provider.GatewaySubscriptionId,
|
||||
provider.Id).Returns(subscription);
|
||||
|
||||
var providerPlans = new List<ProviderPlan>
|
||||
{
|
||||
@ -1066,10 +1235,21 @@ public class ProviderBillingServiceTests
|
||||
new() { PlanType = PlanType.TeamsMonthly, SeatMinimum = 30, PurchasedSeats = 0, AllocatedSeats = 25 }
|
||||
};
|
||||
|
||||
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||
|
||||
await sutProvider.Sut.UpdateSeatMinimums(provider, 30, 20);
|
||||
var command = new UpdateProviderSeatMinimumsCommand(
|
||||
provider.Id,
|
||||
provider.GatewaySubscriptionId,
|
||||
[
|
||||
(PlanType.EnterpriseMonthly, 30),
|
||||
(PlanType.TeamsMonthly, 20)
|
||||
]);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateSeatMinimums(command);
|
||||
|
||||
// Assert
|
||||
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 30));
|
||||
|
||||
@ -1091,8 +1271,11 @@ public class ProviderBillingServiceTests
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||
|
||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||
const string teamsLineItemId = "teams_line_item_id";
|
||||
@ -1120,7 +1303,7 @@ public class ProviderBillingServiceTests
|
||||
}
|
||||
};
|
||||
|
||||
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription);
|
||||
stripeAdapter.ProviderSubscriptionGetAsync(provider.GatewaySubscriptionId, provider.Id).Returns(subscription);
|
||||
|
||||
var providerPlans = new List<ProviderPlan>
|
||||
{
|
||||
@ -1130,8 +1313,18 @@ public class ProviderBillingServiceTests
|
||||
|
||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||
|
||||
await sutProvider.Sut.UpdateSeatMinimums(provider, 70, 50);
|
||||
var command = new UpdateProviderSeatMinimumsCommand(
|
||||
provider.Id,
|
||||
provider.GatewaySubscriptionId,
|
||||
[
|
||||
(PlanType.EnterpriseMonthly, 70),
|
||||
(PlanType.TeamsMonthly, 50)
|
||||
]);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateSeatMinimums(command);
|
||||
|
||||
// Assert
|
||||
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 70));
|
||||
|
||||
@ -1153,8 +1346,11 @@ public class ProviderBillingServiceTests
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||
|
||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||
const string teamsLineItemId = "teams_line_item_id";
|
||||
@ -1182,7 +1378,7 @@ public class ProviderBillingServiceTests
|
||||
}
|
||||
};
|
||||
|
||||
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription);
|
||||
stripeAdapter.ProviderSubscriptionGetAsync(provider.GatewaySubscriptionId, provider.Id).Returns(subscription);
|
||||
|
||||
var providerPlans = new List<ProviderPlan>
|
||||
{
|
||||
@ -1192,8 +1388,18 @@ public class ProviderBillingServiceTests
|
||||
|
||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||
|
||||
await sutProvider.Sut.UpdateSeatMinimums(provider, 60, 60);
|
||||
var command = new UpdateProviderSeatMinimumsCommand(
|
||||
provider.Id,
|
||||
provider.GatewaySubscriptionId,
|
||||
[
|
||||
(PlanType.EnterpriseMonthly, 60),
|
||||
(PlanType.TeamsMonthly, 60)
|
||||
]);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateSeatMinimums(command);
|
||||
|
||||
// Assert
|
||||
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 60 && providerPlan.PurchasedSeats == 10));
|
||||
|
||||
@ -1209,8 +1415,11 @@ public class ProviderBillingServiceTests
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||
|
||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||
const string teamsLineItemId = "teams_line_item_id";
|
||||
@ -1238,7 +1447,7 @@ public class ProviderBillingServiceTests
|
||||
}
|
||||
};
|
||||
|
||||
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription);
|
||||
stripeAdapter.ProviderSubscriptionGetAsync(provider.GatewaySubscriptionId, provider.Id).Returns(subscription);
|
||||
|
||||
var providerPlans = new List<ProviderPlan>
|
||||
{
|
||||
@ -1248,8 +1457,18 @@ public class ProviderBillingServiceTests
|
||||
|
||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||
|
||||
await sutProvider.Sut.UpdateSeatMinimums(provider, 80, 80);
|
||||
var command = new UpdateProviderSeatMinimumsCommand(
|
||||
provider.Id,
|
||||
provider.GatewaySubscriptionId,
|
||||
[
|
||||
(PlanType.EnterpriseMonthly, 80),
|
||||
(PlanType.TeamsMonthly, 80)
|
||||
]);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateSeatMinimums(command);
|
||||
|
||||
// Assert
|
||||
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 80 && providerPlan.PurchasedSeats == 0));
|
||||
|
||||
@ -1271,8 +1490,11 @@ public class ProviderBillingServiceTests
|
||||
Provider provider,
|
||||
SutProvider<ProviderBillingService> sutProvider)
|
||||
{
|
||||
// Arrange
|
||||
var stripeAdapter = sutProvider.GetDependency<IStripeAdapter>();
|
||||
var providerPlanRepository = sutProvider.GetDependency<IProviderPlanRepository>();
|
||||
var providerRepository = sutProvider.GetDependency<IProviderRepository>();
|
||||
providerRepository.GetByIdAsync(provider.Id).Returns(provider);
|
||||
|
||||
const string enterpriseLineItemId = "enterprise_line_item_id";
|
||||
const string teamsLineItemId = "teams_line_item_id";
|
||||
@ -1300,7 +1522,7 @@ public class ProviderBillingServiceTests
|
||||
}
|
||||
};
|
||||
|
||||
stripeAdapter.SubscriptionGetAsync(provider.GatewaySubscriptionId).Returns(subscription);
|
||||
stripeAdapter.ProviderSubscriptionGetAsync(provider.GatewaySubscriptionId, provider.Id).Returns(subscription);
|
||||
|
||||
var providerPlans = new List<ProviderPlan>
|
||||
{
|
||||
@ -1310,8 +1532,18 @@ public class ProviderBillingServiceTests
|
||||
|
||||
providerPlanRepository.GetByProviderId(provider.Id).Returns(providerPlans);
|
||||
|
||||
await sutProvider.Sut.UpdateSeatMinimums(provider, 70, 30);
|
||||
var command = new UpdateProviderSeatMinimumsCommand(
|
||||
provider.Id,
|
||||
provider.GatewaySubscriptionId,
|
||||
[
|
||||
(PlanType.EnterpriseMonthly, 70),
|
||||
(PlanType.TeamsMonthly, 30)
|
||||
]);
|
||||
|
||||
// Act
|
||||
await sutProvider.Sut.UpdateSeatMinimums(command);
|
||||
|
||||
// Assert
|
||||
await providerPlanRepository.Received(1).ReplaceAsync(Arg.Is<ProviderPlan>(
|
||||
providerPlan => providerPlan.PlanType == PlanType.EnterpriseMonthly && providerPlan.SeatMinimum == 70));
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
|
||||
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
||||
|
@ -14,6 +14,7 @@ using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
@ -290,25 +291,39 @@ public class ProvidersController : Controller
|
||||
|
||||
var providerPlans = await _providerPlanRepository.GetByProviderId(id);
|
||||
|
||||
if (providerPlans.Count == 0)
|
||||
switch (provider.Type)
|
||||
{
|
||||
var newProviderPlans = new List<ProviderPlan>
|
||||
{
|
||||
new () { ProviderId = provider.Id, PlanType = PlanType.TeamsMonthly, SeatMinimum = model.TeamsMonthlySeatMinimum, PurchasedSeats = 0, AllocatedSeats = 0 },
|
||||
new () { ProviderId = provider.Id, PlanType = PlanType.EnterpriseMonthly, SeatMinimum = model.EnterpriseMonthlySeatMinimum, PurchasedSeats = 0, AllocatedSeats = 0 }
|
||||
};
|
||||
case ProviderType.Msp:
|
||||
var updateMspSeatMinimumsCommand = new UpdateProviderSeatMinimumsCommand(
|
||||
provider.Id,
|
||||
provider.GatewaySubscriptionId,
|
||||
[
|
||||
(Plan: PlanType.TeamsMonthly, SeatsMinimum: model.TeamsMonthlySeatMinimum),
|
||||
(Plan: PlanType.EnterpriseMonthly, SeatsMinimum: model.EnterpriseMonthlySeatMinimum)
|
||||
]);
|
||||
await _providerBillingService.UpdateSeatMinimums(updateMspSeatMinimumsCommand);
|
||||
break;
|
||||
case ProviderType.MultiOrganizationEnterprise:
|
||||
{
|
||||
var existingMoePlan = providerPlans.Single();
|
||||
|
||||
foreach (var newProviderPlan in newProviderPlans)
|
||||
{
|
||||
await _providerPlanRepository.CreateAsync(newProviderPlan);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await _providerBillingService.UpdateSeatMinimums(
|
||||
provider,
|
||||
model.EnterpriseMonthlySeatMinimum,
|
||||
model.TeamsMonthlySeatMinimum);
|
||||
// 1. Change the plan and take over any old values.
|
||||
var changeMoePlanCommand = new ChangeProviderPlanCommand(
|
||||
existingMoePlan.Id,
|
||||
model.Plan!.Value,
|
||||
provider.GatewaySubscriptionId);
|
||||
await _providerBillingService.ChangePlan(changeMoePlanCommand);
|
||||
|
||||
// 2. Update the seat minimums.
|
||||
var updateMoeSeatMinimumsCommand = new UpdateProviderSeatMinimumsCommand(
|
||||
provider.Id,
|
||||
provider.GatewaySubscriptionId,
|
||||
[
|
||||
(Plan: model.Plan!.Value, SeatsMinimum: model.EnterpriseMinimumSeats!.Value)
|
||||
]);
|
||||
await _providerBillingService.UpdateSeatMinimums(updateMoeSeatMinimumsCommand);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return RedirectToAction("Edit", new { id });
|
||||
|
@ -33,6 +33,13 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
||||
GatewayCustomerUrl = gatewayCustomerUrl;
|
||||
GatewaySubscriptionUrl = gatewaySubscriptionUrl;
|
||||
Type = provider.Type;
|
||||
|
||||
if (Type == ProviderType.MultiOrganizationEnterprise)
|
||||
{
|
||||
var plan = providerPlans.SingleOrDefault();
|
||||
EnterpriseMinimumSeats = plan?.SeatMinimum ?? 0;
|
||||
Plan = plan?.PlanType;
|
||||
}
|
||||
}
|
||||
|
||||
[Display(Name = "Billing Email")]
|
||||
@ -58,13 +65,24 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
||||
[Display(Name = "Provider Type")]
|
||||
public ProviderType Type { get; set; }
|
||||
|
||||
[Display(Name = "Plan")]
|
||||
public PlanType? Plan { get; set; }
|
||||
|
||||
[Display(Name = "Enterprise Seats Minimum")]
|
||||
public int? EnterpriseMinimumSeats { get; set; }
|
||||
|
||||
public virtual Provider ToProvider(Provider existingProvider)
|
||||
{
|
||||
existingProvider.BillingEmail = BillingEmail?.ToLowerInvariant().Trim();
|
||||
existingProvider.BillingPhone = BillingPhone?.ToLowerInvariant().Trim();
|
||||
existingProvider.Gateway = Gateway;
|
||||
existingProvider.GatewayCustomerId = GatewayCustomerId;
|
||||
existingProvider.GatewaySubscriptionId = GatewaySubscriptionId;
|
||||
switch (Type)
|
||||
{
|
||||
case ProviderType.Msp:
|
||||
existingProvider.Gateway = Gateway;
|
||||
existingProvider.GatewayCustomerId = GatewayCustomerId;
|
||||
existingProvider.GatewaySubscriptionId = GatewaySubscriptionId;
|
||||
break;
|
||||
}
|
||||
return existingProvider;
|
||||
}
|
||||
|
||||
@ -82,6 +100,23 @@ public class ProviderEditModel : ProviderViewModel, IValidatableObject
|
||||
yield return new ValidationResult($"The {billingEmailDisplayName} field is required.");
|
||||
}
|
||||
break;
|
||||
case ProviderType.MultiOrganizationEnterprise:
|
||||
if (Plan == null)
|
||||
{
|
||||
var displayName = nameof(Plan).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(Plan);
|
||||
yield return new ValidationResult($"The {displayName} field is required.");
|
||||
}
|
||||
if (EnterpriseMinimumSeats == null)
|
||||
{
|
||||
var displayName = nameof(EnterpriseMinimumSeats).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(EnterpriseMinimumSeats);
|
||||
yield return new ValidationResult($"The {displayName} field is required.");
|
||||
}
|
||||
if (EnterpriseMinimumSeats < 0)
|
||||
{
|
||||
var displayName = nameof(EnterpriseMinimumSeats).GetDisplayAttribute<CreateProviderModel>()?.GetName() ?? nameof(EnterpriseMinimumSeats);
|
||||
yield return new ValidationResult($"The {displayName} field cannot be less than 0.");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,9 @@
|
||||
@using Bit.Admin.Enums;
|
||||
@using Bit.Core
|
||||
@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
|
||||
|
||||
@ -47,60 +50,97 @@
|
||||
</div>
|
||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.EnableConsolidatedBilling) && Model.Provider.IsBillable())
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="TeamsMonthlySeatMinimum"></label>
|
||||
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
|
||||
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<div class="form-group">
|
||||
<label asp-for="Gateway"></label>
|
||||
<select class="form-control" asp-for="Gateway" asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
switch (Model.Provider.Type)
|
||||
{
|
||||
case ProviderType.Msp:
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="TeamsMonthlySeatMinimum"></label>
|
||||
<input type="number" class="form-control" asp-for="TeamsMonthlySeatMinimum">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="GatewayCustomerId"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewayCustomerId">
|
||||
<div class="input-group-append">
|
||||
<a href="@Model.GatewayCustomerUrl" class="btn btn-secondary" target="_blank">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</a>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="EnterpriseMonthlySeatMinimum"></label>
|
||||
<input type="number" class="form-control" asp-for="EnterpriseMonthlySeatMinimum">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="GatewaySubscriptionId"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewaySubscriptionId">
|
||||
<div class="input-group-append">
|
||||
<a href="@Model.GatewaySubscriptionUrl" class="btn btn-secondary" target="_blank">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</a>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<div class="form-group">
|
||||
<label asp-for="Gateway"></label>
|
||||
<select class="form-control" asp-for="Gateway" asp-items="Html.GetEnumSelectList<Bit.Core.Enums.GatewayType>()">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="GatewayCustomerId"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewayCustomerId">
|
||||
<div class="input-group-append">
|
||||
<a href="@Model.GatewayCustomerUrl" class="btn btn-secondary" target="_blank">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="GatewaySubscriptionId"></label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" asp-for="GatewaySubscriptionId">
|
||||
<div class="input-group-append">
|
||||
<a href="@Model.GatewaySubscriptionUrl" class="btn btn-secondary" target="_blank">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
}
|
||||
case ProviderType.MultiOrganizationEnterprise:
|
||||
{
|
||||
@if (FeatureService.IsEnabled(FeatureFlagKeys.PM12275_MultiOrganizationEnterprises) && Model.Provider.Type == ProviderType.MultiOrganizationEnterprise)
|
||||
{
|
||||
<div class="row">
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
@{
|
||||
var multiOrgPlans = new List<PlanType>
|
||||
{
|
||||
PlanType.EnterpriseAnnually,
|
||||
PlanType.EnterpriseMonthly
|
||||
};
|
||||
}
|
||||
<label asp-for="Plan"></label>
|
||||
<select class="form-control" asp-for="Plan" asp-items="Html.GetEnumSelectList(multiOrgPlans)">
|
||||
<option value="">--</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm">
|
||||
<div class="form-group">
|
||||
<label asp-for="EnterpriseMinimumSeats"></label>
|
||||
<input type="number" class="form-control" asp-for="EnterpriseMinimumSeats">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
</form>
|
||||
@await Html.PartialAsync("Organizations", Model)
|
||||
|
@ -4,6 +4,7 @@ using Bit.Admin.Enums;
|
||||
using Bit.Admin.Models;
|
||||
using Bit.Admin.Services;
|
||||
using Bit.Admin.Utilities;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
@ -24,6 +25,8 @@ public class UsersController : Controller
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IAccessControlService _accessControlService;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IUserService _userService;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public UsersController(
|
||||
IUserRepository userRepository,
|
||||
@ -31,7 +34,9 @@ public class UsersController : Controller
|
||||
IPaymentService paymentService,
|
||||
GlobalSettings globalSettings,
|
||||
IAccessControlService accessControlService,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IUserService userService,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_userRepository = userRepository;
|
||||
_cipherRepository = cipherRepository;
|
||||
@ -39,6 +44,8 @@ public class UsersController : Controller
|
||||
_globalSettings = globalSettings;
|
||||
_accessControlService = accessControlService;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_userService = userService;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
[RequirePermission(Permission.User_List_View)]
|
||||
@ -82,8 +89,8 @@ public class UsersController : Controller
|
||||
var ciphers = await _cipherRepository.GetManyByUserIdAsync(id);
|
||||
|
||||
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||
|
||||
return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers));
|
||||
var verifiedDomain = await AccountDeprovisioningEnabled(user.Id);
|
||||
return View(UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, verifiedDomain));
|
||||
}
|
||||
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
@ -99,7 +106,8 @@ public class UsersController : Controller
|
||||
var billingInfo = await _paymentService.GetBillingAsync(user);
|
||||
var billingHistoryInfo = await _paymentService.GetBillingHistoryAsync(user);
|
||||
var isTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(user);
|
||||
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings));
|
||||
var verifiedDomain = await AccountDeprovisioningEnabled(user.Id);
|
||||
return View(new UserEditModel(user, isTwoFactorEnabled, ciphers, billingInfo, billingHistoryInfo, _globalSettings, verifiedDomain));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@ -153,4 +161,12 @@ public class UsersController : Controller
|
||||
|
||||
return RedirectToAction("Index");
|
||||
}
|
||||
|
||||
// TODO: Feature flag to be removed in PM-14207
|
||||
private async Task<bool?> AccountDeprovisioningEnabled(Guid userId)
|
||||
{
|
||||
return _featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
? await _userService.IsManagedByAnyOrganizationAsync(userId)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
@ -20,9 +20,11 @@ public class UserEditModel
|
||||
IEnumerable<Cipher> ciphers,
|
||||
BillingInfo billingInfo,
|
||||
BillingHistoryInfo billingHistoryInfo,
|
||||
GlobalSettings globalSettings)
|
||||
GlobalSettings globalSettings,
|
||||
bool? domainVerified
|
||||
)
|
||||
{
|
||||
User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers);
|
||||
User = UserViewModel.MapViewModel(user, isTwoFactorEnabled, ciphers, domainVerified);
|
||||
|
||||
BillingInfo = billingInfo;
|
||||
BillingHistoryInfo = billingHistoryInfo;
|
||||
|
@ -14,6 +14,7 @@ public class UserViewModel
|
||||
public bool Premium { get; }
|
||||
public short? MaxStorageGb { get; }
|
||||
public bool EmailVerified { get; }
|
||||
public bool? DomainVerified { get; }
|
||||
public bool TwoFactorEnabled { get; }
|
||||
public DateTime AccountRevisionDate { get; }
|
||||
public DateTime RevisionDate { get; }
|
||||
@ -35,6 +36,7 @@ public class UserViewModel
|
||||
bool premium,
|
||||
short? maxStorageGb,
|
||||
bool emailVerified,
|
||||
bool? domainVerified,
|
||||
bool twoFactorEnabled,
|
||||
DateTime accountRevisionDate,
|
||||
DateTime revisionDate,
|
||||
@ -56,6 +58,7 @@ public class UserViewModel
|
||||
Premium = premium;
|
||||
MaxStorageGb = maxStorageGb;
|
||||
EmailVerified = emailVerified;
|
||||
DomainVerified = domainVerified;
|
||||
TwoFactorEnabled = twoFactorEnabled;
|
||||
AccountRevisionDate = accountRevisionDate;
|
||||
RevisionDate = revisionDate;
|
||||
@ -73,10 +76,10 @@ public class UserViewModel
|
||||
public static IEnumerable<UserViewModel> MapViewModels(
|
||||
IEnumerable<User> users,
|
||||
IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup) =>
|
||||
users.Select(user => MapViewModel(user, lookup));
|
||||
users.Select(user => MapViewModel(user, lookup, false));
|
||||
|
||||
public static UserViewModel MapViewModel(User user,
|
||||
IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup) =>
|
||||
IEnumerable<(Guid userId, bool twoFactorIsEnabled)> lookup, bool? domainVerified) =>
|
||||
new(
|
||||
user.Id,
|
||||
user.Name,
|
||||
@ -86,6 +89,7 @@ public class UserViewModel
|
||||
user.Premium,
|
||||
user.MaxStorageGb,
|
||||
user.EmailVerified,
|
||||
domainVerified,
|
||||
IsTwoFactorEnabled(user, lookup),
|
||||
user.AccountRevisionDate,
|
||||
user.RevisionDate,
|
||||
@ -100,9 +104,9 @@ public class UserViewModel
|
||||
Array.Empty<Cipher>());
|
||||
|
||||
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled) =>
|
||||
MapViewModel(user, isTwoFactorEnabled, Array.Empty<Cipher>());
|
||||
MapViewModel(user, isTwoFactorEnabled, Array.Empty<Cipher>(), false);
|
||||
|
||||
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable<Cipher> ciphers) =>
|
||||
public static UserViewModel MapViewModel(User user, bool isTwoFactorEnabled, IEnumerable<Cipher> ciphers, bool? domainVerified) =>
|
||||
new(
|
||||
user.Id,
|
||||
user.Name,
|
||||
@ -112,6 +116,7 @@ public class UserViewModel
|
||||
user.Premium,
|
||||
user.MaxStorageGb,
|
||||
user.EmailVerified,
|
||||
domainVerified,
|
||||
isTwoFactorEnabled,
|
||||
user.AccountRevisionDate,
|
||||
user.RevisionDate,
|
||||
|
@ -110,6 +110,7 @@ public static class RolePermissionMapping
|
||||
Permission.User_Licensing_View,
|
||||
Permission.User_Billing_View,
|
||||
Permission.User_Billing_LaunchGateway,
|
||||
Permission.User_Delete,
|
||||
Permission.Org_List_View,
|
||||
Permission.Org_OrgInformation_View,
|
||||
Permission.Org_GeneralDetails_View,
|
||||
|
@ -1,4 +1,4 @@
|
||||
@model UserViewModel
|
||||
@model UserViewModel
|
||||
<dl class="row">
|
||||
<dt class="col-sm-4 col-lg-3">Id</dt>
|
||||
<dd class="col-sm-8 col-lg-9"><code>@Model.Id</code></dd>
|
||||
@ -12,6 +12,11 @@
|
||||
<dt class="col-sm-4 col-lg-3">Email Verified</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@(Model.EmailVerified ? "Yes" : "No")</dd>
|
||||
|
||||
@if(Model.DomainVerified.HasValue){
|
||||
<dt class="col-sm-4 col-lg-3">Domain Verified</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@(Model.DomainVerified.Value == true ? "Yes" : "No")</dd>
|
||||
}
|
||||
|
||||
<dt class="col-sm-4 col-lg-3">Using 2FA</dt>
|
||||
<dd class="col-sm-8 col-lg-9">@(Model.TwoFactorEnabled ? "Yes" : "No")</dd>
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.AdminConsole.Models.Response.Organizations;
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Api.Models.Request.Organizations;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
@ -545,7 +544,7 @@ public class OrganizationUsersController : Controller
|
||||
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
||||
[HttpDelete("{id}/delete-account")]
|
||||
[HttpPost("{id}/delete-account")]
|
||||
public async Task DeleteAccount(Guid orgId, Guid id, [FromBody] SecretVerificationRequestModel model)
|
||||
public async Task DeleteAccount(Guid orgId, Guid id)
|
||||
{
|
||||
if (!await _currentContext.ManageUsers(orgId))
|
||||
{
|
||||
@ -558,19 +557,13 @@ public class OrganizationUsersController : Controller
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
if (!await _userService.VerifySecretAsync(currentUser, model.Secret))
|
||||
{
|
||||
await Task.Delay(2000);
|
||||
throw new BadRequestException(string.Empty, "User verification failed.");
|
||||
}
|
||||
|
||||
await _deleteManagedOrganizationUserAccountCommand.DeleteUserAsync(orgId, id, currentUser.Id);
|
||||
}
|
||||
|
||||
[RequireFeature(FeatureFlagKeys.AccountDeprovisioning)]
|
||||
[HttpDelete("delete-account")]
|
||||
[HttpPost("delete-account")]
|
||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] SecureOrganizationUserBulkRequestModel model)
|
||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkDeleteAccount(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||
{
|
||||
if (!await _currentContext.ManageUsers(orgId))
|
||||
{
|
||||
@ -583,12 +576,6 @@ public class OrganizationUsersController : Controller
|
||||
throw new UnauthorizedAccessException();
|
||||
}
|
||||
|
||||
if (!await _userService.VerifySecretAsync(currentUser, model.Secret))
|
||||
{
|
||||
await Task.Delay(2000);
|
||||
throw new BadRequestException(string.Empty, "User verification failed.");
|
||||
}
|
||||
|
||||
var results = await _deleteManagedOrganizationUserAccountCommand.DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id);
|
||||
|
||||
return new ListResponseModel<OrganizationUserBulkResponseModel>(results.Select(r =>
|
||||
|
@ -16,6 +16,7 @@ using Bit.Core.Utilities;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.DataProtection;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using AdminConsoleEntities = Bit.Core.AdminConsole.Entities;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Controllers;
|
||||
|
||||
@ -55,17 +56,16 @@ public class PoliciesController : Controller
|
||||
}
|
||||
|
||||
[HttpGet("{type}")]
|
||||
public async Task<PolicyResponseModel> Get(string orgId, int type)
|
||||
public async Task<PolicyResponseModel> Get(Guid orgId, int type)
|
||||
{
|
||||
var orgIdGuid = new Guid(orgId);
|
||||
if (!await _currentContext.ManagePolicies(orgIdGuid))
|
||||
if (!await _currentContext.ManagePolicies(orgId))
|
||||
{
|
||||
throw new NotFoundException();
|
||||
}
|
||||
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(orgIdGuid, (PolicyType)type);
|
||||
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(orgId, (PolicyType)type);
|
||||
if (policy == null)
|
||||
{
|
||||
throw new NotFoundException();
|
||||
return new PolicyResponseModel(new AdminConsoleEntities.Policy() { Type = (PolicyType)type, Enabled = false });
|
||||
}
|
||||
|
||||
return new PolicyResponseModel(policy);
|
||||
|
@ -1,10 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
|
||||
namespace Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
|
||||
public class SecureOrganizationUserBulkRequestModel : SecretVerificationRequestModel
|
||||
{
|
||||
[Required]
|
||||
public IEnumerable<Guid> Ids { get; set; }
|
||||
}
|
@ -35,7 +35,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="6.8.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.9.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -148,6 +148,13 @@ public class AccountsController : Controller
|
||||
throw new BadRequestException("MasterPasswordHash", "Invalid password.");
|
||||
}
|
||||
|
||||
// If Account Deprovisioning is enabled, we need to check if the user is managed by any organization.
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& await _userService.IsManagedByAnyOrganizationAsync(user.Id))
|
||||
{
|
||||
throw new BadRequestException("Cannot change emails for accounts owned by an organization. Contact your organization administrator for additional details.");
|
||||
}
|
||||
|
||||
await _userService.InitiateEmailChangeAsync(user, model.NewEmail);
|
||||
}
|
||||
|
||||
@ -165,6 +172,13 @@ public class AccountsController : Controller
|
||||
throw new BadRequestException("You cannot change your email when using Key Connector.");
|
||||
}
|
||||
|
||||
// If Account Deprovisioning is enabled, we need to check if the user is managed by any organization.
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& await _userService.IsManagedByAnyOrganizationAsync(user.Id))
|
||||
{
|
||||
throw new BadRequestException("Cannot change emails for accounts owned by an organization. Contact your organization administrator for additional details.");
|
||||
}
|
||||
|
||||
var result = await _userService.ChangeEmailAsync(user, model.MasterPasswordHash, model.NewEmail,
|
||||
model.NewMasterPasswordHash, model.Token, model.Key);
|
||||
if (result.Succeeded)
|
||||
@ -566,6 +580,13 @@ public class AccountsController : Controller
|
||||
}
|
||||
else
|
||||
{
|
||||
// If Account Deprovisioning is enabled, we need to check if the user is managed by any organization.
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
|
||||
&& await _userService.IsManagedByAnyOrganizationAsync(user.Id))
|
||||
{
|
||||
throw new BadRequestException("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details.");
|
||||
}
|
||||
|
||||
var result = await _userService.DeleteAsync(user);
|
||||
if (result.Succeeded)
|
||||
{
|
||||
|
@ -26,7 +26,7 @@ public class OrganizationBillingController(
|
||||
[HttpGet("metadata")]
|
||||
public async Task<IResult> GetMetadataAsync([FromRoute] Guid organizationId)
|
||||
{
|
||||
if (!await currentContext.AccessMembersTab(organizationId))
|
||||
if (!await currentContext.OrganizationUser(organizationId))
|
||||
{
|
||||
return Error.Unauthorized();
|
||||
}
|
||||
|
@ -4,10 +4,14 @@ namespace Bit.Api.Billing.Models.Responses;
|
||||
|
||||
public record OrganizationMetadataResponse(
|
||||
bool IsEligibleForSelfHost,
|
||||
bool IsOnSecretsManagerStandalone)
|
||||
bool IsManaged,
|
||||
bool IsOnSecretsManagerStandalone,
|
||||
bool IsSubscriptionUnpaid)
|
||||
{
|
||||
public static OrganizationMetadataResponse From(OrganizationMetadata metadata)
|
||||
=> new(
|
||||
metadata.IsEligibleForSelfHost,
|
||||
metadata.IsOnSecretsManagerStandalone);
|
||||
metadata.IsManaged,
|
||||
metadata.IsOnSecretsManagerStandalone,
|
||||
metadata.IsSubscriptionUnpaid);
|
||||
}
|
||||
|
@ -196,8 +196,8 @@ public class DevicesController : Controller
|
||||
}
|
||||
|
||||
[HttpDelete("{id}")]
|
||||
[HttpPost("{id}/delete")]
|
||||
public async Task Delete(string id)
|
||||
[HttpPost("{id}/deactivate")]
|
||||
public async Task Deactivate(string id)
|
||||
{
|
||||
var device = await _deviceRepository.GetByIdAsync(new Guid(id), _userService.GetProperUserId(User).Value);
|
||||
if (device == null)
|
||||
@ -205,7 +205,7 @@ public class DevicesController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
await _deviceService.DeleteAsync(device);
|
||||
await _deviceService.DeactivateAsync(device);
|
||||
}
|
||||
|
||||
[AllowAnonymous]
|
||||
|
@ -0,0 +1,7 @@
|
||||
using Bit.Core.Models.Mail;
|
||||
|
||||
namespace Bit.Core.Auth.Models.Mail;
|
||||
|
||||
public class CannotDeleteManagedAccountViewModel : BaseMailModel
|
||||
{
|
||||
}
|
@ -11,11 +11,10 @@ namespace Bit.Core.Billing.Extensions;
|
||||
public static class BillingExtensions
|
||||
{
|
||||
public static bool IsBillable(this Provider provider) =>
|
||||
provider is
|
||||
{
|
||||
Type: ProviderType.Msp,
|
||||
Status: ProviderStatusType.Billable
|
||||
};
|
||||
provider.SupportsConsolidatedBilling() && provider.Status == ProviderStatusType.Billable;
|
||||
|
||||
public static bool SupportsConsolidatedBilling(this Provider provider)
|
||||
=> provider.Type is ProviderType.Msp or ProviderType.MultiOrganizationEnterprise;
|
||||
|
||||
public static bool IsValidClient(this Organization organization)
|
||||
=> organization is
|
||||
|
@ -8,6 +8,7 @@ using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Billing.Migration.Models;
|
||||
using Bit.Core.Billing.Repositories;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Billing.Services.Contracts;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@ -307,7 +308,14 @@ public class ProviderMigrator(
|
||||
.FirstOrDefault(providerPlan => providerPlan.PlanType == PlanType.TeamsMonthly)?
|
||||
.SeatMinimum ?? 0;
|
||||
|
||||
await providerBillingService.UpdateSeatMinimums(provider, enterpriseSeatMinimum, teamsSeatMinimum);
|
||||
var updateSeatMinimumsCommand = new UpdateProviderSeatMinimumsCommand(
|
||||
provider.Id,
|
||||
provider.GatewaySubscriptionId,
|
||||
[
|
||||
(Plan: PlanType.EnterpriseMonthly, SeatsMinimum: enterpriseSeatMinimum),
|
||||
(Plan: PlanType.TeamsMonthly, SeatsMinimum: teamsSeatMinimum)
|
||||
]);
|
||||
await providerBillingService.UpdateSeatMinimums(updateSeatMinimumsCommand);
|
||||
|
||||
logger.LogInformation(
|
||||
"CB: Updated Stripe subscription for provider ({ProviderID}) with current seat minimums", provider.Id);
|
||||
@ -325,13 +333,16 @@ public class ProviderMigrator(
|
||||
|
||||
var organizationCancellationCredit = organizationCustomers.Sum(customer => customer.Balance);
|
||||
|
||||
await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId,
|
||||
new CustomerBalanceTransactionCreateOptions
|
||||
{
|
||||
Amount = organizationCancellationCredit,
|
||||
Currency = "USD",
|
||||
Description = "Unused, prorated time for client organization subscriptions."
|
||||
});
|
||||
if (organizationCancellationCredit != 0)
|
||||
{
|
||||
await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId,
|
||||
new CustomerBalanceTransactionCreateOptions
|
||||
{
|
||||
Amount = organizationCancellationCredit,
|
||||
Currency = "USD",
|
||||
Description = "Unused, prorated time for client organization subscriptions."
|
||||
});
|
||||
}
|
||||
|
||||
var migrationRecords = await Task.WhenAll(organizations.Select(organization =>
|
||||
clientOrganizationMigrationRecordRepository.GetByOrganizationId(organization.Id)));
|
||||
|
@ -2,9 +2,6 @@
|
||||
|
||||
public record OrganizationMetadata(
|
||||
bool IsEligibleForSelfHost,
|
||||
bool IsOnSecretsManagerStandalone)
|
||||
{
|
||||
public static OrganizationMetadata Default() => new(
|
||||
IsEligibleForSelfHost: false,
|
||||
IsOnSecretsManagerStandalone: false);
|
||||
}
|
||||
bool IsManaged,
|
||||
bool IsOnSecretsManagerStandalone,
|
||||
bool IsSubscriptionUnpaid);
|
||||
|
@ -24,6 +24,7 @@ public record TeamsPlan : Plan
|
||||
Has2fa = true;
|
||||
HasApi = true;
|
||||
UsersGetPremium = true;
|
||||
HasScim = true;
|
||||
|
||||
UpgradeSortOrder = 3;
|
||||
DisplaySortOrder = 3;
|
||||
|
@ -0,0 +1,8 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
|
||||
namespace Bit.Core.Billing.Services.Contracts;
|
||||
|
||||
public record ChangeProviderPlanCommand(
|
||||
Guid ProviderPlanId,
|
||||
PlanType NewPlan,
|
||||
string GatewaySubscriptionId);
|
@ -0,0 +1,10 @@
|
||||
using Bit.Core.Billing.Enums;
|
||||
|
||||
namespace Bit.Core.Billing.Services.Contracts;
|
||||
|
||||
/// <param name="Id">The ID of the provider to update the seat minimums for.</param>
|
||||
/// <param name="Configuration">The new seat minimums for the provider.</param>
|
||||
public record UpdateProviderSeatMinimumsCommand(
|
||||
Guid Id,
|
||||
string GatewaySubscriptionId,
|
||||
IReadOnlyCollection<(PlanType Plan, int SeatsMinimum)> Configuration);
|
@ -3,6 +3,7 @@ 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.Services.Contracts;
|
||||
using Bit.Core.Models.Business;
|
||||
using Stripe;
|
||||
|
||||
@ -89,8 +90,12 @@ public interface IProviderBillingService
|
||||
Task<Subscription> SetupSubscription(
|
||||
Provider provider);
|
||||
|
||||
Task UpdateSeatMinimums(
|
||||
Provider provider,
|
||||
int enterpriseSeatMinimum,
|
||||
int teamsSeatMinimum);
|
||||
/// <summary>
|
||||
/// Changes the assigned provider plan for the provider.
|
||||
/// </summary>
|
||||
/// <param name="command">The command to change the provider plan.</param>
|
||||
/// <returns></returns>
|
||||
Task ChangePlan(ChangeProviderPlanCommand command);
|
||||
|
||||
Task UpdateSeatMinimums(UpdateProviderSeatMinimumsCommand command);
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Models;
|
||||
@ -27,7 +26,6 @@ public class OrganizationBillingService(
|
||||
IGlobalSettings globalSettings,
|
||||
ILogger<OrganizationBillingService> logger,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IProviderRepository providerRepository,
|
||||
ISetupIntentCache setupIntentCache,
|
||||
IStripeAdapter stripeAdapter,
|
||||
ISubscriberService subscriberService) : IOrganizationBillingService
|
||||
@ -64,18 +62,18 @@ public class OrganizationBillingService(
|
||||
return null;
|
||||
}
|
||||
|
||||
var customer = await subscriberService.GetCustomer(organization, new CustomerGetOptions
|
||||
{
|
||||
Expand = ["discount.coupon.applies_to"]
|
||||
});
|
||||
var customer = await subscriberService.GetCustomer(organization,
|
||||
new CustomerGetOptions { Expand = ["discount.coupon.applies_to"] });
|
||||
|
||||
var subscription = await subscriberService.GetSubscription(organization);
|
||||
|
||||
var isEligibleForSelfHost = await IsEligibleForSelfHost(organization, subscription);
|
||||
|
||||
var isEligibleForSelfHost = IsEligibleForSelfHost(organization);
|
||||
var isManaged = organization.Status == OrganizationStatusType.Managed;
|
||||
var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription);
|
||||
var isSubscriptionUnpaid = IsSubscriptionUnpaid(subscription);
|
||||
|
||||
return new OrganizationMetadata(isEligibleForSelfHost, isOnSecretsManagerStandalone);
|
||||
return new OrganizationMetadata(isEligibleForSelfHost, isManaged, isOnSecretsManagerStandalone,
|
||||
isSubscriptionUnpaid);
|
||||
}
|
||||
|
||||
public async Task UpdatePaymentMethod(
|
||||
@ -339,26 +337,12 @@ public class OrganizationBillingService(
|
||||
return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||
}
|
||||
|
||||
private async Task<bool> IsEligibleForSelfHost(
|
||||
Organization organization,
|
||||
Subscription? organizationSubscription)
|
||||
private static bool IsEligibleForSelfHost(
|
||||
Organization organization)
|
||||
{
|
||||
if (organization.Status != OrganizationStatusType.Managed)
|
||||
{
|
||||
return organization.Plan.Contains("Families") ||
|
||||
organization.Plan.Contains("Enterprise") && IsActive(organizationSubscription);
|
||||
}
|
||||
var eligibleSelfHostPlans = StaticStore.Plans.Where(plan => plan.HasSelfHost).Select(plan => plan.Type);
|
||||
|
||||
var provider = await providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||
|
||||
var providerSubscription = await subscriberService.GetSubscriptionOrThrow(provider);
|
||||
|
||||
return organization.Plan.Contains("Enterprise") && IsActive(providerSubscription);
|
||||
|
||||
bool IsActive(Subscription? subscription) => subscription?.Status is
|
||||
StripeConstants.SubscriptionStatus.Active or
|
||||
StripeConstants.SubscriptionStatus.Trialing or
|
||||
StripeConstants.SubscriptionStatus.PastDue;
|
||||
return eligibleSelfHostPlans.Contains(organization.PlanType);
|
||||
}
|
||||
|
||||
private static bool IsOnSecretsManagerStandalone(
|
||||
@ -392,5 +376,16 @@ public class OrganizationBillingService(
|
||||
return subscriptionProductIds.Intersect(couponAppliesTo ?? []).Any();
|
||||
}
|
||||
|
||||
private static bool IsSubscriptionUnpaid(Subscription subscription)
|
||||
{
|
||||
if (subscription == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return subscription.Status == "unpaid";
|
||||
}
|
||||
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
@ -106,7 +106,6 @@ public static class FeatureFlagKeys
|
||||
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
|
||||
public const string ItemShare = "item-share";
|
||||
public const string DuoRedirect = "duo-redirect";
|
||||
public const string PM5864DollarThreshold = "PM-5864-dollar-threshold";
|
||||
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
||||
public const string EnableConsolidatedBilling = "enable-consolidated-billing";
|
||||
public const string AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section";
|
||||
|
@ -25,7 +25,7 @@
|
||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.40" />
|
||||
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
|
||||
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
|
||||
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.18.1" />
|
||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.21.2" />
|
||||
<PackageReference Include="Azure.Storage.Queues" Version="12.19.1" />
|
||||
@ -35,22 +35,22 @@
|
||||
<PackageReference Include="Fido2.AspNet" Version="3.0.1" />
|
||||
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
|
||||
<PackageReference Include="MailKit" Version="4.8.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.45.0" />
|
||||
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" />
|
||||
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.6.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.SqlServer" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.UserSecrets" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="8.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="8.0.10" />
|
||||
<PackageReference Include="Quartz" Version="3.9.0" />
|
||||
<PackageReference Include="SendGrid" Version="9.29.3" />
|
||||
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Serilog.Extensions.Logging.File" Version="3.0.0" />
|
||||
<PackageReference Include="Sentry.Serilog" Version="3.41.4" />
|
||||
<PackageReference Include="Duende.IdentityServer" Version="7.0.6" />
|
||||
<PackageReference Include="Duende.IdentityServer" Version="7.0.8" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="4.0.0" />
|
||||
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
|
||||
@ -58,7 +58,7 @@
|
||||
<PackageReference Include="Stripe.net" Version="45.14.0" />
|
||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.10" />
|
||||
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
@ -38,6 +38,10 @@ public class Device : ITableObject<Guid>
|
||||
/// </summary>
|
||||
public string? EncryptedPrivateKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether the device is active for the user.
|
||||
/// </summary>
|
||||
public bool Active { get; set; } = true;
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
|
@ -0,0 +1,15 @@
|
||||
{{#>FullHtmlLayout}}
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0 0 10px; -webkit-text-size-adjust: none; text-align: left;" valign="top" align="center">
|
||||
You have requested to delete your account. This action cannot be completed because your account is owned by an organization.
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||
<td class="content-block last" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; color: #333; line-height: 25px; margin: 0; -webkit-font-smoothing: antialiased; padding: 0; -webkit-text-size-adjust: none; text-align: left;" valign="top" align="center">
|
||||
Please contact your organization administrator for additional details.
|
||||
<br style="margin: 0; box-sizing: border-box; color: #333; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;" />
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
{{/FullHtmlLayout}}
|
@ -0,0 +1,6 @@
|
||||
{{#>BasicTextLayout}}
|
||||
You have requested to delete your account. This action cannot be completed because your account is owned by an organization.
|
||||
|
||||
Please contact your organization administrator for additional details.
|
||||
|
||||
{{/BasicTextLayout}}
|
@ -7,7 +7,7 @@ public interface IDeviceService
|
||||
{
|
||||
Task SaveAsync(Device device);
|
||||
Task ClearTokenAsync(Device device);
|
||||
Task DeleteAsync(Device device);
|
||||
Task DeactivateAsync(Device device);
|
||||
Task UpdateDevicesTrustAsync(string currentDeviceIdentifier,
|
||||
Guid currentUserId,
|
||||
DeviceKeysUpdateRequestModel currentDeviceUpdate,
|
||||
|
@ -18,6 +18,7 @@ public interface IMailService
|
||||
ProductTierType productTier,
|
||||
IEnumerable<ProductType> products);
|
||||
Task SendVerifyDeleteEmailAsync(string email, Guid userId, string token);
|
||||
Task SendCannotDeleteManagedAccountEmailAsync(string email);
|
||||
Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail);
|
||||
Task SendChangeEmailEmailAsync(string newEmailAddress, string token);
|
||||
Task SendTwoFactorEmailAsync(string email, string token);
|
||||
|
@ -14,6 +14,17 @@ public interface IStripeAdapter
|
||||
CustomerBalanceTransactionCreateOptions options);
|
||||
Task<Stripe.Subscription> SubscriptionCreateAsync(Stripe.SubscriptionCreateOptions subscriptionCreateOptions);
|
||||
Task<Stripe.Subscription> SubscriptionGetAsync(string id, Stripe.SubscriptionGetOptions options = null);
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves a subscription object for a provider.
|
||||
/// </summary>
|
||||
/// <param name="id">The subscription ID.</param>
|
||||
/// <param name="providerId">The provider ID.</param>
|
||||
/// <param name="options">Additional options.</param>
|
||||
/// <returns>The subscription object.</returns>
|
||||
/// <exception cref="InvalidOperationException">Thrown when the subscription doesn't belong to the provider.</exception>
|
||||
Task<Stripe.Subscription> ProviderSubscriptionGetAsync(string id, Guid providerId, Stripe.SubscriptionGetOptions options = null);
|
||||
|
||||
Task<List<Stripe.Subscription>> SubscriptionListAsync(StripeSubscriptionListOptions subscriptionSearchOptions);
|
||||
Task<Stripe.Subscription> SubscriptionUpdateAsync(string id, Stripe.SubscriptionUpdateOptions options = null);
|
||||
Task<Stripe.Subscription> SubscriptionCancelAsync(string Id, Stripe.SubscriptionCancelOptions options = null);
|
||||
|
@ -41,9 +41,18 @@ public class DeviceService : IDeviceService
|
||||
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
|
||||
}
|
||||
|
||||
public async Task DeleteAsync(Device device)
|
||||
public async Task DeactivateAsync(Device device)
|
||||
{
|
||||
await _deviceRepository.DeleteAsync(device);
|
||||
// already deactivated
|
||||
if (!device.Active)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
device.Active = false;
|
||||
device.RevisionDate = DateTime.UtcNow;
|
||||
await _deviceRepository.UpsertAsync(device);
|
||||
|
||||
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
|
||||
}
|
||||
|
||||
|
@ -112,6 +112,19 @@ public class HandlebarsMailService : IMailService
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendCannotDeleteManagedAccountEmailAsync(string email)
|
||||
{
|
||||
var message = CreateDefaultMessage("Delete Your Account", email);
|
||||
var model = new CannotDeleteManagedAccountViewModel
|
||||
{
|
||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||
SiteName = _globalSettings.SiteName,
|
||||
};
|
||||
await AddMessageContentAsync(message, "AdminConsole.CannotDeleteManagedAccount", model);
|
||||
message.Category = "CannotDeleteManagedAccount";
|
||||
await _mailDeliveryService.SendEmailAsync(message);
|
||||
}
|
||||
|
||||
public async Task SendChangeEmailAlreadyExistsEmailAsync(string fromEmail, string toEmail)
|
||||
{
|
||||
var message = CreateDefaultMessage("Your Email Change", toEmail);
|
||||
|
@ -79,6 +79,20 @@ public class StripeAdapter : IStripeAdapter
|
||||
return _subscriptionService.GetAsync(id, options);
|
||||
}
|
||||
|
||||
public async Task<Subscription> ProviderSubscriptionGetAsync(
|
||||
string id,
|
||||
Guid providerId,
|
||||
SubscriptionGetOptions options = null)
|
||||
{
|
||||
var subscription = await _subscriptionService.GetAsync(id, options);
|
||||
if (subscription.Metadata.TryGetValue("providerId", out var value) && value == providerId.ToString())
|
||||
{
|
||||
return subscription;
|
||||
}
|
||||
|
||||
throw new InvalidOperationException("Subscription does not belong to the provider.");
|
||||
}
|
||||
|
||||
public Task<Stripe.Subscription> SubscriptionUpdateAsync(string id,
|
||||
Stripe.SubscriptionUpdateOptions options = null)
|
||||
{
|
||||
|
@ -792,19 +792,16 @@ public class StripePaymentService : IPaymentService
|
||||
var daysUntilDue = sub.DaysUntilDue;
|
||||
var chargeNow = collectionMethod == "charge_automatically";
|
||||
var updatedItemOptions = subscriptionUpdate.UpgradeItemsOptions(sub);
|
||||
var isPm5864DollarThresholdEnabled = _featureService.IsEnabled(FeatureFlagKeys.PM5864DollarThreshold);
|
||||
var isAnnualPlan = sub?.Items?.Data.FirstOrDefault()?.Plan?.Interval == "year";
|
||||
|
||||
var subUpdateOptions = new SubscriptionUpdateOptions
|
||||
{
|
||||
Items = updatedItemOptions,
|
||||
ProrationBehavior = !isPm5864DollarThresholdEnabled || invoiceNow
|
||||
? Constants.AlwaysInvoice
|
||||
: Constants.CreateProrations,
|
||||
ProrationBehavior = invoiceNow ? Constants.AlwaysInvoice : Constants.CreateProrations,
|
||||
DaysUntilDue = daysUntilDue ?? 1,
|
||||
CollectionMethod = "send_invoice"
|
||||
};
|
||||
if (!invoiceNow && isAnnualPlan && isPm5864DollarThresholdEnabled && sub.Status.Trim() != "trialing")
|
||||
if (!invoiceNow && isAnnualPlan && sub.Status.Trim() != "trialing")
|
||||
{
|
||||
subUpdateOptions.PendingInvoiceItemInterval =
|
||||
new SubscriptionPendingInvoiceItemIntervalOptions { Interval = "month" };
|
||||
@ -838,7 +835,7 @@ public class StripePaymentService : IPaymentService
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!isPm5864DollarThresholdEnabled && !invoiceNow)
|
||||
if (invoiceNow)
|
||||
{
|
||||
if (chargeNow)
|
||||
{
|
||||
|
@ -297,6 +297,12 @@ public class UserService : UserManager<User>, IUserService, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
if (await IsManagedByAnyOrganizationAsync(user.Id))
|
||||
{
|
||||
await _mailService.SendCannotDeleteManagedAccountEmailAsync(user.Email);
|
||||
return;
|
||||
}
|
||||
|
||||
var token = await base.GenerateUserTokenAsync(user, TokenOptions.DefaultProvider, "DeleteAccount");
|
||||
await _mailService.SendVerifyDeleteEmailAsync(user.Email, user.Id, token);
|
||||
}
|
||||
|
@ -94,6 +94,11 @@ public class NoopMailService : IMailService
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendCannotDeleteManagedAccountEmailAsync(string email)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task SendPasswordlessSignInAsync(string returnUrl, string token, string email)
|
||||
{
|
||||
return Task.FromResult(0);
|
||||
|
16
src/Core/Tools/Entities/PasswordHealthReportApplication.cs
Normal file
16
src/Core/Tools/Entities/PasswordHealthReportApplication.cs
Normal file
@ -0,0 +1,16 @@
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Utilities;
|
||||
|
||||
public class PasswordHealthReportApplication : ITableObject<Guid>, IRevisable
|
||||
{
|
||||
public Guid Id { get; set; }
|
||||
public Guid OrganizationId { get; set; }
|
||||
public string Uri { get; set; }
|
||||
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
|
||||
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
|
||||
|
||||
public void SetNewId()
|
||||
{
|
||||
Id = CoreHelpers.GenerateComb();
|
||||
}
|
||||
}
|
@ -21,6 +21,10 @@ public class DeviceEntityTypeConfiguration : IEntityTypeConfiguration<Device>
|
||||
.HasIndex(d => d.Identifier)
|
||||
.IsClustered(false);
|
||||
|
||||
builder.Property(c => c.Active)
|
||||
.ValueGeneratedNever()
|
||||
.HasDefaultValue(true);
|
||||
|
||||
builder.ToTable(nameof(Device));
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.8.1" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.9.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -9,7 +9,8 @@
|
||||
@RevisionDate DATETIME2(7),
|
||||
@EncryptedUserKey VARCHAR(MAX) = NULL,
|
||||
@EncryptedPublicKey VARCHAR(MAX) = NULL,
|
||||
@EncryptedPrivateKey VARCHAR(MAX) = NULL
|
||||
@EncryptedPrivateKey VARCHAR(MAX) = NULL,
|
||||
@Active BIT = 1
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@ -26,7 +27,8 @@ BEGIN
|
||||
[RevisionDate],
|
||||
[EncryptedUserKey],
|
||||
[EncryptedPublicKey],
|
||||
[EncryptedPrivateKey]
|
||||
[EncryptedPrivateKey],
|
||||
[Active]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@ -40,6 +42,7 @@ BEGIN
|
||||
@RevisionDate,
|
||||
@EncryptedUserKey,
|
||||
@EncryptedPublicKey,
|
||||
@EncryptedPrivateKey
|
||||
@EncryptedPrivateKey,
|
||||
@Active
|
||||
)
|
||||
END
|
||||
|
@ -1,12 +0,0 @@
|
||||
CREATE PROCEDURE [dbo].[Device_DeleteById]
|
||||
@Id UNIQUEIDENTIFIER
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
DELETE
|
||||
FROM
|
||||
[dbo].[Device]
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
@ -9,7 +9,8 @@
|
||||
@RevisionDate DATETIME2(7),
|
||||
@EncryptedUserKey VARCHAR(MAX) = NULL,
|
||||
@EncryptedPublicKey VARCHAR(MAX) = NULL,
|
||||
@EncryptedPrivateKey VARCHAR(MAX) = NULL
|
||||
@EncryptedPrivateKey VARCHAR(MAX) = NULL,
|
||||
@Active BIT = 1
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
@ -26,7 +27,8 @@ BEGIN
|
||||
[RevisionDate] = @RevisionDate,
|
||||
[EncryptedUserKey] = @EncryptedUserKey,
|
||||
[EncryptedPublicKey] = @EncryptedPublicKey,
|
||||
[EncryptedPrivateKey] = @EncryptedPrivateKey
|
||||
[EncryptedPrivateKey] = @EncryptedPrivateKey,
|
||||
[Active] = @Active
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
||||
|
@ -0,0 +1,10 @@
|
||||
CREATE PROCEDURE dbo.PasswordHealthReportApplication_Create
|
||||
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@Uri nvarchar(max),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7)
|
||||
AS
|
||||
SET NOCOUNT ON;
|
||||
INSERT INTO dbo.PasswordHealthReportApplication ( Id, OrganizationId, Uri, CreationDate, RevisionDate )
|
||||
VALUES ( @Id, @OrganizationId, @Uri, @CreationDate, @RevisionDate )
|
@ -0,0 +1,10 @@
|
||||
CREATE PROCEDURE dbo.PasswordHealthReportApplication_DeleteById
|
||||
@Id UNIQUEIDENTIFIER
|
||||
AS
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF @Id IS NULL
|
||||
THROW 50000, 'Id cannot be null', 1;
|
||||
|
||||
DELETE FROM [dbo].[PasswordHealthReportApplication]
|
||||
WHERE [Id] = @Id
|
@ -0,0 +1,16 @@
|
||||
CREATE PROCEDURE dbo.PasswordHealthReportApplication_ReadById
|
||||
@Id UNIQUEIDENTIFIER
|
||||
AS
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF @Id IS NULL
|
||||
THROW 50000, 'Id cannot be null', 1;
|
||||
|
||||
SELECT
|
||||
Id,
|
||||
OrganizationId,
|
||||
Uri,
|
||||
CreationDate,
|
||||
RevisionDate
|
||||
FROM [dbo].[PasswordHealthReportApplicationView]
|
||||
WHERE Id = @Id;
|
@ -0,0 +1,16 @@
|
||||
CREATE PROCEDURE dbo.PasswordHealthReportApplication_ReadByOrganizationId
|
||||
@OrganizationId UNIQUEIDENTIFIER
|
||||
AS
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF @OrganizationId IS NULL
|
||||
THROW 50000, 'OrganizationId cannot be null', 1;
|
||||
|
||||
SELECT
|
||||
Id,
|
||||
OrganizationId,
|
||||
Uri,
|
||||
CreationDate,
|
||||
RevisionDate
|
||||
FROM [dbo].[PasswordHealthReportApplicationView]
|
||||
WHERE OrganizationId = @OrganizationId;
|
@ -0,0 +1,13 @@
|
||||
CREATE PROC dbo.PasswordHealthReportApplication_Update
|
||||
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@Uri nvarchar(max),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7)
|
||||
AS
|
||||
SET NOCOUNT ON;
|
||||
UPDATE dbo.PasswordHealthReportApplication
|
||||
SET OrganizationId = @OrganizationId,
|
||||
Uri = @Uri,
|
||||
RevisionDate = @RevisionDate
|
||||
WHERE Id = @Id
|
@ -1,25 +1,24 @@
|
||||
CREATE TABLE [dbo].[Device] (
|
||||
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||
[UserId] UNIQUEIDENTIFIER NOT NULL,
|
||||
[Name] NVARCHAR (50) NOT NULL,
|
||||
[Type] SMALLINT NOT NULL,
|
||||
[Identifier] NVARCHAR (50) NOT NULL,
|
||||
[PushToken] NVARCHAR (255) NULL,
|
||||
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||
[RevisionDate] DATETIME2 (7) NOT NULL,
|
||||
[EncryptedUserKey] VARCHAR (MAX) NULL,
|
||||
[EncryptedPublicKey] VARCHAR (MAX) NULL,
|
||||
[EncryptedPrivateKey] VARCHAR (MAX) NULL,
|
||||
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||
[UserId] UNIQUEIDENTIFIER NOT NULL,
|
||||
[Name] NVARCHAR (50) NOT NULL,
|
||||
[Type] SMALLINT NOT NULL,
|
||||
[Identifier] NVARCHAR (50) NOT NULL,
|
||||
[PushToken] NVARCHAR (255) NULL,
|
||||
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||
[RevisionDate] DATETIME2 (7) NOT NULL,
|
||||
[EncryptedUserKey] VARCHAR (MAX) NULL,
|
||||
[EncryptedPublicKey] VARCHAR (MAX) NULL,
|
||||
[EncryptedPrivateKey] VARCHAR (MAX) NULL,
|
||||
[Active] BIT NOT NULL CONSTRAINT [DF_Device_Active] DEFAULT (1),
|
||||
CONSTRAINT [PK_Device] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||
CONSTRAINT [FK_Device_User] FOREIGN KEY ([UserId]) REFERENCES [dbo].[User] ([Id])
|
||||
);
|
||||
|
||||
|
||||
GO
|
||||
CREATE UNIQUE NONCLUSTERED INDEX [UX_Device_UserId_Identifier]
|
||||
ON [dbo].[Device]([UserId] ASC, [Identifier] ASC);
|
||||
|
||||
|
||||
GO
|
||||
CREATE NONCLUSTERED INDEX [IX_Device_Identifier]
|
||||
ON [dbo].[Device]([Identifier] ASC);
|
||||
|
15
src/Sql/dbo/Tables/PasswordHealthReportApplication.sql
Normal file
15
src/Sql/dbo/Tables/PasswordHealthReportApplication.sql
Normal file
@ -0,0 +1,15 @@
|
||||
CREATE TABLE [dbo].[PasswordHealthReportApplication]
|
||||
(
|
||||
Id UNIQUEIDENTIFIER NOT NULL,
|
||||
OrganizationId UNIQUEIDENTIFIER NOT NULL,
|
||||
Uri nvarchar(max),
|
||||
CreationDate DATETIME2(7) NOT NULL,
|
||||
RevisionDate DATETIME2(7) NOT NULL,
|
||||
CONSTRAINT [PK_PasswordHealthReportApplication] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||
CONSTRAINT [FK_PasswordHealthReportApplication_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]),
|
||||
);
|
||||
GO
|
||||
|
||||
CREATE NONCLUSTERED INDEX [IX_PasswordHealthReportApplication_OrganizationId]
|
||||
ON [dbo].[PasswordHealthReportApplication] (OrganizationId);
|
||||
GO
|
@ -0,0 +1,2 @@
|
||||
CREATE VIEW [dbo].[PasswordHealthReportApplicationView] AS
|
||||
SELECT * FROM [dbo].[PasswordHealthReportApplication]
|
@ -2,6 +2,7 @@
|
||||
|
||||
using Bit.Admin.Models;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Bit.Test.Common.AutoFixture.Attributes;
|
||||
|
||||
namespace Admin.Test.Models;
|
||||
@ -79,7 +80,7 @@ public class UserViewModelTests
|
||||
{
|
||||
var lookup = new List<(Guid, bool)> { (user.Id, true) };
|
||||
|
||||
var actual = UserViewModel.MapViewModel(user, lookup);
|
||||
var actual = UserViewModel.MapViewModel(user, lookup, false);
|
||||
|
||||
Assert.True(actual.TwoFactorEnabled);
|
||||
}
|
||||
@ -90,7 +91,7 @@ public class UserViewModelTests
|
||||
{
|
||||
var lookup = new List<(Guid, bool)> { (user.Id, false) };
|
||||
|
||||
var actual = UserViewModel.MapViewModel(user, lookup);
|
||||
var actual = UserViewModel.MapViewModel(user, lookup, false);
|
||||
|
||||
Assert.False(actual.TwoFactorEnabled);
|
||||
}
|
||||
@ -101,8 +102,44 @@ public class UserViewModelTests
|
||||
{
|
||||
var lookup = new List<(Guid, bool)> { (Guid.NewGuid(), true) };
|
||||
|
||||
var actual = UserViewModel.MapViewModel(user, lookup);
|
||||
var actual = UserViewModel.MapViewModel(user, lookup, false);
|
||||
|
||||
Assert.False(actual.TwoFactorEnabled);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void MapUserViewModel_WithVerifiedDomain_ReturnsUserViewModel(User user)
|
||||
{
|
||||
|
||||
var verifiedDomain = true;
|
||||
|
||||
var actual = UserViewModel.MapViewModel(user, true, Array.Empty<Cipher>(), verifiedDomain);
|
||||
|
||||
Assert.True(actual.DomainVerified);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void MapUserViewModel_WithoutVerifiedDomain_ReturnsUserViewModel(User user)
|
||||
{
|
||||
|
||||
var verifiedDomain = false;
|
||||
|
||||
var actual = UserViewModel.MapViewModel(user, true, Array.Empty<Cipher>(), verifiedDomain);
|
||||
|
||||
Assert.False(actual.DomainVerified);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public void MapUserViewModel_WithNullVerifiedDomain_ReturnsUserViewModel(User user)
|
||||
{
|
||||
|
||||
var actual = UserViewModel.MapViewModel(user, true, Array.Empty<Cipher>(), null);
|
||||
|
||||
Assert.Null(actual.DomainVerified);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,15 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Api.IntegrationTest.Factories;
|
||||
using Bit.Api.IntegrationTest.Helpers;
|
||||
using Bit.Api.Models.Response;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Services;
|
||||
using NSubstitute;
|
||||
using Xunit;
|
||||
|
||||
namespace Bit.Api.IntegrationTest.Controllers;
|
||||
@ -35,4 +44,82 @@ public class AccountsControllerTest : IClassFixture<ApiApplicationFactory>
|
||||
Assert.Null(content.PrivateKey);
|
||||
Assert.NotNull(content.SecurityStamp);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostEmailToken_WhenAccountDeprovisioningEnabled_WithManagedAccount_ThrowsBadRequest()
|
||||
{
|
||||
var email = await SetupOrganizationManagedAccount();
|
||||
|
||||
var tokens = await _factory.LoginAsync(email);
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var model = new EmailTokenRequestModel
|
||||
{
|
||||
NewEmail = $"{Guid.NewGuid()}@example.com",
|
||||
MasterPasswordHash = "master_password_hash"
|
||||
};
|
||||
|
||||
using var message = new HttpRequestMessage(HttpMethod.Post, "/accounts/email-token")
|
||||
{
|
||||
Content = JsonContent.Create(model)
|
||||
};
|
||||
message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token);
|
||||
var response = await client.SendAsync(message);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Contains("Cannot change emails for accounts owned by an organization", content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostEmail_WhenAccountDeprovisioningEnabled_WithManagedAccount_ThrowsBadRequest()
|
||||
{
|
||||
var email = await SetupOrganizationManagedAccount();
|
||||
|
||||
var tokens = await _factory.LoginAsync(email);
|
||||
var client = _factory.CreateClient();
|
||||
|
||||
var model = new EmailRequestModel
|
||||
{
|
||||
NewEmail = $"{Guid.NewGuid()}@example.com",
|
||||
MasterPasswordHash = "master_password_hash",
|
||||
NewMasterPasswordHash = "master_password_hash",
|
||||
Token = "validtoken",
|
||||
Key = "key"
|
||||
};
|
||||
|
||||
using var message = new HttpRequestMessage(HttpMethod.Post, "/accounts/email")
|
||||
{
|
||||
Content = JsonContent.Create(model)
|
||||
};
|
||||
message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", tokens.Token);
|
||||
var response = await client.SendAsync(message);
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
|
||||
var content = await response.Content.ReadAsStringAsync();
|
||||
Assert.Contains("Cannot change emails for accounts owned by an organization", content);
|
||||
}
|
||||
|
||||
private async Task<string> SetupOrganizationManagedAccount()
|
||||
{
|
||||
_factory.SubstituteService<IFeatureService>(featureService =>
|
||||
featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true));
|
||||
|
||||
// Create the owner account
|
||||
var ownerEmail = $"{Guid.NewGuid()}@bitwarden.com";
|
||||
await _factory.LoginWithNewAccount(ownerEmail);
|
||||
|
||||
// Create the organization
|
||||
var (_organization, _) = await OrganizationTestHelpers.SignUpAsync(_factory, plan: PlanType.EnterpriseAnnually2023,
|
||||
ownerEmail: ownerEmail, passwordManagerSeats: 10, paymentMethod: PaymentMethodType.Card);
|
||||
|
||||
// Create a new organization member
|
||||
var (email, orgUser) = await OrganizationTestHelpers.CreateNewUserWithAccountAsync(_factory, _organization.Id,
|
||||
OrganizationUserType.Custom, new Permissions { AccessReports = true, ManageScim = true });
|
||||
|
||||
// Add a verified domain
|
||||
await OrganizationTestHelpers.CreateVerifiedDomainAsync(_factory, _organization.Id, "bitwarden.com");
|
||||
|
||||
return email;
|
||||
}
|
||||
}
|
||||
|
@ -105,4 +105,22 @@ public static class OrganizationTestHelpers
|
||||
|
||||
return (email, organizationUser);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a VerifiedDomain for the specified organization.
|
||||
/// </summary>
|
||||
public static async Task CreateVerifiedDomainAsync(ApiApplicationFactory factory, Guid organizationId, string domain)
|
||||
{
|
||||
var organizationDomainRepository = factory.GetService<IOrganizationDomainRepository>();
|
||||
|
||||
var verifiedDomain = new OrganizationDomain
|
||||
{
|
||||
OrganizationId = organizationId,
|
||||
DomainName = domain,
|
||||
Txt = "btw+test18383838383"
|
||||
};
|
||||
verifiedDomain.SetVerifiedDate();
|
||||
|
||||
await organizationDomainRepository.CreateAsync(verifiedDomain);
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Api.AdminConsole.Controllers;
|
||||
using Bit.Api.AdminConsole.Models.Request.Organizations;
|
||||
using Bit.Api.Auth.Models.Request.Accounts;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
@ -273,17 +272,12 @@ public class OrganizationUsersControllerTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteAccount_WhenUserCanManageUsers_Success(
|
||||
Guid orgId,
|
||||
Guid id,
|
||||
SecretVerificationRequestModel model,
|
||||
User currentUser,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
Guid orgId, Guid id, User currentUser, SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser);
|
||||
sutProvider.GetDependency<IUserService>().VerifySecretAsync(currentUser, model.Secret).Returns(true);
|
||||
|
||||
await sutProvider.Sut.DeleteAccount(orgId, id, model);
|
||||
await sutProvider.Sut.DeleteAccount(orgId, id);
|
||||
|
||||
await sutProvider.GetDependency<IDeleteManagedOrganizationUserAccountCommand>()
|
||||
.Received(1)
|
||||
@ -293,60 +287,34 @@ public class OrganizationUsersControllerTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteAccount_WhenUserCannotManageUsers_ThrowsNotFoundException(
|
||||
Guid orgId,
|
||||
Guid id,
|
||||
SecretVerificationRequestModel model,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
Guid orgId, Guid id, SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(false);
|
||||
|
||||
await Assert.ThrowsAsync<NotFoundException>(() =>
|
||||
sutProvider.Sut.DeleteAccount(orgId, id, model));
|
||||
sutProvider.Sut.DeleteAccount(orgId, id));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException(
|
||||
Guid orgId,
|
||||
Guid id,
|
||||
SecretVerificationRequestModel model,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
Guid orgId, Guid id, SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs((User)null);
|
||||
|
||||
await Assert.ThrowsAsync<UnauthorizedAccessException>(() =>
|
||||
sutProvider.Sut.DeleteAccount(orgId, id, model));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task DeleteAccount_WhenSecretVerificationFails_ThrowsBadRequestException(
|
||||
Guid orgId,
|
||||
Guid id,
|
||||
SecretVerificationRequestModel model,
|
||||
User currentUser,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser);
|
||||
sutProvider.GetDependency<IUserService>().VerifySecretAsync(currentUser, model.Secret).Returns(false);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.DeleteAccount(orgId, id, model));
|
||||
sutProvider.Sut.DeleteAccount(orgId, id));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task BulkDeleteAccount_WhenUserCanManageUsers_Success(
|
||||
Guid orgId,
|
||||
SecureOrganizationUserBulkRequestModel model,
|
||||
User currentUser,
|
||||
List<(Guid, string)> deleteResults,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
Guid orgId, OrganizationUserBulkRequestModel model, User currentUser,
|
||||
List<(Guid, string)> deleteResults, SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser);
|
||||
sutProvider.GetDependency<IUserService>().VerifySecretAsync(currentUser, model.Secret).Returns(true);
|
||||
sutProvider.GetDependency<IDeleteManagedOrganizationUserAccountCommand>()
|
||||
.DeleteManyUsersAsync(orgId, model.Ids, currentUser.Id)
|
||||
.Returns(deleteResults);
|
||||
@ -363,9 +331,7 @@ public class OrganizationUsersControllerTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task BulkDeleteAccount_WhenUserCannotManageUsers_ThrowsNotFoundException(
|
||||
Guid orgId,
|
||||
SecureOrganizationUserBulkRequestModel model,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
Guid orgId, OrganizationUserBulkRequestModel model, SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(false);
|
||||
|
||||
@ -376,9 +342,7 @@ public class OrganizationUsersControllerTests
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task BulkDeleteAccount_WhenCurrentUserNotFound_ThrowsUnauthorizedAccessException(
|
||||
Guid orgId,
|
||||
SecureOrganizationUserBulkRequestModel model,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
Guid orgId, OrganizationUserBulkRequestModel model, SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs((User)null);
|
||||
@ -387,21 +351,6 @@ public class OrganizationUsersControllerTests
|
||||
sutProvider.Sut.BulkDeleteAccount(orgId, model));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task BulkDeleteAccount_WhenSecretVerificationFails_ThrowsBadRequestException(
|
||||
Guid orgId,
|
||||
SecureOrganizationUserBulkRequestModel model,
|
||||
User currentUser,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().ManageUsers(orgId).Returns(true);
|
||||
sutProvider.GetDependency<IUserService>().GetUserByPrincipalAsync(default).ReturnsForAnyArgs(currentUser);
|
||||
sutProvider.GetDependency<IUserService>().VerifySecretAsync(currentUser, model.Secret).Returns(false);
|
||||
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.BulkDeleteAccount(orgId, model));
|
||||
}
|
||||
|
||||
private void GetMany_Setup(OrganizationAbility organizationAbility,
|
||||
ICollection<OrganizationUserUserDetails> organizationUsers,
|
||||
SutProvider<OrganizationUsersController> sutProvider)
|
||||
|
@ -7,6 +7,7 @@ using Bit.Api.Auth.Models.Request.WebAuthn;
|
||||
using Bit.Api.Auth.Validators;
|
||||
using Bit.Api.Tools.Models.Request;
|
||||
using Bit.Api.Vault.Models.Request;
|
||||
using Bit.Core;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Auth.Entities;
|
||||
@ -143,6 +144,21 @@ public class AccountsControllerTests : IDisposable
|
||||
await _userService.Received(1).InitiateEmailChangeAsync(user, newEmail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostEmailToken_WithAccountDeprovisioningEnabled_WhenUserIsNotManagedByAnOrganization_ShouldInitiateEmailChange()
|
||||
{
|
||||
var user = GenerateExampleUser();
|
||||
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
||||
ConfigureUserServiceToAcceptPasswordFor(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||
_userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(false);
|
||||
var newEmail = "example@user.com";
|
||||
|
||||
await _sut.PostEmailToken(new EmailTokenRequestModel { NewEmail = newEmail });
|
||||
|
||||
await _userService.Received(1).InitiateEmailChangeAsync(user, newEmail);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostEmailToken_WhenNotAuthorized_ShouldThrowUnauthorizedAccessException()
|
||||
{
|
||||
@ -165,6 +181,22 @@ public class AccountsControllerTests : IDisposable
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostEmailToken_WithAccountDeprovisioningEnabled_WhenUserIsManagedByAnOrganization_ShouldThrowBadRequestException()
|
||||
{
|
||||
var user = GenerateExampleUser();
|
||||
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
||||
ConfigureUserServiceToAcceptPasswordFor(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||
_userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(true);
|
||||
|
||||
var result = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => _sut.PostEmailToken(new EmailTokenRequestModel())
|
||||
);
|
||||
|
||||
Assert.Equal("Cannot change emails for accounts owned by an organization. Contact your organization administrator for additional details.", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostEmail_ShouldChangeUserEmail()
|
||||
{
|
||||
@ -178,6 +210,21 @@ public class AccountsControllerTests : IDisposable
|
||||
await _userService.Received(1).ChangeEmailAsync(user, default, default, default, default, default);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostEmail_WithAccountDeprovisioningEnabled_WhenUserIsNotManagedByAnOrganization_ShouldChangeUserEmail()
|
||||
{
|
||||
var user = GenerateExampleUser();
|
||||
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
||||
_userService.ChangeEmailAsync(user, default, default, default, default, default)
|
||||
.Returns(Task.FromResult(IdentityResult.Success));
|
||||
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||
_userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(false);
|
||||
|
||||
await _sut.PostEmail(new EmailRequestModel());
|
||||
|
||||
await _userService.Received(1).ChangeEmailAsync(user, default, default, default, default, default);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostEmail_WhenNotAuthorized_ShouldThrownUnauthorizedAccessException()
|
||||
{
|
||||
@ -201,6 +248,21 @@ public class AccountsControllerTests : IDisposable
|
||||
);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostEmail_WithAccountDeprovisioningEnabled_WhenUserIsManagedByAnOrganization_ShouldThrowBadRequestException()
|
||||
{
|
||||
var user = GenerateExampleUser();
|
||||
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||
_userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(true);
|
||||
|
||||
var result = await Assert.ThrowsAsync<BadRequestException>(
|
||||
() => _sut.PostEmail(new EmailRequestModel())
|
||||
);
|
||||
|
||||
Assert.Equal("Cannot change emails for accounts owned by an organization. Contact your organization administrator for additional details.", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PostVerifyEmail_ShouldSendEmailVerification()
|
||||
{
|
||||
@ -472,6 +534,34 @@ public class AccountsControllerTests : IDisposable
|
||||
await Assert.ThrowsAsync<BadRequestException>(() => _sut.PostSetPasswordAsync(model));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WhenAccountDeprovisioningIsEnabled_WithUserManagedByAnOrganization_ThrowsBadRequestException()
|
||||
{
|
||||
var user = GenerateExampleUser();
|
||||
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
||||
ConfigureUserServiceToAcceptPasswordFor(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||
_userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(true);
|
||||
|
||||
var result = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Delete(new SecretVerificationRequestModel()));
|
||||
|
||||
Assert.Equal("Cannot delete accounts owned by an organization. Contact your organization administrator for additional details.", result.Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_WhenAccountDeprovisioningIsEnabled_WithUserNotManagedByAnOrganization_ShouldSucceed()
|
||||
{
|
||||
var user = GenerateExampleUser();
|
||||
ConfigureUserServiceToReturnValidPrincipalFor(user);
|
||||
ConfigureUserServiceToAcceptPasswordFor(user);
|
||||
_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning).Returns(true);
|
||||
_userService.IsManagedByAnyOrganizationAsync(user.Id).Returns(false);
|
||||
_userService.DeleteAsync(user).Returns(IdentityResult.Success);
|
||||
|
||||
await _sut.Delete(new SecretVerificationRequestModel());
|
||||
|
||||
await _userService.Received(1).DeleteAsync(user);
|
||||
}
|
||||
|
||||
// Below are helper functions that currently belong to this
|
||||
// test class, but ultimately may need to be split out into
|
||||
|
@ -37,7 +37,7 @@ public class OrganizationBillingControllerTests
|
||||
Guid organizationId,
|
||||
SutProvider<OrganizationBillingController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessMembersTab(organizationId).Returns(true);
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(organizationId).Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationBillingService>().GetMetadata(organizationId).Returns((OrganizationMetadata)null);
|
||||
|
||||
var result = await sutProvider.Sut.GetMetadataAsync(organizationId);
|
||||
@ -50,18 +50,20 @@ public class OrganizationBillingControllerTests
|
||||
Guid organizationId,
|
||||
SutProvider<OrganizationBillingController> sutProvider)
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessMembersTab(organizationId).Returns(true);
|
||||
sutProvider.GetDependency<ICurrentContext>().OrganizationUser(organizationId).Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationBillingService>().GetMetadata(organizationId)
|
||||
.Returns(new OrganizationMetadata(true, true));
|
||||
.Returns(new OrganizationMetadata(true, true, true, true));
|
||||
|
||||
var result = await sutProvider.Sut.GetMetadataAsync(organizationId);
|
||||
|
||||
Assert.IsType<Ok<OrganizationMetadataResponse>>(result);
|
||||
|
||||
var organizationMetadataResponse = ((Ok<OrganizationMetadataResponse>)result).Value;
|
||||
var response = ((Ok<OrganizationMetadataResponse>)result).Value;
|
||||
|
||||
Assert.True(organizationMetadataResponse.IsEligibleForSelfHost);
|
||||
Assert.True(organizationMetadataResponse.IsOnSecretsManagerStandalone);
|
||||
Assert.True(response.IsEligibleForSelfHost);
|
||||
Assert.True(response.IsManaged);
|
||||
Assert.True(response.IsOnSecretsManagerStandalone);
|
||||
Assert.True(response.IsSubscriptionUnpaid);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
|
@ -3,8 +3,10 @@ using System.Text.Json;
|
||||
using Bit.Api.AdminConsole.Controllers;
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Api.Response;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
@ -132,4 +134,71 @@ public class PoliciesControllerTests
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetMasterPasswordPolicy(orgId));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Get_WhenUserCanManagePolicies_WithExistingType_ReturnsExistingPolicy(
|
||||
SutProvider<PoliciesController> sutProvider, Guid orgId, Policy policy, int type)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.ManagePolicies(orgId)
|
||||
.Returns(true);
|
||||
|
||||
policy.Type = (PolicyType)type;
|
||||
policy.Enabled = true;
|
||||
policy.Data = null;
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(orgId, (PolicyType)type)
|
||||
.Returns(policy);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.Get(orgId, type);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<PolicyResponseModel>(result);
|
||||
Assert.Equal(policy.Id, result.Id);
|
||||
Assert.Equal(policy.Type, result.Type);
|
||||
Assert.Equal(policy.Enabled, result.Enabled);
|
||||
Assert.Equal(policy.OrganizationId, result.OrganizationId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Get_WhenUserCanManagePolicies_WithNonExistingType_ReturnsDefaultPolicy(
|
||||
SutProvider<PoliciesController> sutProvider, Guid orgId, int type)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.ManagePolicies(orgId)
|
||||
.Returns(true);
|
||||
|
||||
sutProvider.GetDependency<IPolicyRepository>()
|
||||
.GetByOrganizationIdTypeAsync(orgId, (PolicyType)type)
|
||||
.Returns((Policy)null);
|
||||
|
||||
// Act
|
||||
var result = await sutProvider.Sut.Get(orgId, type);
|
||||
|
||||
// Assert
|
||||
Assert.IsType<PolicyResponseModel>(result);
|
||||
Assert.Equal(result.Type, (PolicyType)type);
|
||||
Assert.False(result.Enabled);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[BitAutoData]
|
||||
public async Task Get_WhenUserCannotManagePolicies_ThrowsNotFoundException(
|
||||
SutProvider<PoliciesController> sutProvider, Guid orgId, int type)
|
||||
{
|
||||
// Arrange
|
||||
sutProvider.GetDependency<ICurrentContext>()
|
||||
.ManagePolicies(orgId)
|
||||
.Returns(false);
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.Get(orgId, type));
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
|
||||
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
||||
|
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<Nullable>enable</Nullable>
|
||||
@ -8,10 +8,10 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="8.9.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.2" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="8.10.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
|
||||
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)">
|
||||
|
@ -5,7 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.8" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
|
13
util/Migrator/DbScripts/2024-10-22_00_AddSCIMToTeamsPlan.sql
Normal file
13
util/Migrator/DbScripts/2024-10-22_00_AddSCIMToTeamsPlan.sql
Normal file
@ -0,0 +1,13 @@
|
||||
SET DEADLOCK_PRIORITY HIGH
|
||||
GO
|
||||
UPDATE
|
||||
[dbo].[Organization]
|
||||
SET
|
||||
[UseScim] = 1
|
||||
WHERE
|
||||
[PlanType] IN (
|
||||
17, -- Teams (Monthly)
|
||||
18 -- Teams (Annually)
|
||||
)
|
||||
SET DEADLOCK_PRIORITY NORMAL
|
||||
GO
|
@ -0,0 +1,103 @@
|
||||
|
||||
IF OBJECT_ID('dbo.PasswordHealthReportApplication') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE [dbo].[PasswordHealthReportApplication]
|
||||
(
|
||||
Id UNIQUEIDENTIFIER NOT NULL,
|
||||
OrganizationId UNIQUEIDENTIFIER NOT NULL,
|
||||
Uri nvarchar(max),
|
||||
CreationDate DATETIME2(7) NOT NULL,
|
||||
RevisionDate DATETIME2(7) NOT NULL,
|
||||
CONSTRAINT [PK_PasswordHealthReportApplication] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||
CONSTRAINT [FK_PasswordHealthReportApplication_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id]),
|
||||
);
|
||||
|
||||
CREATE NONCLUSTERED INDEX [IX_PasswordHealthReportApplication_OrganizationId]
|
||||
ON [dbo].[PasswordHealthReportApplication] (OrganizationId);
|
||||
END
|
||||
GO
|
||||
|
||||
IF OBJECT_ID('dbo.PasswordHealthReportApplicationView') IS NOT NULL
|
||||
BEGIN
|
||||
DROP VIEW [dbo].[PasswordHealthReportApplicationView]
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE VIEW [dbo].[PasswordHealthReportApplicationView] AS
|
||||
SELECT * FROM [dbo].[PasswordHealthReportApplication]
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROC dbo.PasswordHealthReportApplication_Create
|
||||
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@Uri nvarchar(max),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7)
|
||||
AS
|
||||
SET NOCOUNT ON;
|
||||
INSERT INTO dbo.PasswordHealthReportApplication ( Id, OrganizationId, Uri, CreationDate, RevisionDate )
|
||||
VALUES ( @Id, @OrganizationId, @Uri, @CreationDate, @RevisionDate )
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROC dbo.PasswordHealthReportApplication_ReadByOrganizationId
|
||||
@OrganizationId UNIQUEIDENTIFIER
|
||||
AS
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF @OrganizationId IS NULL
|
||||
THROW 50000, 'OrganizationId cannot be null', 1;
|
||||
|
||||
SELECT
|
||||
Id,
|
||||
OrganizationId,
|
||||
Uri,
|
||||
CreationDate,
|
||||
RevisionDate
|
||||
FROM [dbo].[PasswordHealthReportApplicationView]
|
||||
WHERE OrganizationId = @OrganizationId;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROC dbo.PasswordHealthReportApplication_ReadById
|
||||
@Id UNIQUEIDENTIFIER
|
||||
AS
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF @Id IS NULL
|
||||
THROW 50000, 'Id cannot be null', 1;
|
||||
|
||||
SELECT
|
||||
Id,
|
||||
OrganizationId,
|
||||
Uri,
|
||||
CreationDate,
|
||||
RevisionDate
|
||||
FROM [dbo].[PasswordHealthReportApplicationView]
|
||||
WHERE Id = @Id;
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROC dbo.PasswordHealthReportApplication_Update
|
||||
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||
@OrganizationId UNIQUEIDENTIFIER,
|
||||
@Uri nvarchar(max),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7)
|
||||
AS
|
||||
SET NOCOUNT ON;
|
||||
UPDATE dbo.PasswordHealthReportApplication
|
||||
SET OrganizationId = @OrganizationId,
|
||||
Uri = @Uri,
|
||||
RevisionDate = @RevisionDate
|
||||
WHERE Id = @Id
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROC dbo.PasswordHealthReportApplication_DeleteById
|
||||
@Id UNIQUEIDENTIFIER
|
||||
AS
|
||||
SET NOCOUNT ON;
|
||||
|
||||
IF @Id IS NULL
|
||||
THROW 50000, 'Id cannot be null', 1;
|
||||
|
||||
DELETE FROM [dbo].[PasswordHealthReportApplication]
|
||||
WHERE [Id] = @Id
|
||||
GO
|
118
util/Migrator/DbScripts/2024-10-31-00_DeviceActivation.sql
Normal file
118
util/Migrator/DbScripts/2024-10-31-00_DeviceActivation.sql
Normal file
@ -0,0 +1,118 @@
|
||||
SET DEADLOCK_PRIORITY HIGH
|
||||
GO
|
||||
|
||||
-- add column
|
||||
IF COL_LENGTH('[dbo].[Device]', 'Active') IS NULL
|
||||
BEGIN
|
||||
ALTER TABLE
|
||||
[dbo].[Device]
|
||||
ADD
|
||||
[Active] BIT NOT NULL CONSTRAINT [DF_Device_Active] DEFAULT (1)
|
||||
END
|
||||
GO
|
||||
|
||||
-- refresh view
|
||||
CREATE OR ALTER VIEW [dbo].[DeviceView]
|
||||
AS
|
||||
SELECT
|
||||
*
|
||||
FROM
|
||||
[dbo].[Device]
|
||||
GO
|
||||
|
||||
-- drop now-unused proc for deletion
|
||||
IF OBJECT_ID('[dbo].[Device_DeleteById]') IS NOT NULL
|
||||
BEGIN
|
||||
DROP PROCEDURE [dbo].[Device_DeleteById]
|
||||
END
|
||||
GO
|
||||
|
||||
-- refresh procs
|
||||
CREATE OR ALTER PROCEDURE [dbo].[Device_Create]
|
||||
@Id UNIQUEIDENTIFIER OUTPUT,
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@Name NVARCHAR(50),
|
||||
@Type TINYINT,
|
||||
@Identifier NVARCHAR(50),
|
||||
@PushToken NVARCHAR(255),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7),
|
||||
@EncryptedUserKey VARCHAR(MAX) = NULL,
|
||||
@EncryptedPublicKey VARCHAR(MAX) = NULL,
|
||||
@EncryptedPrivateKey VARCHAR(MAX) = NULL,
|
||||
@Active BIT = 1
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
INSERT INTO [dbo].[Device]
|
||||
(
|
||||
[Id],
|
||||
[UserId],
|
||||
[Name],
|
||||
[Type],
|
||||
[Identifier],
|
||||
[PushToken],
|
||||
[CreationDate],
|
||||
[RevisionDate],
|
||||
[EncryptedUserKey],
|
||||
[EncryptedPublicKey],
|
||||
[EncryptedPrivateKey],
|
||||
[Active]
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
@Id,
|
||||
@UserId,
|
||||
@Name,
|
||||
@Type,
|
||||
@Identifier,
|
||||
@PushToken,
|
||||
@CreationDate,
|
||||
@RevisionDate,
|
||||
@EncryptedUserKey,
|
||||
@EncryptedPublicKey,
|
||||
@EncryptedPrivateKey,
|
||||
@Active
|
||||
)
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE OR ALTER PROCEDURE [dbo].[Device_Update]
|
||||
@Id UNIQUEIDENTIFIER,
|
||||
@UserId UNIQUEIDENTIFIER,
|
||||
@Name NVARCHAR(50),
|
||||
@Type TINYINT,
|
||||
@Identifier NVARCHAR(50),
|
||||
@PushToken NVARCHAR(255),
|
||||
@CreationDate DATETIME2(7),
|
||||
@RevisionDate DATETIME2(7),
|
||||
@EncryptedUserKey VARCHAR(MAX) = NULL,
|
||||
@EncryptedPublicKey VARCHAR(MAX) = NULL,
|
||||
@EncryptedPrivateKey VARCHAR(MAX) = NULL,
|
||||
@Active BIT = 1
|
||||
AS
|
||||
BEGIN
|
||||
SET NOCOUNT ON
|
||||
|
||||
UPDATE
|
||||
[dbo].[Device]
|
||||
SET
|
||||
[UserId] = @UserId,
|
||||
[Name] = @Name,
|
||||
[Type] = @Type,
|
||||
[Identifier] = @Identifier,
|
||||
[PushToken] = @PushToken,
|
||||
[CreationDate] = @CreationDate,
|
||||
[RevisionDate] = @RevisionDate,
|
||||
[EncryptedUserKey] = @EncryptedUserKey,
|
||||
[EncryptedPublicKey] = @EncryptedPublicKey,
|
||||
[EncryptedPrivateKey] = @EncryptedPrivateKey,
|
||||
[Active] = @Active
|
||||
WHERE
|
||||
[Id] = @Id
|
||||
END
|
||||
GO
|
||||
|
||||
SET DEADLOCK_PRIORITY NORMAL
|
||||
GO
|
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="DbScripts\**\*.sql" />
|
||||
@ -7,7 +7,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="dbup-sqlserver" Version="5.0.41" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -1,18 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Migrator\Migrator.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommandDotNet" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<IncludeAllContentForSelfExtract>true</IncludeAllContentForSelfExtract>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Migrator\Migrator.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommandDotNet" Version="7.0.4" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="8.0.1" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
2845
util/MySqlMigrations/Migrations/20241031154652_PasswordHealthReportApplication.Designer.cs
generated
Normal file
2845
util/MySqlMigrations/Migrations/20241031154652_PasswordHealthReportApplication.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,47 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.MySqlMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class PasswordHealthReportApplication : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PasswordHealthReportApplication",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||
OrganizationId = table.Column<Guid>(type: "char(36)", nullable: true, collation: "ascii_general_ci"),
|
||||
Uri = table.Column<string>(type: "longtext", nullable: true)
|
||||
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||
CreationDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||
RevisionDate = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PasswordHealthReportApplication", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_PasswordHealthReportApplication_Organization_OrganizationId",
|
||||
column: x => x.OrganizationId,
|
||||
principalTable: "Organization",
|
||||
principalColumn: "Id");
|
||||
})
|
||||
.Annotation("MySql:CharSet", "utf8mb4");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PasswordHealthReportApplication_OrganizationId",
|
||||
table: "PasswordHealthReportApplication",
|
||||
column: "OrganizationId");
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(name: "PasswordHealthReportApplication");
|
||||
}
|
||||
}
|
2849
util/MySqlMigrations/Migrations/20241031170511_DeviceActivation.Designer.cs
generated
Normal file
2849
util/MySqlMigrations/Migrations/20241031170511_DeviceActivation.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.MySqlMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class DeviceActivation : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "Active",
|
||||
table: "Device",
|
||||
type: "tinyint(1)",
|
||||
nullable: false,
|
||||
defaultValue: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Active",
|
||||
table: "Device");
|
||||
}
|
||||
}
|
@ -939,6 +939,10 @@ namespace Bit.MySqlMigrations.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("char(36)");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("tinyint(1)")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<DateTime>("CreationDate")
|
||||
.HasColumnType("datetime(6)");
|
||||
|
||||
|
2851
util/PostgresMigrations/Migrations/20241031154656_PasswordHealthReportApplication.Designer.cs
generated
Normal file
2851
util/PostgresMigrations/Migrations/20241031154656_PasswordHealthReportApplication.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,43 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.PostgresMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class PasswordHealthReportApplication : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PasswordHealthReportApplication",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||
OrganizationId = table.Column<Guid>(type: "uuid", nullable: true),
|
||||
Uri = table.Column<string>(type: "text", nullable: true),
|
||||
CreationDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||
RevisionDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PasswordHealthReportApplication", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_PasswordHealthReportApplication_Organization_OrganizationId",
|
||||
column: x => x.OrganizationId,
|
||||
principalTable: "Organization",
|
||||
principalColumn: "Id");
|
||||
});
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PasswordHealthReportApplication_OrganizationId",
|
||||
table: "PasswordHealthReportApplication",
|
||||
column: "OrganizationId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(name: "PasswordHealthReportApplication");
|
||||
}
|
||||
}
|
2855
util/PostgresMigrations/Migrations/20241031170505_DeviceActivation.Designer.cs
generated
Normal file
2855
util/PostgresMigrations/Migrations/20241031170505_DeviceActivation.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,28 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.PostgresMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class DeviceActivation : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.AddColumn<bool>(
|
||||
name: "Active",
|
||||
table: "Device",
|
||||
type: "boolean",
|
||||
nullable: false,
|
||||
defaultValue: true);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropColumn(
|
||||
name: "Active",
|
||||
table: "Device");
|
||||
}
|
||||
}
|
@ -944,6 +944,10 @@ namespace Bit.PostgresMigrations.Migrations
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("uuid");
|
||||
|
||||
b.Property<bool>("Active")
|
||||
.HasColumnType("boolean")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<DateTime>("CreationDate")
|
||||
.HasColumnType("timestamp with time zone");
|
||||
|
||||
|
@ -51,6 +51,12 @@ public class EnvironmentFileBuilder
|
||||
_globalOverrideValues.Remove("globalSettings__pushRelayBaseUri");
|
||||
}
|
||||
|
||||
if (_globalOverrideValues.TryGetValue("globalSettings__baseServiceUri__vault", out var vaultUri) && vaultUri != _context.Config.Url)
|
||||
{
|
||||
_globalOverrideValues["globalSettings__baseServiceUri__vault"] = _context.Config.Url;
|
||||
Helpers.WriteLine(_context, "Updated globalSettings__baseServiceUri__vault to match value in config.yml");
|
||||
}
|
||||
|
||||
Build();
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
@ -11,7 +11,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||
<PackageReference Include="YamlDotNet" Version="11.2.1" />
|
||||
</ItemGroup>
|
||||
|
||||
|
2834
util/SqliteMigrations/Migrations/20241031154700_PasswordHealthReportApplication.Designer.cs
generated
Normal file
2834
util/SqliteMigrations/Migrations/20241031154700_PasswordHealthReportApplication.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,43 @@
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Bit.SqliteMigrations.Migrations;
|
||||
|
||||
/// <inheritdoc />
|
||||
public partial class PasswordHealthReportApplication : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "PasswordHealthReportApplication",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||
OrganizationId = table.Column<Guid>(type: "TEXT", nullable: true),
|
||||
Uri = table.Column<string>(type: "TEXT", nullable: true),
|
||||
CreationDate = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||
RevisionDate = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_PasswordHealthReportApplication", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_PasswordHealthReportApplication_Organization_OrganizationId",
|
||||
column: x => x.OrganizationId,
|
||||
principalTable: "Organization",
|
||||
principalColumn: "Id");
|
||||
});
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_PasswordHealthReportApplication_OrganizationId",
|
||||
table: "PasswordHealthReportApplication",
|
||||
column: "OrganizationId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(name: "PasswordHealthReportApplication");
|
||||
}
|
||||
}
|
2838
util/SqliteMigrations/Migrations/20241031170500_DeviceActivation.Designer.cs
generated
Normal file
2838
util/SqliteMigrations/Migrations/20241031170500_DeviceActivation.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user