mirror of
https://github.com/bitwarden/server.git
synced 2025-07-02 00:22:50 -05:00
Merge branch 'main' into ac/ac-1682/data-migrations-for-deprecated-permissions
This commit is contained in:
@ -4,27 +4,11 @@
|
|||||||
"tools": {
|
"tools": {
|
||||||
"swashbuckle.aspnetcore.cli": {
|
"swashbuckle.aspnetcore.cli": {
|
||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"commands": [
|
"commands": ["swagger"]
|
||||||
"swagger"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"coverlet.console": {
|
|
||||||
"version": "3.1.2",
|
|
||||||
"commands": [
|
|
||||||
"coverlet"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"dotnet-reportgenerator-globaltool": {
|
|
||||||
"version": "5.1.6",
|
|
||||||
"commands": [
|
|
||||||
"reportgenerator"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"dotnet-ef": {
|
"dotnet-ef": {
|
||||||
"version": "7.0.14",
|
"version": "7.0.14",
|
||||||
"commands": [
|
"commands": ["dotnet-ef"]
|
||||||
"dotnet-ef"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
7
.github/renovate.json
vendored
7
.github/renovate.json
vendored
@ -37,6 +37,10 @@
|
|||||||
"matchManagers": ["github-actions"],
|
"matchManagers": ["github-actions"],
|
||||||
"matchUpdateTypes": ["minor", "patch"]
|
"matchUpdateTypes": ["minor", "patch"]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"matchManagers": ["github-actions", "dockerfile", "docker-compose"],
|
||||||
|
"commitMessagePrefix": "[deps] DevOps:"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"matchPackageNames": ["DnsClient", "Quartz"],
|
"matchPackageNames": ["DnsClient", "Quartz"],
|
||||||
"description": "Admin Console owned dependencies",
|
"description": "Admin Console owned dependencies",
|
||||||
@ -59,8 +63,7 @@
|
|||||||
"Azure.Storage.Blobs",
|
"Azure.Storage.Blobs",
|
||||||
"Azure.Storage.Queues",
|
"Azure.Storage.Queues",
|
||||||
"Fido2.AspNet",
|
"Fido2.AspNet",
|
||||||
"IdentityServer4",
|
"Duende.IdentityServer",
|
||||||
"IdentityServer4.AccessTokenValidation",
|
|
||||||
"Microsoft.Azure.Cosmos",
|
"Microsoft.Azure.Cosmos",
|
||||||
"Microsoft.Azure.Cosmos.Table",
|
"Microsoft.Azure.Cosmos.Table",
|
||||||
"Microsoft.Extensions.Caching.StackExchangeRedis",
|
"Microsoft.Extensions.Caching.StackExchangeRedis",
|
||||||
|
87
.github/workflows/build.yml
vendored
87
.github/workflows/build.yml
vendored
@ -61,28 +61,14 @@ jobs:
|
|||||||
echo "GitHub ref: $GITHUB_REF"
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
- name: Restore
|
|
||||||
run: dotnet restore --locked-mode
|
|
||||||
shell: pwsh
|
|
||||||
|
|
||||||
- name: Remove SQL proj
|
- name: Remove SQL proj
|
||||||
run: dotnet sln bitwarden-server.sln remove src/Sql/Sql.sqlproj
|
run: dotnet sln bitwarden-server.sln remove src/Sql/Sql.sqlproj
|
||||||
|
|
||||||
- name: Build OSS solution
|
|
||||||
run: dotnet build bitwarden-server.sln -p:Configuration=Debug -p:DefineConstants="OSS" --verbosity minimal
|
|
||||||
shell: pwsh
|
|
||||||
|
|
||||||
- name: Build solution
|
|
||||||
run: dotnet build bitwarden-server.sln -p:Configuration=Debug --verbosity minimal
|
|
||||||
shell: pwsh
|
|
||||||
|
|
||||||
- name: Test OSS solution
|
- name: Test OSS solution
|
||||||
run: dotnet test ./test --configuration Debug --no-build --logger "trx;LogFileName=oss-test-results.trx"
|
run: dotnet test ./test --configuration Release --logger "trx;LogFileName=oss-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
|
||||||
shell: pwsh
|
|
||||||
|
|
||||||
- name: Test Bitwarden solution
|
- name: Test Bitwarden solution
|
||||||
run: dotnet test ./bitwarden_license/test --configuration Debug --no-build --logger "trx;LogFileName=bw-test-results.trx"
|
run: dotnet test ./bitwarden_license/test --configuration Release --logger "trx;LogFileName=bw-test-results.trx" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage"
|
||||||
shell: pwsh
|
|
||||||
|
|
||||||
- name: Report test results
|
- name: Report test results
|
||||||
uses: dorny/test-reporter@c9b3d0e2bd2a4e96aaf424dbaa31c46b42318226 # v1.6.0
|
uses: dorny/test-reporter@c9b3d0e2bd2a4e96aaf424dbaa31c46b42318226 # v1.6.0
|
||||||
@ -93,6 +79,11 @@ jobs:
|
|||||||
reporter: dotnet-trx
|
reporter: dotnet-trx
|
||||||
fail-on-error: true
|
fail-on-error: true
|
||||||
|
|
||||||
|
- name: Upload to codecov.io
|
||||||
|
uses: codecov/codecov-action@eaaf4bedf32dbdc6b720b63067d99c4d77d6047d # v3.1.4
|
||||||
|
env:
|
||||||
|
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||||
|
|
||||||
build-artifacts:
|
build-artifacts:
|
||||||
name: Build artifacts
|
name: Build artifacts
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
@ -156,14 +147,6 @@ jobs:
|
|||||||
echo "GitHub ref: $GITHUB_REF"
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
- name: Restore/Clean project
|
|
||||||
working-directory: ${{ matrix.base_path }}/${{ matrix.project_name }}
|
|
||||||
run: |
|
|
||||||
echo "Restore"
|
|
||||||
dotnet restore
|
|
||||||
echo "Clean"
|
|
||||||
dotnet clean -c "Release" -o obj/build-output/publish
|
|
||||||
|
|
||||||
- name: Build node
|
- name: Build node
|
||||||
if: ${{ matrix.node }}
|
if: ${{ matrix.node }}
|
||||||
working-directory: ${{ matrix.base_path }}/${{ matrix.project_name }}
|
working-directory: ${{ matrix.base_path }}/${{ matrix.project_name }}
|
||||||
@ -357,9 +340,6 @@ jobs:
|
|||||||
- name: Login to PROD ACR
|
- name: Login to PROD ACR
|
||||||
run: az acr login -n $_AZ_REGISTRY --only-show-errors
|
run: az acr login -n $_AZ_REGISTRY --only-show-errors
|
||||||
|
|
||||||
- name: Restore
|
|
||||||
run: dotnet tool restore
|
|
||||||
|
|
||||||
- name: Make Docker stubs
|
- name: Make Docker stubs
|
||||||
if: github.ref == 'refs/heads/main' ||
|
if: github.ref == 'refs/heads/main' ||
|
||||||
github.ref == 'refs/heads/rc' ||
|
github.ref == 'refs/heads/rc' ||
|
||||||
@ -443,10 +423,8 @@ jobs:
|
|||||||
- name: Build Swagger
|
- name: Build Swagger
|
||||||
run: |
|
run: |
|
||||||
cd ./src/Api
|
cd ./src/Api
|
||||||
echo "Restore"
|
echo "Restore tools"
|
||||||
dotnet restore
|
dotnet tool restore
|
||||||
echo "Clean"
|
|
||||||
dotnet clean -c "Release" -o obj/build-output/publish
|
|
||||||
echo "Publish"
|
echo "Publish"
|
||||||
dotnet publish -c "Release" -o obj/build-output/publish
|
dotnet publish -c "Release" -o obj/build-output/publish
|
||||||
|
|
||||||
@ -495,11 +473,6 @@ jobs:
|
|||||||
echo "GitHub ref: $GITHUB_REF"
|
echo "GitHub ref: $GITHUB_REF"
|
||||||
echo "GitHub event: $GITHUB_EVENT"
|
echo "GitHub event: $GITHUB_EVENT"
|
||||||
|
|
||||||
- name: Restore project
|
|
||||||
run: |
|
|
||||||
echo "Restore"
|
|
||||||
dotnet restore
|
|
||||||
|
|
||||||
- name: Publish project
|
- name: Publish project
|
||||||
run: |
|
run: |
|
||||||
dotnet publish -c "Release" -o obj/build-output/publish -r ${{ matrix.target }} -p:PublishSingleFile=true \
|
dotnet publish -c "Release" -o obj/build-output/publish -r ${{ matrix.target }} -p:PublishSingleFile=true \
|
||||||
@ -521,12 +494,10 @@ jobs:
|
|||||||
path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility
|
path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
|
||||||
self-host-build:
|
self-host-build:
|
||||||
name: Trigger self-host build
|
name: Trigger self-host build
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs: build-docker
|
||||||
- build-docker
|
|
||||||
steps:
|
steps:
|
||||||
- name: Login to Azure - CI Subscription
|
- name: Login to Azure - CI Subscription
|
||||||
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||||
@ -555,6 +526,40 @@ jobs:
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
trigger-k8s-deploy:
|
||||||
|
name: Trigger k8s deploy
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs: build-docker
|
||||||
|
steps:
|
||||||
|
- name: Login to Azure - CI Subscription
|
||||||
|
uses: Azure/login@92a5484dfaf04ca78a94597f4f19fea633851fa2 # v1.4.7
|
||||||
|
with:
|
||||||
|
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||||
|
|
||||||
|
- name: Retrieve github PAT secrets
|
||||||
|
id: retrieve-secret-pat
|
||||||
|
uses: bitwarden/gh-actions/get-keyvault-secrets@main
|
||||||
|
with:
|
||||||
|
keyvault: "bitwarden-ci"
|
||||||
|
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
|
||||||
|
|
||||||
|
- name: Trigger k8s deploy
|
||||||
|
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1
|
||||||
|
with:
|
||||||
|
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
|
||||||
|
script: |
|
||||||
|
await github.rest.actions.createWorkflowDispatch({
|
||||||
|
owner: 'bitwarden',
|
||||||
|
repo: 'devops',
|
||||||
|
workflow_id: 'deploy-k8s.yml',
|
||||||
|
ref: 'main',
|
||||||
|
inputs: {
|
||||||
|
environment: 'US-DEV Cloud',
|
||||||
|
tag: 'main'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
check-failures:
|
check-failures:
|
||||||
name: Check for failures
|
name: Check for failures
|
||||||
if: always()
|
if: always()
|
||||||
@ -568,6 +573,7 @@ jobs:
|
|||||||
- upload
|
- upload
|
||||||
- build-mssqlmigratorutility
|
- build-mssqlmigratorutility
|
||||||
- self-host-build
|
- self-host-build
|
||||||
|
- trigger-k8s-deploy
|
||||||
steps:
|
steps:
|
||||||
- name: Check if any job failed
|
- name: Check if any job failed
|
||||||
if: |
|
if: |
|
||||||
@ -583,6 +589,7 @@ jobs:
|
|||||||
UPLOAD_STATUS: ${{ needs.upload.result }}
|
UPLOAD_STATUS: ${{ needs.upload.result }}
|
||||||
BUILD_MSSQLMIGRATORUTILITY_STATUS: ${{ needs.build-mssqlmigratorutility.result }}
|
BUILD_MSSQLMIGRATORUTILITY_STATUS: ${{ needs.build-mssqlmigratorutility.result }}
|
||||||
TRIGGER_SELF_HOST_BUILD_STATUS: ${{ needs.self-host-build.result }}
|
TRIGGER_SELF_HOST_BUILD_STATUS: ${{ needs.self-host-build.result }}
|
||||||
|
TRIGGER_K8S_DEPLOY_STATUS: ${{ needs.trigger-k8s-deploy.result }}
|
||||||
run: |
|
run: |
|
||||||
if [ "$CLOC_STATUS" = "failure" ]; then
|
if [ "$CLOC_STATUS" = "failure" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
@ -600,6 +607,8 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
elif [ "$TRIGGER_SELF_HOST_BUILD_STATUS" = "failure" ]; then
|
elif [ "$TRIGGER_SELF_HOST_BUILD_STATUS" = "failure" ]; then
|
||||||
exit 1
|
exit 1
|
||||||
|
elif [ "$TRIGGER_K8S_DEPLOY_STATUS" = "failure" ]; then
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Login to Azure - CI subscription
|
- name: Login to Azure - CI subscription
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net6.0</TargetFramework>
|
<TargetFramework>net6.0</TargetFramework>
|
||||||
<Version>2023.12.1</Version>
|
<Version>2024.1.0</Version>
|
||||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
<IncludeSourceRevisionInInformationalVersion>false</IncludeSourceRevisionInInformationalVersion>
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Bit.Core;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Business.Provider;
|
using Bit.Core.AdminConsole.Models.Business.Provider;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
@ -33,13 +36,14 @@ public class ProviderService : IProviderService
|
|||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly IOrganizationService _organizationService;
|
private readonly IOrganizationService _organizationService;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
|
private readonly IStripeAdapter _stripeAdapter;
|
||||||
|
|
||||||
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
|
public ProviderService(IProviderRepository providerRepository, IProviderUserRepository providerUserRepository,
|
||||||
IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,
|
IProviderOrganizationRepository providerOrganizationRepository, IUserRepository userRepository,
|
||||||
IUserService userService, IOrganizationService organizationService, IMailService mailService,
|
IUserService userService, IOrganizationService organizationService, IMailService mailService,
|
||||||
IDataProtectionProvider dataProtectionProvider, IEventService eventService,
|
IDataProtectionProvider dataProtectionProvider, IEventService eventService,
|
||||||
IOrganizationRepository organizationRepository, GlobalSettings globalSettings,
|
IOrganizationRepository organizationRepository, GlobalSettings globalSettings,
|
||||||
ICurrentContext currentContext)
|
ICurrentContext currentContext, IStripeAdapter stripeAdapter)
|
||||||
{
|
{
|
||||||
_providerRepository = providerRepository;
|
_providerRepository = providerRepository;
|
||||||
_providerUserRepository = providerUserRepository;
|
_providerUserRepository = providerUserRepository;
|
||||||
@ -53,6 +57,7 @@ public class ProviderService : IProviderService
|
|||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_dataProtector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
|
_dataProtector = dataProtectionProvider.CreateProtector("ProviderServiceDataProtector");
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
|
_stripeAdapter = stripeAdapter;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key)
|
public async Task<Provider> CompleteSetupAsync(Provider provider, Guid ownerUserId, string token, string key)
|
||||||
@ -369,6 +374,7 @@ public class ProviderService : IProviderService
|
|||||||
Key = key,
|
Key = key,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
await ApplyProviderPriceRateAsync(organizationId, providerId);
|
||||||
await _providerOrganizationRepository.CreateAsync(providerOrganization);
|
await _providerOrganizationRepository.CreateAsync(providerOrganization);
|
||||||
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Added);
|
await _eventService.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Added);
|
||||||
}
|
}
|
||||||
@ -381,18 +387,110 @@ public class ProviderService : IProviderService
|
|||||||
throw new BadRequestException("Provider must be of type Reseller in order to assign Organizations to it.");
|
throw new BadRequestException("Provider must be of type Reseller in order to assign Organizations to it.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var existingProviderOrganizationsCount = await _providerOrganizationRepository.GetCountByOrganizationIdsAsync(organizationIds);
|
var orgIdsList = organizationIds.ToList();
|
||||||
|
var existingProviderOrganizationsCount = await _providerOrganizationRepository.GetCountByOrganizationIdsAsync(orgIdsList);
|
||||||
if (existingProviderOrganizationsCount > 0)
|
if (existingProviderOrganizationsCount > 0)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Organizations must not be assigned to any Provider.");
|
throw new BadRequestException("Organizations must not be assigned to any Provider.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var providerOrganizationsToInsert = organizationIds.Select(orgId => new ProviderOrganization { ProviderId = providerId, OrganizationId = orgId });
|
var providerOrganizationsToInsert = orgIdsList.Select(orgId => new ProviderOrganization { ProviderId = providerId, OrganizationId = orgId });
|
||||||
var insertedProviderOrganizations = await _providerOrganizationRepository.CreateManyAsync(providerOrganizationsToInsert);
|
var insertedProviderOrganizations = await _providerOrganizationRepository.CreateManyAsync(providerOrganizationsToInsert);
|
||||||
|
|
||||||
await _eventService.LogProviderOrganizationEventsAsync(insertedProviderOrganizations.Select(ipo => (ipo, EventType.ProviderOrganization_Added, (DateTime?)null)));
|
await _eventService.LogProviderOrganizationEventsAsync(insertedProviderOrganizations.Select(ipo => (ipo, EventType.ProviderOrganization_Added, (DateTime?)null)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task ApplyProviderPriceRateAsync(Guid organizationId, Guid providerId)
|
||||||
|
{
|
||||||
|
var provider = await _providerRepository.GetByIdAsync(providerId);
|
||||||
|
// if a provider was created before Nov 6, 2023.If true, the organization plan assigned to that provider is updated to a 2020 plan.
|
||||||
|
if (provider.CreationDate >= Constants.ProviderCreatedPriorNov62023)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
var subscriptionItem = await GetSubscriptionItemAsync(organization.GatewaySubscriptionId, GetStripeSeatPlanId(organization.PlanType));
|
||||||
|
var extractedPlanType = PlanTypeMappings(organization);
|
||||||
|
if (subscriptionItem != null)
|
||||||
|
{
|
||||||
|
await UpdateSubscriptionAsync(subscriptionItem, GetStripeSeatPlanId(extractedPlanType), organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _organizationRepository.UpsertAsync(organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Stripe.SubscriptionItem> GetSubscriptionItemAsync(string subscriptionId, string oldPlanId)
|
||||||
|
{
|
||||||
|
var subscriptionDetails = await _stripeAdapter.SubscriptionGetAsync(subscriptionId);
|
||||||
|
return subscriptionDetails.Items.Data.FirstOrDefault(item => item.Price.Id == oldPlanId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetStripeSeatPlanId(PlanType planType)
|
||||||
|
{
|
||||||
|
return StaticStore.GetPlan(planType).PasswordManager.StripeSeatPlanId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdateSubscriptionAsync(Stripe.SubscriptionItem subscriptionItem, string extractedPlanType, Organization organization)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (subscriptionItem.Price.Id != extractedPlanType)
|
||||||
|
{
|
||||||
|
await _stripeAdapter.SubscriptionUpdateAsync(subscriptionItem.Subscription,
|
||||||
|
new Stripe.SubscriptionUpdateOptions
|
||||||
|
{
|
||||||
|
Items = new List<Stripe.SubscriptionItemOptions>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = subscriptionItem.Id,
|
||||||
|
Price = extractedPlanType,
|
||||||
|
Quantity = organization.Seats.Value,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
throw new Exception("Unable to update existing plan on stripe");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlanType PlanTypeMappings(Organization organization)
|
||||||
|
{
|
||||||
|
var planTypeMappings = new Dictionary<PlanType, string>
|
||||||
|
{
|
||||||
|
{ PlanType.EnterpriseAnnually2020, GetEnumDisplayName(PlanType.EnterpriseAnnually2020) },
|
||||||
|
{ PlanType.EnterpriseMonthly2020, GetEnumDisplayName(PlanType.EnterpriseMonthly2020) },
|
||||||
|
{ PlanType.TeamsMonthly2020, GetEnumDisplayName(PlanType.TeamsMonthly2020) },
|
||||||
|
{ PlanType.TeamsAnnually2020, GetEnumDisplayName(PlanType.TeamsAnnually2020) }
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var mapping in planTypeMappings)
|
||||||
|
{
|
||||||
|
if (mapping.Value.IndexOf(organization.Plan, StringComparison.Ordinal) != -1)
|
||||||
|
{
|
||||||
|
organization.PlanType = mapping.Key;
|
||||||
|
organization.Plan = mapping.Value;
|
||||||
|
return organization.PlanType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ArgumentException("Invalid PlanType selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetEnumDisplayName(Enum value)
|
||||||
|
{
|
||||||
|
var fieldInfo = value.GetType().GetField(value.ToString());
|
||||||
|
|
||||||
|
var displayAttribute = (DisplayAttribute)Attribute.GetCustomAttribute(fieldInfo!, typeof(DisplayAttribute));
|
||||||
|
|
||||||
|
return displayAttribute?.Name ?? value.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<ProviderOrganization> CreateOrganizationAsync(Guid providerId,
|
public async Task<ProviderOrganization> CreateOrganizationAsync(Guid providerId,
|
||||||
OrganizationSignup organizationSignup, string clientOwnerEmail, User user)
|
OrganizationSignup organizationSignup, string clientOwnerEmail, User user)
|
||||||
{
|
{
|
||||||
|
@ -18,6 +18,7 @@ using Bit.Test.Common.AutoFixture.Attributes;
|
|||||||
using Microsoft.AspNetCore.DataProtection;
|
using Microsoft.AspNetCore.DataProtection;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using NSubstitute.ReturnsExtensions;
|
using NSubstitute.ReturnsExtensions;
|
||||||
|
using Stripe;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Provider = Bit.Core.AdminConsole.Entities.Provider.Provider;
|
using Provider = Bit.Core.AdminConsole.Entities.Provider.Provider;
|
||||||
using ProviderUser = Bit.Core.AdminConsole.Entities.Provider.ProviderUser;
|
using ProviderUser = Bit.Core.AdminConsole.Entities.Provider.ProviderUser;
|
||||||
@ -598,4 +599,98 @@ public class ProviderServiceTests
|
|||||||
await sutProvider.GetDependency<IEventService>().Received()
|
await sutProvider.GetDependency<IEventService>().Received()
|
||||||
.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed);
|
.LogProviderOrganizationEventAsync(providerOrganization, EventType.ProviderOrganization_Removed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task AddOrganization_CreateAfterNov162023_PlanTypeDoesNotUpdated(Provider provider, Organization organization, string key,
|
||||||
|
SutProvider<ProviderService> sutProvider)
|
||||||
|
{
|
||||||
|
provider.Type = ProviderType.Msp;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||||
|
|
||||||
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
|
var expectedPlanType = PlanType.EnterpriseAnnually;
|
||||||
|
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
|
||||||
|
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
|
||||||
|
|
||||||
|
await providerOrganizationRepository.ReceivedWithAnyArgs().CreateAsync(default);
|
||||||
|
await sutProvider.GetDependency<IEventService>()
|
||||||
|
.Received().LogProviderOrganizationEventAsync(Arg.Any<ProviderOrganization>(),
|
||||||
|
EventType.ProviderOrganization_Added);
|
||||||
|
Assert.Equal(organization.PlanType, expectedPlanType);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task AddOrganization_CreateBeforeNov162023_PlanTypeUpdated(Provider provider, Organization organization, string key,
|
||||||
|
SutProvider<ProviderService> sutProvider)
|
||||||
|
{
|
||||||
|
var newCreationDate = DateTime.UtcNow.AddMonths(-3);
|
||||||
|
BackdateProviderCreationDate(provider, newCreationDate);
|
||||||
|
provider.Type = ProviderType.Msp;
|
||||||
|
|
||||||
|
organization.PlanType = PlanType.EnterpriseAnnually;
|
||||||
|
organization.Plan = "Enterprise (Annually)";
|
||||||
|
|
||||||
|
var expectedPlanType = PlanType.EnterpriseAnnually2020;
|
||||||
|
|
||||||
|
var expectedPlanId = "2020-enterprise-org-seat-annually";
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IProviderRepository>().GetByIdAsync(provider.Id).Returns(provider);
|
||||||
|
var providerOrganizationRepository = sutProvider.GetDependency<IProviderOrganizationRepository>();
|
||||||
|
providerOrganizationRepository.GetByOrganizationId(organization.Id).ReturnsNull();
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
var subscriptionItem = GetSubscription(organization.GatewaySubscriptionId);
|
||||||
|
sutProvider.GetDependency<IStripeAdapter>().SubscriptionGetAsync(organization.GatewaySubscriptionId)
|
||||||
|
.Returns(GetSubscription(organization.GatewaySubscriptionId));
|
||||||
|
await sutProvider.GetDependency<IStripeAdapter>().SubscriptionUpdateAsync(
|
||||||
|
organization.GatewaySubscriptionId, SubscriptionUpdateRequest(expectedPlanId, subscriptionItem));
|
||||||
|
|
||||||
|
await sutProvider.Sut.AddOrganization(provider.Id, organization.Id, key);
|
||||||
|
|
||||||
|
await providerOrganizationRepository.ReceivedWithAnyArgs().CreateAsync(default);
|
||||||
|
await sutProvider.GetDependency<IEventService>()
|
||||||
|
.Received().LogProviderOrganizationEventAsync(Arg.Any<ProviderOrganization>(),
|
||||||
|
EventType.ProviderOrganization_Added);
|
||||||
|
|
||||||
|
Assert.Equal(organization.PlanType, expectedPlanType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SubscriptionUpdateOptions SubscriptionUpdateRequest(string expectedPlanId, Subscription subscriptionItem) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Items = new List<Stripe.SubscriptionItemOptions>
|
||||||
|
{
|
||||||
|
new() { Id = subscriptionItem.Id, Price = expectedPlanId },
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static Subscription GetSubscription(string subscriptionId) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = subscriptionId,
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Id = "sub_item_123",
|
||||||
|
Price = new Price()
|
||||||
|
{
|
||||||
|
Id = "2023-enterprise-org-seat-annually"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static void BackdateProviderCreationDate(Provider provider, DateTime newCreationDate)
|
||||||
|
{
|
||||||
|
// Set the CreationDate to the desired value
|
||||||
|
provider.GetType().GetProperty("CreationDate")?.SetValue(provider, newCreationDate, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,12 +6,12 @@
|
|||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script>
|
<script>
|
||||||
function onRowSelect(selectingPage = false) {
|
function onRowSelect(selectingPage = false) {
|
||||||
var checkboxes = document.getElementsByClassName('row-check');
|
let checkboxes = document.getElementsByClassName('row-check');
|
||||||
var checkedCheckboxCount = 0;
|
let checkedCheckboxCount = 0;
|
||||||
var bulkActions = document.getElementById('bulkActions');
|
let bulkActions = document.getElementById('bulkActions');
|
||||||
|
|
||||||
var selectPage = document.getElementById('selectPage');
|
let selectPage = document.getElementById('selectPage');
|
||||||
for(var i = 0; i < checkboxes.length; i++){
|
for(let i = 0; i < checkboxes.length; i++){
|
||||||
if((checkboxes[i].checked && !selectingPage) || selectingPage && selectPage.checked) {
|
if((checkboxes[i].checked && !selectingPage) || selectingPage && selectPage.checked) {
|
||||||
checkboxes[i].checked = true;
|
checkboxes[i].checked = true;
|
||||||
checkedCheckboxCount += 1;
|
checkedCheckboxCount += 1;
|
||||||
@ -26,40 +26,39 @@
|
|||||||
bulkActions.classList.add("d-none");
|
bulkActions.classList.add("d-none");
|
||||||
}
|
}
|
||||||
|
|
||||||
var selectPage = document.getElementById('selectPage');
|
let selectAll = document.getElementById('selectAll');
|
||||||
var selectAll = document.getElementById('selectAll');
|
if (checkedCheckboxCount === checkboxes.length) {
|
||||||
if (checkedCheckboxCount == checkboxes.length) {
|
|
||||||
selectPage.checked = true;
|
selectPage.checked = true;
|
||||||
selectAll.classList.remove("d-none");
|
selectAll.classList.remove("d-none");
|
||||||
|
|
||||||
var selectAllElement = document.getElementById('selectAllElement');
|
let selectAllElement = document.getElementById('selectAllElement');
|
||||||
selectAllElement.classList.remove('d-none');
|
selectAllElement.classList.remove('d-none');
|
||||||
|
|
||||||
var selectedAllConfirmation = document.getElementById('selectedAllConfirmation');
|
let selectedAllConfirmation = document.getElementById('selectedAllConfirmation');
|
||||||
selectedAllConfirmation.classList.add('d-none');
|
selectedAllConfirmation.classList.add('d-none');
|
||||||
} else {
|
} else {
|
||||||
selectPage.checked = false;
|
selectPage.checked = false;
|
||||||
selectAll.classList.add("d-none");
|
selectAll.classList.add("d-none");
|
||||||
var selectAllInput = document.getElementById('selectAllInput');
|
let selectAllInput = document.getElementById('selectAllInput');
|
||||||
selectAllInput.checked = false;
|
selectAllInput.checked = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSelectAll() {
|
function onSelectAll() {
|
||||||
var selectAllInput = document.getElementById('selectAllInput');
|
let selectAllInput = document.getElementById('selectAllInput');
|
||||||
selectAllInput.checked = true;
|
selectAllInput.checked = true;
|
||||||
|
|
||||||
var selectAllElement = document.getElementById('selectAllElement');
|
let selectAllElement = document.getElementById('selectAllElement');
|
||||||
selectAllElement.classList.add('d-none');
|
selectAllElement.classList.add('d-none');
|
||||||
|
|
||||||
var selectedAllConfirmation = document.getElementById('selectedAllConfirmation');
|
let selectedAllConfirmation = document.getElementById('selectedAllConfirmation');
|
||||||
selectedAllConfirmation.classList.remove('d-none');
|
selectedAllConfirmation.classList.remove('d-none');
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportSelectedSubscriptions() {
|
function exportSelectedSubscriptions() {
|
||||||
var selectAll = document.getElementById('selectAll');
|
let selectAll = document.getElementById('selectAll');
|
||||||
var httpRequest = new XMLHttpRequest();
|
let httpRequest = new XMLHttpRequest();
|
||||||
httpRequest.open('POST');
|
httpRequest.open("POST");
|
||||||
httpRequest.send();
|
httpRequest.send();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,200 +73,209 @@
|
|||||||
{
|
{
|
||||||
<div class="alert alert-success"></div>
|
<div class="alert alert-success"></div>
|
||||||
}
|
}
|
||||||
<form method="post">
|
<form method="post">
|
||||||
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
<div asp-validation-summary="All" class="alert alert-danger"></div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<label asp-for="Filter.Status">Status</label>
|
<label asp-for="Filter.Status">Status</label>
|
||||||
<select asp-for="Filter.Status" name="filter.Status" class="form-control mr-2">
|
<select asp-for="Filter.Status" name="filter.Status" class="form-control mr-2">
|
||||||
<option asp-selected="Model.Filter.Status == null" value="all">All</option>
|
<option asp-selected="Model.Filter.Status == null" value="all">All</option>
|
||||||
<option asp-selected='Model.Filter.Status == "active"' value="active">Active</option>
|
<option asp-selected='Model.Filter.Status == "active"' value="active">Active</option>
|
||||||
<option asp-selected='Model.Filter.Status == "unpaid"' value="unpaid">Unpaid</option>
|
<option asp-selected='Model.Filter.Status == "unpaid"' value="unpaid">Unpaid</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6">
|
<div class="col-6">
|
||||||
<label asp-for="Filter.CurrentPeriodEnd">Current Period End</label>
|
<label asp-for="Filter.CurrentPeriodEnd">Current Period End</label>
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<div class="input-group-text">
|
<div class="input-group-text">
|
||||||
<span class="mr-1">
|
<span class="mr-1">
|
||||||
<input type="radio" class="mr-1" asp-for="Filter.CurrentPeriodEndRange" name="filter.CurrentPeriodEndRange" value="lt">Before
|
<input type="radio" class="mr-1" asp-for="Filter.CurrentPeriodEndRange" name="filter.CurrentPeriodEndRange" value="lt">Before
|
||||||
</span>
|
</span>
|
||||||
<input type="radio" asp-for="Filter.CurrentPeriodEndRange" name="filter.CurrentPeriodEndRange" value="gt">After
|
<input type="radio" asp-for="Filter.CurrentPeriodEndRange" name="filter.CurrentPeriodEndRange" value="gt">After
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@{
|
|
||||||
var date = @Model.Filter.CurrentPeriodEndDate.HasValue ? @Model.Filter.CurrentPeriodEndDate.Value.ToString("yyyy-MM-dd") : string.Empty;
|
|
||||||
<input type="date" class="form-control" asp-for="Filter.CurrentPeriodEndDate" name="filter.CurrentPeriodEndDate" value="@date">
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
@{
|
||||||
<div class="row mt-2">
|
var date = @Model.Filter.CurrentPeriodEndDate.HasValue ? @Model.Filter.CurrentPeriodEndDate.Value.ToString("yyyy-MM-dd") : string.Empty;
|
||||||
<div class="col-6">
|
<input type="date" class="form-control" asp-for="Filter.CurrentPeriodEndDate" name="filter.CurrentPeriodEndDate" value="@date">
|
||||||
<label asp-for="Filter.Price">Price ID</label>
|
}
|
||||||
<select asp-for="Filter.Price" name="filter.Price" class="form-control mr-2">
|
|
||||||
<option asp-selected="Model.Filter.Price == null" value="@null">All</option>
|
|
||||||
@foreach (var price in Model.Prices)
|
|
||||||
{<option asp-selected='@Model.Filter.Price == @price.Id' value="@price.Id">@price.Id</option>}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="col-6">
|
|
||||||
<label asp-for="Filter.TestClock">Test Clock</label>
|
|
||||||
<select asp-for="Filter.TestClock" name="filter.TestClock" class="form-control mr-2">
|
|
||||||
<option asp-selected="Model.Filter.TestClock == null" value="@null">All</option>
|
|
||||||
@foreach (var clock in Model.TestClocks)
|
|
||||||
{<option asp-selected='@Model.Filter.TestClock == @clock.Id' value="@clock.Id">@clock.Name</option>}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row col-12 d-flex justify-content-end my-3">
|
|
||||||
<button type="submit" class="btn btn-primary" title="Search" name="action" asp-for="Action" value="@StripeSubscriptionsAction.Search"><i class="fa fa-search"></i> Search</button>
|
|
||||||
</div>
|
|
||||||
<hr/>
|
|
||||||
<input type="checkbox" class="d-none" asp-for="Filter.SelectAll" name="filter.SelectAll" id="selectAllInput" asp-for="Model.Filter.SelectAll">
|
|
||||||
<div class="text-center row d-flex justify-content-center">
|
|
||||||
<div id="selectAll" class="d-none col-8">
|
|
||||||
All @Model.Items.Count subscriptions on this page are selected.<br/>
|
|
||||||
<button type="button" id="selectAllElement" class="btn btn-link p-0 pb-1" onclick="onSelectAll()">Click here to select all subscriptions for this search.</button>
|
|
||||||
<span id="selectedAllConfirmation" class="d-none text-muted">✔ All subscriptions for this search are selected.</span><br/>
|
|
||||||
<div class="alert alert-warning" role="alert">Please be aware that bulk operations may take several minutes to complete.</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="table-responsive">
|
</div>
|
||||||
<table class="table table-striped table-hover">
|
<div class="row mt-2">
|
||||||
<thead>
|
<div class="col-6">
|
||||||
<tr>
|
<label asp-for="Filter.Price">Price ID</label>
|
||||||
<th>
|
<select asp-for="Filter.Price" name="filter.Price" class="form-control mr-2">
|
||||||
<div class="form-check form-check-inline">
|
<option asp-selected="Model.Filter.Price == null" value="@null">All</option>
|
||||||
<input id="selectPage" class="form-check-input" type="checkbox" onchange="onRowSelect(true)">
|
@foreach (var price in Model.Prices)
|
||||||
</div>
|
|
||||||
</th>
|
|
||||||
<th>Id</th>
|
|
||||||
<th>Customer Email</th>
|
|
||||||
<th>Status</th>
|
|
||||||
<th>Product</th>
|
|
||||||
<th>Current Period End</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
@if(!Model.Items.Any())
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<td colspan="6">No results to list.</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
@for(var i = 0; i < Model.Items.Count; i++)
|
|
||||||
{
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<input type="hidden" asp-for="@Model.Items[i].Subscription.Id" value="@Model.Items[i].Subscription.Id">
|
|
||||||
<input type="hidden" asp-for="@Model.Items[i].Subscription.Status" value="@Model.Items[i].Subscription.Status">
|
|
||||||
<input type="hidden" asp-for="@Model.Items[i].Subscription.CurrentPeriodEnd" value="@Model.Items[i].Subscription.CurrentPeriodEnd">
|
|
||||||
<input type="hidden" asp-for="@Model.Items[i].Subscription.Customer.Email" value="@Model.Items[i].Subscription.Customer.Email">
|
|
||||||
<input type="hidden" asp-for="@Model.Items[i].Subscription.LatestInvoice.Status" value="@Model.Items[i].Subscription.LatestInvoice.Status">
|
|
||||||
<input type="hidden" asp-for="@Model.Items[i].Subscription.LatestInvoice.Id" value="@Model.Items[i].Subscription.LatestInvoice.Id">
|
|
||||||
|
|
||||||
@for(var j = 0; j < Model.Items[i].Subscription.Items.Data.Count; j++)
|
|
||||||
{
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
asp-for="@Model.Items[i].Subscription.Items.Data[j].Plan.Id"
|
|
||||||
value="@Model.Items[i].Subscription.Items.Data[j].Plan.Id"
|
|
||||||
>
|
|
||||||
}
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input row-check" onchange="onRowSelect()" asp-for="@Model.Items[i].Selected">
|
|
||||||
</div>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
@Model.Items[i].Subscription.Id
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
@Model.Items[i].Subscription.Customer?.Email
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
@Model.Items[i].Subscription.Status
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
@string.Join(",", Model.Items[i].Subscription.Items.Data.Select(product => product.Plan.Id).ToArray())
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
@Model.Items[i].Subscription.CurrentPeriodEnd.ToShortDateString()
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<nav class="d-inline-flex">
|
|
||||||
<ul class="pagination">
|
|
||||||
@if(!string.IsNullOrWhiteSpace(Model.Filter.EndingBefore))
|
|
||||||
{
|
{
|
||||||
<input type="hidden" asp-for="@Model.Filter.EndingBefore" value="@Model.Filter.EndingBefore">
|
<option asp-selected='@Model.Filter.Price == @price.Id' value="@price.Id">@price.Id</option>
|
||||||
<li class="page-item">
|
}
|
||||||
<button
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<label asp-for="Filter.TestClock">Test Clock</label>
|
||||||
|
<select asp-for="Filter.TestClock" name="filter.TestClock" class="form-control mr-2">
|
||||||
|
<option asp-selected="Model.Filter.TestClock == null" value="@null">All</option>
|
||||||
|
@foreach (var clock in Model.TestClocks)
|
||||||
|
{
|
||||||
|
<option asp-selected='@Model.Filter.TestClock == @clock.Id' value="@clock.Id">@clock.Name</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row col-12 d-flex justify-content-end my-3">
|
||||||
|
<button type="submit" class="btn btn-primary" title="Search" name="action" asp-for="Action" value="@StripeSubscriptionsAction.Search"><i class="fa fa-search"></i> Search</button>
|
||||||
|
</div>
|
||||||
|
<hr/>
|
||||||
|
<input type="checkbox" class="d-none" name="filter.SelectAll" id="selectAllInput" asp-for="@Model.Filter.SelectAll">
|
||||||
|
<div class="text-center row d-flex justify-content-center">
|
||||||
|
<div id="selectAll" class="d-none col-8">
|
||||||
|
All @Model.Items.Count subscriptions on this page are selected.<br/>
|
||||||
|
<button type="button" id="selectAllElement" class="btn btn-link p-0 pb-1" onclick="onSelectAll()">Click here to select all subscriptions for this search.</button>
|
||||||
|
<span id="selectedAllConfirmation" class="d-none text-muted">✔ All subscriptions for this search are selected.</span><br/>
|
||||||
|
<div class="alert alert-warning" role="alert">Please be aware that bulk operations may take several minutes to complete.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input id="selectPage" class="form-check-input" type="checkbox" onchange="onRowSelect(true)">
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
<th>Id</th>
|
||||||
|
<th>Customer Email</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Product</th>
|
||||||
|
<th>Current Period End</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@if (!Model.Items.Any())
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td colspan="6">No results to list.</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
@for (var i = 0; i < Model.Items.Count; i++)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
@{
|
||||||
|
var i0 = i;
|
||||||
|
}
|
||||||
|
<input type="hidden" asp-for="@Model.Items[i0].Subscription.Id" value="@Model.Items[i].Subscription.Id">
|
||||||
|
<input type="hidden" asp-for="@Model.Items[i0].Subscription.Status" value="@Model.Items[i].Subscription.Status">
|
||||||
|
<input type="hidden" asp-for="@Model.Items[i0].Subscription.CurrentPeriodEnd" value="@Model.Items[i].Subscription.CurrentPeriodEnd">
|
||||||
|
<input type="hidden" asp-for="@Model.Items[i0].Subscription.Customer.Email" value="@Model.Items[i].Subscription.Customer.Email">
|
||||||
|
<input type="hidden" asp-for="@Model.Items[i0].Subscription.LatestInvoice.Status" value="@Model.Items[i].Subscription.LatestInvoice.Status">
|
||||||
|
<input type="hidden" asp-for="@Model.Items[i0].Subscription.LatestInvoice.Id" value="@Model.Items[i].Subscription.LatestInvoice.Id">
|
||||||
|
|
||||||
|
@for (var j = 0; j < Model.Items[i].Subscription.Items.Data.Count; j++)
|
||||||
|
{
|
||||||
|
var i1 = i;
|
||||||
|
var j1 = j;
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
asp-for="@Model.Items[i1].Subscription.Items.Data[j1].Plan.Id"
|
||||||
|
value="@Model.Items[i].Subscription.Items.Data[j].Plan.Id">
|
||||||
|
}
|
||||||
|
<div class="form-check">
|
||||||
|
|
||||||
|
@{
|
||||||
|
var i2 = i;
|
||||||
|
}
|
||||||
|
<input class="form-check-input row-check" onchange="onRowSelect()" asp-for="@Model.Items[i2].Selected">
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@Model.Items[i].Subscription.Id
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@Model.Items[i].Subscription.Customer?.Email
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@Model.Items[i].Subscription.Status
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@string.Join(",", Model.Items[i].Subscription.Items.Data.Select(product => product.Plan.Id).ToArray())
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@Model.Items[i].Subscription.CurrentPeriodEnd.ToShortDateString()
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<nav class="d-inline-flex">
|
||||||
|
<ul class="pagination">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Model.Filter.EndingBefore))
|
||||||
|
{
|
||||||
|
<input type="hidden" asp-for="@Model.Filter.EndingBefore" value="@Model.Filter.EndingBefore">
|
||||||
|
<li class="page-item">
|
||||||
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
class="page-link"
|
class="page-link"
|
||||||
name="action"
|
name="action"
|
||||||
asp-for="Action"
|
asp-for="Action"
|
||||||
value="@StripeSubscriptionsAction.PreviousPage"
|
value="@StripeSubscriptionsAction.PreviousPage">
|
||||||
>
|
Previous
|
||||||
Previous
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<li class="page-item disabled">
|
|
||||||
<a class="page-link" href="#" tabindex="-1">Previous</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
@if(!string.IsNullOrWhiteSpace(Model.Filter.StartingAfter))
|
|
||||||
{
|
|
||||||
<input type="hidden" asp-for="@Model.Filter.StartingAfter" value="@Model.Filter.StartingAfter">
|
|
||||||
<li class="page-item">
|
|
||||||
<button class="page-link"
|
|
||||||
type="submit"
|
|
||||||
name="action"
|
|
||||||
asp-for="Action"
|
|
||||||
value="@StripeSubscriptionsAction.NextPage"
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<li class="page-item disabled">
|
|
||||||
<a class="page-link" href="#" tabindex="-1">Next</a>
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
</ul>
|
|
||||||
<span id="bulkActions" class="d-none ml-2">
|
|
||||||
<span class="d-inline-flex">
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="btn btn-primary mr-1"
|
|
||||||
name="action"
|
|
||||||
asp-for="Action"
|
|
||||||
value="@StripeSubscriptionsAction.Export"
|
|
||||||
>
|
|
||||||
Export
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
</li>
|
||||||
type="submit"
|
}
|
||||||
class="btn btn-danger"
|
else
|
||||||
name="action"
|
{
|
||||||
asp-for="Action"
|
<li class="page-item disabled">
|
||||||
value="@StripeSubscriptionsAction.BulkCancel"
|
<a class="page-link" href="#" tabindex="-1">Previous</a>
|
||||||
>
|
</li>
|
||||||
Bulk Cancel
|
}
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Model.Filter.StartingAfter))
|
||||||
|
{
|
||||||
|
<input type="hidden" asp-for="@Model.Filter.StartingAfter" value="@Model.Filter.StartingAfter">
|
||||||
|
<li class="page-item">
|
||||||
|
<button class="page-link"
|
||||||
|
type="submit"
|
||||||
|
name="action"
|
||||||
|
asp-for="Action"
|
||||||
|
value="@StripeSubscriptionsAction.NextPage">
|
||||||
|
Next
|
||||||
</button>
|
</button>
|
||||||
</span>
|
</li>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<li class="page-item disabled">
|
||||||
|
<a class="page-link" href="#" tabindex="-1">Next</a>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
<span id="bulkActions" class="d-none ml-2">
|
||||||
|
<span class="d-inline-flex">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary mr-1"
|
||||||
|
name="action"
|
||||||
|
asp-for="Action"
|
||||||
|
value="@StripeSubscriptionsAction.Export">
|
||||||
|
Export
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-danger"
|
||||||
|
name="action"
|
||||||
|
asp-for="Action"
|
||||||
|
value="@StripeSubscriptionsAction.BulkCancel">
|
||||||
|
Bulk Cancel
|
||||||
|
</button>
|
||||||
</span>
|
</span>
|
||||||
</nav>
|
</span>
|
||||||
</form>
|
</nav>
|
||||||
|
</form>
|
@ -21,6 +21,7 @@ public class ProviderResponseModel : ResponseModel
|
|||||||
BusinessCountry = provider.BusinessCountry;
|
BusinessCountry = provider.BusinessCountry;
|
||||||
BusinessTaxNumber = provider.BusinessTaxNumber;
|
BusinessTaxNumber = provider.BusinessTaxNumber;
|
||||||
BillingEmail = provider.BillingEmail;
|
BillingEmail = provider.BillingEmail;
|
||||||
|
CreationDate = provider.CreationDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
@ -32,4 +33,5 @@ public class ProviderResponseModel : ResponseModel
|
|||||||
public string BusinessCountry { get; set; }
|
public string BusinessCountry { get; set; }
|
||||||
public string BusinessTaxNumber { get; set; }
|
public string BusinessTaxNumber { get; set; }
|
||||||
public string BillingEmail { get; set; }
|
public string BillingEmail { get; set; }
|
||||||
|
public DateTime CreationDate { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace Bit.Api.Auth.Models.Public;
|
namespace Bit.Api.AdminConsole.Public.Models;
|
||||||
|
|
||||||
public abstract class AssociationWithPermissionsBaseModel
|
public abstract class AssociationWithPermissionsBaseModel
|
||||||
{
|
{
|
@ -1,6 +1,6 @@
|
|||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
|
|
||||||
namespace Bit.Api.Auth.Models.Public.Request;
|
namespace Bit.Api.AdminConsole.Public.Models.Request;
|
||||||
|
|
||||||
public class AssociationWithPermissionsRequestModel : AssociationWithPermissionsBaseModel
|
public class AssociationWithPermissionsRequestModel : AssociationWithPermissionsBaseModel
|
||||||
{
|
{
|
@ -1,5 +1,4 @@
|
|||||||
using Bit.Api.Auth.Models.Public.Request;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
|
||||||
|
|
||||||
namespace Bit.Api.AdminConsole.Public.Models.Request;
|
namespace Bit.Api.AdminConsole.Public.Models.Request;
|
||||||
|
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using Bit.Api.Auth.Models.Public.Request;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Entities;
|
|
||||||
|
|
||||||
namespace Bit.Api.AdminConsole.Public.Models.Request;
|
namespace Bit.Api.AdminConsole.Public.Models.Request;
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
|
|
||||||
namespace Bit.Api.Auth.Models.Public.Response;
|
namespace Bit.Api.AdminConsole.Public.Models.Response;
|
||||||
|
|
||||||
public class AssociationWithPermissionsResponseModel : AssociationWithPermissionsBaseModel
|
public class AssociationWithPermissionsResponseModel : AssociationWithPermissionsBaseModel
|
||||||
{
|
{
|
@ -1,5 +1,4 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Api.Auth.Models.Public.Response;
|
|
||||||
using Bit.Api.Models.Public.Response;
|
using Bit.Api.Models.Public.Response;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Api.Auth.Models.Public.Response;
|
|
||||||
using Bit.Api.Models.Public.Response;
|
using Bit.Api.Models.Public.Response;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
|
100
src/Api/Billing/Public/Controllers/OrganizationController.cs
Normal file
100
src/Api/Billing/Public/Controllers/OrganizationController.cs
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
using System.Net;
|
||||||
|
using Bit.Api.Models.Public.Response;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.OrganizationFeatures.OrganizationSubscriptions.Interface;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using OrganizationSubscriptionUpdateRequestModel = Bit.Api.Billing.Public.Models.OrganizationSubscriptionUpdateRequestModel;
|
||||||
|
|
||||||
|
namespace Bit.Api.Billing.Public.Controllers;
|
||||||
|
|
||||||
|
[Route("public/organization")]
|
||||||
|
[Authorize("Organization")]
|
||||||
|
public class OrganizationController : Controller
|
||||||
|
{
|
||||||
|
private readonly IOrganizationService _organizationService;
|
||||||
|
private readonly ICurrentContext _currentContext;
|
||||||
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
|
||||||
|
|
||||||
|
public OrganizationController(
|
||||||
|
IOrganizationService organizationService,
|
||||||
|
ICurrentContext currentContext,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand)
|
||||||
|
{
|
||||||
|
_organizationService = organizationService;
|
||||||
|
_currentContext = currentContext;
|
||||||
|
_organizationRepository = organizationRepository;
|
||||||
|
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Update the organization's current subscription for Password Manager and/or Secrets Manager.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="model">The request model containing the updated subscription information.</param>
|
||||||
|
[HttpPut("subscription")]
|
||||||
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
|
[ProducesResponseType((int)HttpStatusCode.OK)]
|
||||||
|
[ProducesResponseType(typeof(ErrorResponseModel), (int)HttpStatusCode.BadRequest)]
|
||||||
|
[ProducesResponseType((int)HttpStatusCode.NotFound)]
|
||||||
|
public async Task<IActionResult> PostSubscriptionAsync([FromBody] OrganizationSubscriptionUpdateRequestModel model)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await UpdatePasswordManagerAsync(model, _currentContext.OrganizationId.Value);
|
||||||
|
|
||||||
|
var secretsManagerResult = await UpdateSecretsManagerAsync(model, _currentContext.OrganizationId.Value);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(secretsManagerResult))
|
||||||
|
{
|
||||||
|
return Ok(new { Message = secretsManagerResult });
|
||||||
|
}
|
||||||
|
|
||||||
|
return Ok(new { Message = "Subscription updated successfully." });
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return StatusCode(500, new { Message = "An error occurred while updating the subscription." });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task UpdatePasswordManagerAsync(OrganizationSubscriptionUpdateRequestModel model, Guid organizationId)
|
||||||
|
{
|
||||||
|
if (model.PasswordManager != null)
|
||||||
|
{
|
||||||
|
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
|
||||||
|
model.PasswordManager.ToPasswordManagerSubscriptionUpdate(organization);
|
||||||
|
await _organizationService.UpdateSubscription(organization.Id, (int)model.PasswordManager.Seats,
|
||||||
|
model.PasswordManager.MaxAutoScaleSeats);
|
||||||
|
if (model.PasswordManager.Storage.HasValue)
|
||||||
|
{
|
||||||
|
await _organizationService.AdjustStorageAsync(organization.Id, (short)model.PasswordManager.Storage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<string> UpdateSecretsManagerAsync(OrganizationSubscriptionUpdateRequestModel model, Guid organizationId)
|
||||||
|
{
|
||||||
|
if (model.SecretsManager == null)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
|
||||||
|
if (!organization.UseSecretsManager)
|
||||||
|
{
|
||||||
|
return "Organization has no access to Secrets Manager.";
|
||||||
|
}
|
||||||
|
|
||||||
|
var secretsManagerUpdate = model.SecretsManager.ToSecretsManagerSubscriptionUpdate(organization);
|
||||||
|
await _updateSecretsManagerSubscriptionCommand.UpdateSubscriptionAsync(secretsManagerUpdate);
|
||||||
|
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,138 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
|
|
||||||
|
namespace Bit.Api.Billing.Public.Models;
|
||||||
|
|
||||||
|
public class OrganizationSubscriptionUpdateRequestModel : IValidatableObject
|
||||||
|
{
|
||||||
|
public PasswordManagerSubscriptionUpdateModel PasswordManager { get; set; }
|
||||||
|
public SecretsManagerSubscriptionUpdateModel SecretsManager { get; set; }
|
||||||
|
|
||||||
|
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
|
||||||
|
{
|
||||||
|
if (PasswordManager == null && SecretsManager == null)
|
||||||
|
{
|
||||||
|
yield return new ValidationResult("At least one of PasswordManager or SecretsManager must be provided.");
|
||||||
|
}
|
||||||
|
|
||||||
|
yield return ValidationResult.Success;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PasswordManagerSubscriptionUpdateModel
|
||||||
|
{
|
||||||
|
public int? Seats { get; set; }
|
||||||
|
public int? Storage { get; set; }
|
||||||
|
private int? _maxAutoScaleSeats;
|
||||||
|
public int? MaxAutoScaleSeats
|
||||||
|
{
|
||||||
|
get { return _maxAutoScaleSeats; }
|
||||||
|
set { _maxAutoScaleSeats = value < 0 ? null : value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void ToPasswordManagerSubscriptionUpdate(Organization organization)
|
||||||
|
{
|
||||||
|
UpdateMaxAutoScaleSeats(organization);
|
||||||
|
|
||||||
|
UpdateSeats(organization);
|
||||||
|
|
||||||
|
UpdateStorage(organization);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateMaxAutoScaleSeats(Organization organization)
|
||||||
|
{
|
||||||
|
MaxAutoScaleSeats ??= organization.MaxAutoscaleSeats;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSeats(Organization organization)
|
||||||
|
{
|
||||||
|
if (Seats is > 0)
|
||||||
|
{
|
||||||
|
if (organization.Seats.HasValue)
|
||||||
|
{
|
||||||
|
Seats = Seats.Value - organization.Seats.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Seats = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateStorage(Organization organization)
|
||||||
|
{
|
||||||
|
if (Storage is > 0)
|
||||||
|
{
|
||||||
|
if (organization.MaxStorageGb.HasValue)
|
||||||
|
{
|
||||||
|
Storage = (short?)(Storage - organization.MaxStorageGb.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Storage = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SecretsManagerSubscriptionUpdateModel
|
||||||
|
{
|
||||||
|
public int? Seats { get; set; }
|
||||||
|
private int? _maxAutoScaleSeats;
|
||||||
|
public int? MaxAutoScaleSeats
|
||||||
|
{
|
||||||
|
get { return _maxAutoScaleSeats; }
|
||||||
|
set { _maxAutoScaleSeats = value < 0 ? null : value; }
|
||||||
|
}
|
||||||
|
public int? ServiceAccounts { get; set; }
|
||||||
|
private int? _maxAutoScaleServiceAccounts;
|
||||||
|
public int? MaxAutoScaleServiceAccounts
|
||||||
|
{
|
||||||
|
get { return _maxAutoScaleServiceAccounts; }
|
||||||
|
set { _maxAutoScaleServiceAccounts = value < 0 ? null : value; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual SecretsManagerSubscriptionUpdate ToSecretsManagerSubscriptionUpdate(Organization organization)
|
||||||
|
{
|
||||||
|
var update = UpdateUpdateMaxAutoScale(organization);
|
||||||
|
UpdateSeats(organization, update);
|
||||||
|
UpdateServiceAccounts(organization, update);
|
||||||
|
return update;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SecretsManagerSubscriptionUpdate UpdateUpdateMaxAutoScale(Organization organization)
|
||||||
|
{
|
||||||
|
var update = new SecretsManagerSubscriptionUpdate(organization, false)
|
||||||
|
{
|
||||||
|
MaxAutoscaleSmSeats = MaxAutoScaleSeats ?? organization.MaxAutoscaleSmSeats,
|
||||||
|
MaxAutoscaleSmServiceAccounts = MaxAutoScaleServiceAccounts ?? organization.MaxAutoscaleSmServiceAccounts
|
||||||
|
};
|
||||||
|
return update;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateSeats(Organization organization, SecretsManagerSubscriptionUpdate update)
|
||||||
|
{
|
||||||
|
if (Seats is > 0)
|
||||||
|
{
|
||||||
|
if (organization.SmSeats.HasValue)
|
||||||
|
{
|
||||||
|
Seats = Seats.Value - organization.SmSeats.Value;
|
||||||
|
|
||||||
|
}
|
||||||
|
update.AdjustSeats(Seats.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateServiceAccounts(Organization organization, SecretsManagerSubscriptionUpdate update)
|
||||||
|
{
|
||||||
|
if (ServiceAccounts is > 0)
|
||||||
|
{
|
||||||
|
if (organization.SmServiceAccounts.HasValue)
|
||||||
|
{
|
||||||
|
ServiceAccounts = ServiceAccounts.Value - organization.SmServiceAccounts.Value;
|
||||||
|
}
|
||||||
|
update.AdjustServiceAccounts(ServiceAccounts.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -584,11 +584,6 @@ public class CollectionsController : Controller
|
|||||||
|
|
||||||
// Filter the assigned collections to only return those where the user has Manage permission
|
// Filter the assigned collections to only return those where the user has Manage permission
|
||||||
var manageableOrgCollections = assignedOrgCollections.Where(c => c.Item1.Manage).ToList();
|
var manageableOrgCollections = assignedOrgCollections.Where(c => c.Item1.Manage).ToList();
|
||||||
var readAssignedAuthorized = await _authorizationService.AuthorizeAsync(User, manageableOrgCollections.Select(c => c.Item1), BulkCollectionOperations.ReadWithAccess);
|
|
||||||
if (!readAssignedAuthorized.Succeeded)
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
return new ListResponseModel<CollectionAccessDetailsResponseModel>(manageableOrgCollections.Select(c =>
|
return new ListResponseModel<CollectionAccessDetailsResponseModel>(manageableOrgCollections.Select(c =>
|
||||||
new CollectionAccessDetailsResponseModel(c.Item1, c.Item2.Groups, c.Item2.Users)
|
new CollectionAccessDetailsResponseModel(c.Item1, c.Item2.Groups, c.Item2.Users)
|
||||||
@ -609,16 +604,8 @@ public class CollectionsController : Controller
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var collections = await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId.Value, FlexibleCollectionsIsEnabled);
|
var assignedCollections = await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId.Value, FlexibleCollectionsIsEnabled);
|
||||||
var readAuthorized = (await _authorizationService.AuthorizeAsync(User, collections, BulkCollectionOperations.Read)).Succeeded;
|
orgCollections = assignedCollections.Where(c => c.OrganizationId == orgId && c.Manage).ToList();
|
||||||
if (readAuthorized)
|
|
||||||
{
|
|
||||||
orgCollections = collections.Where(c => c.OrganizationId == orgId);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var responses = orgCollections.Select(c => new CollectionResponseModel(c));
|
var responses = orgCollections.Select(c => new CollectionResponseModel(c));
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Api.Auth.Models.Request;
|
using Api.Models.Request;
|
||||||
|
using Bit.Api.Auth.Models.Request;
|
||||||
using Bit.Api.Auth.Models.Request.Accounts;
|
using Bit.Api.Auth.Models.Request.Accounts;
|
||||||
using Bit.Api.Models.Request;
|
using Bit.Api.Models.Request;
|
||||||
using Bit.Api.Models.Response;
|
using Bit.Api.Models.Response;
|
||||||
@ -206,10 +207,8 @@ public class DevicesController : Controller
|
|||||||
|
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
[HttpGet("knowndevice")]
|
[HttpGet("knowndevice")]
|
||||||
public async Task<bool> GetByIdentifierQuery(
|
public async Task<bool> GetByIdentifierQuery([FromHeader] KnownDeviceRequestModel request)
|
||||||
[FromHeader(Name = "X-Request-Email")] string email,
|
=> await GetByIdentifier(CoreHelpers.Base64UrlDecodeString(request.Email), request.DeviceIdentifier);
|
||||||
[FromHeader(Name = "X-Device-Identifier")] string deviceIdentifier)
|
|
||||||
=> await GetByIdentifier(CoreHelpers.Base64UrlDecodeString(email), deviceIdentifier);
|
|
||||||
|
|
||||||
[Obsolete("Path is deprecated due to encoding issues, use /knowndevice instead.")]
|
[Obsolete("Path is deprecated due to encoding issues, use /knowndevice instead.")]
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
using Bit.Api.Auth.Models.Public.Request;
|
using Bit.Api.AdminConsole.Public.Models.Request;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
|
|
||||||
namespace Bit.Api.Models.Public.Request;
|
namespace Bit.Api.Models.Public.Request;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Api.Auth.Models.Public.Response;
|
using Bit.Api.AdminConsole.Public.Models.Response;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
|
|
||||||
|
16
src/Api/Models/Request/KnownDeviceRequestModel.cs
Normal file
16
src/Api/Models/Request/KnownDeviceRequestModel.cs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
|
namespace Api.Models.Request;
|
||||||
|
|
||||||
|
public class KnownDeviceRequestModel
|
||||||
|
{
|
||||||
|
[Required]
|
||||||
|
[FromHeader(Name = "X-Request-Email")]
|
||||||
|
public string Email { get; set; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[FromHeader(Name = "X-Device-Identifier")]
|
||||||
|
public string DeviceIdentifier { get; set; }
|
||||||
|
|
||||||
|
}
|
@ -36,6 +36,7 @@ public class ProfileResponseModel : ResponseModel
|
|||||||
ForcePasswordReset = user.ForcePasswordReset;
|
ForcePasswordReset = user.ForcePasswordReset;
|
||||||
UsesKeyConnector = user.UsesKeyConnector;
|
UsesKeyConnector = user.UsesKeyConnector;
|
||||||
AvatarColor = user.AvatarColor;
|
AvatarColor = user.AvatarColor;
|
||||||
|
CreationDate = user.CreationDate;
|
||||||
Organizations = organizationsUserDetails?.Select(o => new ProfileOrganizationResponseModel(o));
|
Organizations = organizationsUserDetails?.Select(o => new ProfileOrganizationResponseModel(o));
|
||||||
Providers = providerUserDetails?.Select(p => new ProfileProviderResponseModel(p));
|
Providers = providerUserDetails?.Select(p => new ProfileProviderResponseModel(p));
|
||||||
ProviderOrganizations =
|
ProviderOrganizations =
|
||||||
@ -61,6 +62,7 @@ public class ProfileResponseModel : ResponseModel
|
|||||||
public bool ForcePasswordReset { get; set; }
|
public bool ForcePasswordReset { get; set; }
|
||||||
public bool UsesKeyConnector { get; set; }
|
public bool UsesKeyConnector { get; set; }
|
||||||
public string AvatarColor { get; set; }
|
public string AvatarColor { get; set; }
|
||||||
|
public DateTime CreationDate { get; set; }
|
||||||
public IEnumerable<ProfileOrganizationResponseModel> Organizations { get; set; }
|
public IEnumerable<ProfileOrganizationResponseModel> Organizations { get; set; }
|
||||||
public IEnumerable<ProfileProviderResponseModel> Providers { get; set; }
|
public IEnumerable<ProfileProviderResponseModel> Providers { get; set; }
|
||||||
public IEnumerable<ProfileProviderOrganizationResponseModel> ProviderOrganizations { get; set; }
|
public IEnumerable<ProfileProviderOrganizationResponseModel> ProviderOrganizations { get; set; }
|
||||||
|
@ -4,6 +4,7 @@ using Bit.Core.Context;
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Data.Organizations;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
@ -20,6 +21,7 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
|||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly ICollectionRepository _collectionRepository;
|
private readonly ICollectionRepository _collectionRepository;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
private Guid _targetOrganizationId;
|
private Guid _targetOrganizationId;
|
||||||
|
|
||||||
private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
||||||
@ -27,11 +29,13 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
|||||||
public BulkCollectionAuthorizationHandler(
|
public BulkCollectionAuthorizationHandler(
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
ICollectionRepository collectionRepository,
|
ICollectionRepository collectionRepository,
|
||||||
IFeatureService featureService)
|
IFeatureService featureService,
|
||||||
|
IApplicationCacheService applicationCacheService)
|
||||||
{
|
{
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_collectionRepository = collectionRepository;
|
_collectionRepository = collectionRepository;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
|
_applicationCacheService = applicationCacheService;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||||
@ -84,7 +88,7 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
|||||||
|
|
||||||
case not null when requirement == BulkCollectionOperations.Update:
|
case not null when requirement == BulkCollectionOperations.Update:
|
||||||
case not null when requirement == BulkCollectionOperations.ModifyAccess:
|
case not null when requirement == BulkCollectionOperations.ModifyAccess:
|
||||||
await CanUpdateCollection(context, requirement, resources, org);
|
await CanUpdateCollectionAsync(context, requirement, resources, org);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case not null when requirement == BulkCollectionOperations.Delete:
|
case not null when requirement == BulkCollectionOperations.Delete:
|
||||||
@ -96,10 +100,8 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
|||||||
private async Task CanCreateAsync(AuthorizationHandlerContext context, IAuthorizationRequirement requirement,
|
private async Task CanCreateAsync(AuthorizationHandlerContext context, IAuthorizationRequirement requirement,
|
||||||
CurrentContextOrganization? org)
|
CurrentContextOrganization? org)
|
||||||
{
|
{
|
||||||
// If the limit collection management setting is disabled, allow any user to create collections
|
// Owners, Admins, and users with CreateNewCollections permission can always create collections
|
||||||
// Otherwise, Owners, Admins, and users with CreateNewCollections permission can always create collections
|
|
||||||
if (org is
|
if (org is
|
||||||
{ LimitCollectionCreationDeletion: false } or
|
|
||||||
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
||||||
{ Permissions.CreateNewCollections: true })
|
{ Permissions.CreateNewCollections: true })
|
||||||
{
|
{
|
||||||
@ -107,6 +109,13 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the limit collection management setting is disabled, allow any user to create collections
|
||||||
|
if (await GetOrganizationAbilityAsync(org) is { LimitCollectionCreationDeletion: false })
|
||||||
|
{
|
||||||
|
context.Succeed(requirement);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Allow provider users to create collections if they are a provider for the target organization
|
// Allow provider users to create collections if they are a provider for the target organization
|
||||||
if (await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId))
|
if (await _currentContext.ProviderUserForOrgAsync(_targetOrganizationId))
|
||||||
{
|
{
|
||||||
@ -131,8 +140,8 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
|||||||
// ensure they have access for the collection being read
|
// ensure they have access for the collection being read
|
||||||
if (org is not null)
|
if (org is not null)
|
||||||
{
|
{
|
||||||
var isAssignedToCollections = await IsAssignedToCollectionsAsync(resources, org, false);
|
var canManageCollections = await CanManageCollectionsAsync(resources, org);
|
||||||
if (isAssignedToCollections)
|
if (canManageCollections)
|
||||||
{
|
{
|
||||||
context.Succeed(requirement);
|
context.Succeed(requirement);
|
||||||
return;
|
return;
|
||||||
@ -164,8 +173,8 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
|||||||
// ensure they have access with manage permission for the collection being read
|
// ensure they have access with manage permission for the collection being read
|
||||||
if (org is not null)
|
if (org is not null)
|
||||||
{
|
{
|
||||||
var isAssignedToCollections = await IsAssignedToCollectionsAsync(resources, org, true);
|
var canManageCollections = await CanManageCollectionsAsync(resources, org);
|
||||||
if (isAssignedToCollections)
|
if (canManageCollections)
|
||||||
{
|
{
|
||||||
context.Succeed(requirement);
|
context.Succeed(requirement);
|
||||||
return;
|
return;
|
||||||
@ -182,7 +191,7 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Ensures the acting user is allowed to update the target collections or manage access permissions for them.
|
/// Ensures the acting user is allowed to update the target collections or manage access permissions for them.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task CanUpdateCollection(AuthorizationHandlerContext context,
|
private async Task CanUpdateCollectionAsync(AuthorizationHandlerContext context,
|
||||||
IAuthorizationRequirement requirement, ICollection<Collection> resources,
|
IAuthorizationRequirement requirement, ICollection<Collection> resources,
|
||||||
CurrentContextOrganization? org)
|
CurrentContextOrganization? org)
|
||||||
{
|
{
|
||||||
@ -199,7 +208,7 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
|||||||
// ensure they have manage permission for the collection being managed
|
// ensure they have manage permission for the collection being managed
|
||||||
if (org is not null)
|
if (org is not null)
|
||||||
{
|
{
|
||||||
var canManageCollections = await IsAssignedToCollectionsAsync(resources, org, true);
|
var canManageCollections = await CanManageCollectionsAsync(resources, org);
|
||||||
if (canManageCollections)
|
if (canManageCollections)
|
||||||
{
|
{
|
||||||
context.Succeed(requirement);
|
context.Succeed(requirement);
|
||||||
@ -226,11 +235,12 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for non-null org here: the user must be apart of the organization for this setting to take affect
|
||||||
// The limit collection management setting is disabled,
|
// The limit collection management setting is disabled,
|
||||||
// ensure acting user has manage permissions for all collections being deleted
|
// ensure acting user has manage permissions for all collections being deleted
|
||||||
if (org is { LimitCollectionCreationDeletion: false })
|
if (await GetOrganizationAbilityAsync(org) is { LimitCollectionCreationDeletion: false })
|
||||||
{
|
{
|
||||||
var canManageCollections = await IsAssignedToCollectionsAsync(resources, org, true);
|
var canManageCollections = await CanManageCollectionsAsync(resources, org);
|
||||||
if (canManageCollections)
|
if (canManageCollections)
|
||||||
{
|
{
|
||||||
context.Succeed(requirement);
|
context.Succeed(requirement);
|
||||||
@ -245,21 +255,35 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> IsAssignedToCollectionsAsync(
|
private async Task<bool> CanManageCollectionsAsync(
|
||||||
ICollection<Collection> targetCollections,
|
ICollection<Collection> targetCollections,
|
||||||
CurrentContextOrganization org,
|
CurrentContextOrganization org)
|
||||||
bool requireManagePermission)
|
|
||||||
{
|
{
|
||||||
// List of collection Ids the acting user has access to
|
// List of collection Ids the acting user has access to
|
||||||
var assignedCollectionIds =
|
var assignedCollectionIds =
|
||||||
(await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId!.Value, useFlexibleCollections: true))
|
(await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId!.Value, useFlexibleCollections: true))
|
||||||
.Where(c =>
|
.Where(c =>
|
||||||
// Check Collections with Manage permission
|
// Check Collections with Manage permission
|
||||||
(!requireManagePermission || c.Manage) && c.OrganizationId == org.Id)
|
c.Manage && c.OrganizationId == org.Id)
|
||||||
.Select(c => c.Id)
|
.Select(c => c.Id)
|
||||||
.ToHashSet();
|
.ToHashSet();
|
||||||
|
|
||||||
// Check if the acting user has access to all target collections
|
// Check if the acting user has access to all target collections
|
||||||
return targetCollections.All(tc => assignedCollectionIds.Contains(tc.Id));
|
return targetCollections.All(tc => assignedCollectionIds.Contains(tc.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<OrganizationAbility?> GetOrganizationAbilityAsync(CurrentContextOrganization? organization)
|
||||||
|
{
|
||||||
|
// If the CurrentContextOrganization is null, then the user isn't a member of the org so the setting is
|
||||||
|
// irrelevant
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
(await _applicationCacheService.GetOrganizationAbilitiesAsync())
|
||||||
|
.TryGetValue(organization.Id, out var organizationAbility);
|
||||||
|
|
||||||
|
return organizationAbility;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ using Bit.Core;
|
|||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Data.Organizations;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
@ -16,15 +17,18 @@ public class GroupAuthorizationHandler : AuthorizationHandler<GroupOperationRequ
|
|||||||
{
|
{
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
|
|
||||||
private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
||||||
|
|
||||||
public GroupAuthorizationHandler(
|
public GroupAuthorizationHandler(
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IFeatureService featureService)
|
IFeatureService featureService,
|
||||||
|
IApplicationCacheService applicationCacheService)
|
||||||
{
|
{
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
|
_applicationCacheService = applicationCacheService;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||||
@ -62,10 +66,8 @@ public class GroupAuthorizationHandler : AuthorizationHandler<GroupOperationRequ
|
|||||||
private async Task CanReadAllAsync(AuthorizationHandlerContext context, GroupOperationRequirement requirement,
|
private async Task CanReadAllAsync(AuthorizationHandlerContext context, GroupOperationRequirement requirement,
|
||||||
CurrentContextOrganization? org)
|
CurrentContextOrganization? org)
|
||||||
{
|
{
|
||||||
// If the limit collection management setting is disabled, allow any user to read all groups
|
// Owners, Admins, and users with any of ManageGroups, ManageUsers, EditAnyCollection, DeleteAnyCollection, CreateNewCollections permissions can always read all groups
|
||||||
// Otherwise, Owners, Admins, and users with any of ManageGroups, ManageUsers, EditAnyCollection, DeleteAnyCollection, CreateNewCollections permissions can always read all groups
|
|
||||||
if (org is
|
if (org is
|
||||||
{ LimitCollectionCreationDeletion: false } or
|
|
||||||
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
||||||
{ Permissions.ManageGroups: true } or
|
{ Permissions.ManageGroups: true } or
|
||||||
{ Permissions.ManageUsers: true } or
|
{ Permissions.ManageUsers: true } or
|
||||||
@ -77,10 +79,33 @@ public class GroupAuthorizationHandler : AuthorizationHandler<GroupOperationRequ
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for non-null org here: the user must be apart of the organization for this setting to take affect
|
||||||
|
// If the limit collection management setting is disabled, allow any user to read all groups
|
||||||
|
if (await GetOrganizationAbilityAsync(org) is { LimitCollectionCreationDeletion: false })
|
||||||
|
{
|
||||||
|
context.Succeed(requirement);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Allow provider users to read all groups if they are a provider for the target organization
|
// Allow provider users to read all groups if they are a provider for the target organization
|
||||||
if (await _currentContext.ProviderUserForOrgAsync(requirement.OrganizationId))
|
if (await _currentContext.ProviderUserForOrgAsync(requirement.OrganizationId))
|
||||||
{
|
{
|
||||||
context.Succeed(requirement);
|
context.Succeed(requirement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<OrganizationAbility?> GetOrganizationAbilityAsync(CurrentContextOrganization? organization)
|
||||||
|
{
|
||||||
|
// If the CurrentContextOrganization is null, then the user isn't a member of the org so the setting is
|
||||||
|
// irrelevant
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
(await _applicationCacheService.GetOrganizationAbilitiesAsync())
|
||||||
|
.TryGetValue(organization.Id, out var organizationAbility);
|
||||||
|
|
||||||
|
return organizationAbility;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,7 @@ using Bit.Core;
|
|||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Data.Organizations;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
|
|
||||||
@ -16,15 +17,18 @@ public class OrganizationUserAuthorizationHandler : AuthorizationHandler<Organiz
|
|||||||
{
|
{
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly IApplicationCacheService _applicationCacheService;
|
||||||
|
|
||||||
private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
private bool FlexibleCollectionsIsEnabled => _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
||||||
|
|
||||||
public OrganizationUserAuthorizationHandler(
|
public OrganizationUserAuthorizationHandler(
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IFeatureService featureService)
|
IFeatureService featureService,
|
||||||
|
IApplicationCacheService applicationCacheService)
|
||||||
{
|
{
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
|
_applicationCacheService = applicationCacheService;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
|
||||||
@ -64,7 +68,6 @@ public class OrganizationUserAuthorizationHandler : AuthorizationHandler<Organiz
|
|||||||
// If the limit collection management setting is disabled, allow any user to read all organization users
|
// If the limit collection management setting is disabled, allow any user to read all organization users
|
||||||
// Otherwise, Owners, Admins, and users with any of ManageGroups, ManageUsers, EditAnyCollection, DeleteAnyCollection, CreateNewCollections permissions can always read all organization users
|
// Otherwise, Owners, Admins, and users with any of ManageGroups, ManageUsers, EditAnyCollection, DeleteAnyCollection, CreateNewCollections permissions can always read all organization users
|
||||||
if (org is
|
if (org is
|
||||||
{ LimitCollectionCreationDeletion: false } or
|
|
||||||
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
{ Type: OrganizationUserType.Owner or OrganizationUserType.Admin } or
|
||||||
{ Permissions.ManageGroups: true } or
|
{ Permissions.ManageGroups: true } or
|
||||||
{ Permissions.ManageUsers: true } or
|
{ Permissions.ManageUsers: true } or
|
||||||
@ -76,10 +79,33 @@ public class OrganizationUserAuthorizationHandler : AuthorizationHandler<Organiz
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check for non-null org here: the user must be apart of the organization for this setting to take affect
|
||||||
|
// If the limit collection management setting is disabled, allow any user to read all organization users
|
||||||
|
if (await GetOrganizationAbilityAsync(org) is { LimitCollectionCreationDeletion: false })
|
||||||
|
{
|
||||||
|
context.Succeed(requirement);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Allow provider users to read all organization users if they are a provider for the target organization
|
// Allow provider users to read all organization users if they are a provider for the target organization
|
||||||
if (await _currentContext.ProviderUserForOrgAsync(requirement.OrganizationId))
|
if (await _currentContext.ProviderUserForOrgAsync(requirement.OrganizationId))
|
||||||
{
|
{
|
||||||
context.Succeed(requirement);
|
context.Succeed(requirement);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<OrganizationAbility?> GetOrganizationAbilityAsync(CurrentContextOrganization? organization)
|
||||||
|
{
|
||||||
|
// If the CurrentContextOrganization is null, then the user isn't a member of the org so the setting is
|
||||||
|
// irrelevant
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
(await _applicationCacheService.GetOrganizationAbilitiesAsync())
|
||||||
|
.TryGetValue(organization.Id, out var organizationAbility);
|
||||||
|
|
||||||
|
return organizationAbility;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -96,6 +96,7 @@ public class SyncController : Controller
|
|||||||
{
|
{
|
||||||
collections = await _collectionRepository.GetManyByUserIdAsync(user.Id, UseFlexibleCollections);
|
collections = await _collectionRepository.GetManyByUserIdAsync(user.Id, UseFlexibleCollections);
|
||||||
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdAsync(user.Id, UseFlexibleCollections);
|
var collectionCiphers = await _collectionCipherRepository.GetManyByUserIdAsync(user.Id, UseFlexibleCollections);
|
||||||
|
collectionCiphersGroupDict = collectionCiphers.GroupBy(c => c.CipherId).ToDictionary(s => s.Key);
|
||||||
}
|
}
|
||||||
|
|
||||||
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
|
||||||
|
@ -15,14 +15,10 @@ public class CurrentContextOrganization
|
|||||||
Type = orgUser.Type;
|
Type = orgUser.Type;
|
||||||
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(orgUser.Permissions);
|
Permissions = CoreHelpers.LoadClassFromJsonData<Permissions>(orgUser.Permissions);
|
||||||
AccessSecretsManager = orgUser.AccessSecretsManager && orgUser.UseSecretsManager && orgUser.Enabled;
|
AccessSecretsManager = orgUser.AccessSecretsManager && orgUser.UseSecretsManager && orgUser.Enabled;
|
||||||
LimitCollectionCreationDeletion = orgUser.LimitCollectionCreationDeletion;
|
|
||||||
AllowAdminAccessToAllCollectionItems = orgUser.AllowAdminAccessToAllCollectionItems;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
public OrganizationUserType Type { get; set; }
|
public OrganizationUserType Type { get; set; }
|
||||||
public Permissions Permissions { get; set; } = new();
|
public Permissions Permissions { get; set; } = new();
|
||||||
public bool AccessSecretsManager { get; set; }
|
public bool AccessSecretsManager { get; set; }
|
||||||
public bool LimitCollectionCreationDeletion { get; set; }
|
|
||||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
|
||||||
}
|
}
|
||||||
|
@ -236,7 +236,10 @@ public class Organization : ITableObject<Guid>, ISubscriber, IStorable, IStorabl
|
|||||||
return providers[provider];
|
return providers[provider];
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateFromLicense(OrganizationLicense license, bool flexibleCollectionsIsEnabled)
|
public void UpdateFromLicense(
|
||||||
|
OrganizationLicense license,
|
||||||
|
bool flexibleCollectionsMvpIsEnabled,
|
||||||
|
bool flexibleCollectionsV1IsEnabled)
|
||||||
{
|
{
|
||||||
Name = license.Name;
|
Name = license.Name;
|
||||||
BusinessName = license.BusinessName;
|
BusinessName = license.BusinessName;
|
||||||
@ -267,6 +270,7 @@ public class Organization : ITableObject<Guid>, ISubscriber, IStorable, IStorabl
|
|||||||
UseSecretsManager = license.UseSecretsManager;
|
UseSecretsManager = license.UseSecretsManager;
|
||||||
SmSeats = license.SmSeats;
|
SmSeats = license.SmSeats;
|
||||||
SmServiceAccounts = license.SmServiceAccounts;
|
SmServiceAccounts = license.SmServiceAccounts;
|
||||||
LimitCollectionCreationDeletion = !flexibleCollectionsIsEnabled || license.LimitCollectionCreationDeletion;
|
LimitCollectionCreationDeletion = !flexibleCollectionsMvpIsEnabled || license.LimitCollectionCreationDeletion;
|
||||||
|
AllowAdminAccessToAllCollectionItems = !flexibleCollectionsV1IsEnabled || license.AllowAdminAccessToAllCollectionItems;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,8 @@ public class OrganizationAbility
|
|||||||
UseResetPassword = organization.UseResetPassword;
|
UseResetPassword = organization.UseResetPassword;
|
||||||
UseCustomPermissions = organization.UseCustomPermissions;
|
UseCustomPermissions = organization.UseCustomPermissions;
|
||||||
UsePolicies = organization.UsePolicies;
|
UsePolicies = organization.UsePolicies;
|
||||||
|
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
|
||||||
|
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Guid Id { get; set; }
|
public Guid Id { get; set; }
|
||||||
@ -35,4 +37,6 @@ public class OrganizationAbility
|
|||||||
public bool UseResetPassword { get; set; }
|
public bool UseResetPassword { get; set; }
|
||||||
public bool UseCustomPermissions { get; set; }
|
public bool UseCustomPermissions { get; set; }
|
||||||
public bool UsePolicies { get; set; }
|
public bool UsePolicies { get; set; }
|
||||||
|
public bool LimitCollectionCreationDeletion { get; set; }
|
||||||
|
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -558,8 +558,10 @@ public class OrganizationService : IOrganizationService
|
|||||||
|
|
||||||
await ValidateSignUpPoliciesAsync(owner.Id);
|
await ValidateSignUpPoliciesAsync(owner.Id);
|
||||||
|
|
||||||
var flexibleCollectionsIsEnabled =
|
var flexibleCollectionsMvpIsEnabled =
|
||||||
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
||||||
|
var flexibleCollectionsV1IsEnabled =
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1, _currentContext);
|
||||||
|
|
||||||
var organization = new Organization
|
var organization = new Organization
|
||||||
{
|
{
|
||||||
@ -601,7 +603,8 @@ public class OrganizationService : IOrganizationService
|
|||||||
UseSecretsManager = license.UseSecretsManager,
|
UseSecretsManager = license.UseSecretsManager,
|
||||||
SmSeats = license.SmSeats,
|
SmSeats = license.SmSeats,
|
||||||
SmServiceAccounts = license.SmServiceAccounts,
|
SmServiceAccounts = license.SmServiceAccounts,
|
||||||
LimitCollectionCreationDeletion = !flexibleCollectionsIsEnabled || license.LimitCollectionCreationDeletion
|
LimitCollectionCreationDeletion = !flexibleCollectionsMvpIsEnabled || license.LimitCollectionCreationDeletion,
|
||||||
|
AllowAdminAccessToAllCollectionItems = !flexibleCollectionsV1IsEnabled || license.AllowAdminAccessToAllCollectionItems
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);
|
var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);
|
||||||
@ -1118,7 +1121,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
|
|
||||||
// Determine if org has SSO enabled and if user is required to login with SSO
|
// Determine if org has SSO enabled and if user is required to login with SSO
|
||||||
// Note: we only want to call the DB after checking if the org can use SSO per plan and if they have any policies enabled.
|
// Note: we only want to call the DB after checking if the org can use SSO per plan and if they have any policies enabled.
|
||||||
var orgSsoEnabled = organization.UseSso && (await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id)).Enabled;
|
var orgSsoEnabled = organization.UseSso && (await _ssoConfigRepository.GetByOrganizationIdAsync(organization.Id))?.Enabled == true;
|
||||||
// Even though the require SSO policy can be turned on regardless of SSO being enabled, for this logic, we only
|
// Even though the require SSO policy can be turned on regardless of SSO being enabled, for this logic, we only
|
||||||
// need to check the policy if the org has SSO enabled.
|
// need to check the policy if the org has SSO enabled.
|
||||||
var orgSsoLoginRequiredPolicyEnabled = orgSsoEnabled &&
|
var orgSsoLoginRequiredPolicyEnabled = orgSsoEnabled &&
|
||||||
@ -1890,11 +1893,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
|
|
||||||
public void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade)
|
public void ValidatePasswordManagerPlan(Models.StaticStore.Plan plan, OrganizationUpgrade upgrade)
|
||||||
{
|
{
|
||||||
if (plan is not { LegacyYear: null })
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Invalid Password Manager plan selected.");
|
|
||||||
}
|
|
||||||
|
|
||||||
ValidatePlan(plan, upgrade.AdditionalSeats, "Password Manager");
|
ValidatePlan(plan, upgrade.AdditionalSeats, "Password Manager");
|
||||||
|
|
||||||
if (plan.PasswordManager.BaseSeats + upgrade.AdditionalSeats <= 0)
|
if (plan.PasswordManager.BaseSeats + upgrade.AdditionalSeats <= 0)
|
||||||
@ -2409,12 +2407,8 @@ public class OrganizationService : IOrganizationService
|
|||||||
public async Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted)
|
public async Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted)
|
||||||
{
|
{
|
||||||
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
|
var plan = StaticStore.Plans.FirstOrDefault(p => p.Type == organization.PlanType);
|
||||||
if (plan is not { LegacyYear: null })
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Invalid plan selected.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plan.Disabled)
|
if (plan!.Disabled)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Plan not found.");
|
throw new BadRequestException("Plan not found.");
|
||||||
}
|
}
|
||||||
|
@ -1,23 +1,25 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
#nullable enable
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
namespace Bit.Core.Auth.Entities;
|
namespace Bit.Core.Auth.Entities;
|
||||||
|
|
||||||
public class Grant
|
public class Grant
|
||||||
{
|
{
|
||||||
|
public int Id { get; set; }
|
||||||
[MaxLength(200)]
|
[MaxLength(200)]
|
||||||
public string Key { get; set; }
|
public string Key { get; set; } = null!;
|
||||||
[MaxLength(50)]
|
[MaxLength(50)]
|
||||||
public string Type { get; set; }
|
public string Type { get; set; } = null!;
|
||||||
[MaxLength(200)]
|
[MaxLength(200)]
|
||||||
public string SubjectId { get; set; }
|
public string? SubjectId { get; set; }
|
||||||
[MaxLength(100)]
|
[MaxLength(100)]
|
||||||
public string SessionId { get; set; }
|
public string? SessionId { get; set; }
|
||||||
[MaxLength(200)]
|
[MaxLength(200)]
|
||||||
public string ClientId { get; set; }
|
public string ClientId { get; set; } = null!;
|
||||||
[MaxLength(200)]
|
[MaxLength(200)]
|
||||||
public string Description { get; set; }
|
public string? Description { get; set; }
|
||||||
public DateTime CreationDate { get; set; }
|
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
|
||||||
public DateTime? ExpirationDate { get; set; }
|
public DateTime? ExpirationDate { get; set; }
|
||||||
public DateTime? ConsumedDate { get; set; }
|
public DateTime? ConsumedDate { get; set; }
|
||||||
public string Data { get; set; }
|
public string Data { get; set; } = null!;
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,12 @@ public static class Constants
|
|||||||
/// Used by IdentityServer to identify our own provider.
|
/// Used by IdentityServer to identify our own provider.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string IdentityProvider = "bitwarden";
|
public const string IdentityProvider = "bitwarden";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Date identifier used in ProviderService to determine if a provider was created before Nov 6, 2023.
|
||||||
|
/// If true, the organization plan assigned to that provider is updated to a 2020 plan.
|
||||||
|
/// </summary>
|
||||||
|
public static readonly DateTime ProviderCreatedPriorNov62023 = new DateTime(2023, 11, 6);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class AuthConstants
|
public static class AuthConstants
|
||||||
|
@ -54,7 +54,7 @@ public class CurrentContext : ICurrentContext
|
|||||||
{
|
{
|
||||||
_providerOrganizationRepository = providerOrganizationRepository;
|
_providerOrganizationRepository = providerOrganizationRepository;
|
||||||
_providerUserRepository = providerUserRepository;
|
_providerUserRepository = providerUserRepository;
|
||||||
_featureService = featureService;
|
_featureService = featureService; ;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async virtual Task BuildAsync(HttpContext httpContext, GlobalSettings globalSettings)
|
public async virtual Task BuildAsync(HttpContext httpContext, GlobalSettings globalSettings)
|
||||||
@ -383,15 +383,10 @@ public class CurrentContext : ICurrentContext
|
|||||||
throw new FeatureUnavailableException("Flexible Collections is ON when it should be OFF.");
|
throw new FeatureUnavailableException("Flexible Collections is ON when it should be OFF.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var canCreateNewCollections = false;
|
|
||||||
var org = GetOrganization(orgId);
|
var org = GetOrganization(orgId);
|
||||||
if (org != null)
|
|
||||||
{
|
|
||||||
canCreateNewCollections = !org.LimitCollectionCreationDeletion || org.Permissions.CreateNewCollections;
|
|
||||||
}
|
|
||||||
return await EditAssignedCollections(orgId)
|
return await EditAssignedCollections(orgId)
|
||||||
|| await DeleteAssignedCollections(orgId)
|
|| await DeleteAssignedCollections(orgId)
|
||||||
|| canCreateNewCollections;
|
|| (org != null && org.Permissions.CreateNewCollections);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> ManageGroups(Guid orgId)
|
public async Task<bool> ManageGroups(Guid orgId)
|
||||||
|
@ -21,8 +21,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="1.0.1" />
|
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="1.0.1" />
|
||||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.0.150" />
|
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.300.31" />
|
||||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.2.47" />
|
<PackageReference Include="AWSSDK.SQS" Version="3.7.300.31" />
|
||||||
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.2" />
|
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.2" />
|
||||||
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.15.0" />
|
<PackageReference Include="Azure.Messaging.ServiceBus" Version="7.15.0" />
|
||||||
<PackageReference Include="Azure.Storage.Blobs" Version="12.14.1" />
|
<PackageReference Include="Azure.Storage.Blobs" Version="12.14.1" />
|
||||||
@ -47,7 +47,7 @@
|
|||||||
<PackageReference Include="Serilog.Extensions.Logging" Version="3.1.0" />
|
<PackageReference Include="Serilog.Extensions.Logging" Version="3.1.0" />
|
||||||
<PackageReference Include="Serilog.Extensions.Logging.File" Version="3.0.0" />
|
<PackageReference Include="Serilog.Extensions.Logging.File" Version="3.0.0" />
|
||||||
<PackageReference Include="Sentry.Serilog" Version="3.41.3" />
|
<PackageReference Include="Sentry.Serilog" Version="3.41.3" />
|
||||||
<PackageReference Include="Duende.IdentityServer" Version="6.0.4" />
|
<PackageReference Include="Duende.IdentityServer" Version="6.3.7" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||||
<PackageReference Include="Serilog.Sinks.AzureCosmosDB" Version="2.0.0" />
|
<PackageReference Include="Serilog.Sinks.AzureCosmosDB" Version="2.0.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="2.0.9" />
|
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="2.0.9" />
|
||||||
|
329
src/Core/Models/Business/CompleteSubscriptionUpdate.cs
Normal file
329
src/Core/Models/Business/CompleteSubscriptionUpdate.cs
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Stripe;
|
||||||
|
|
||||||
|
namespace Bit.Core.Models.Business;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A model representing the data required to upgrade from one subscription to another using a <see cref="CompleteSubscriptionUpdate"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class SubscriptionData
|
||||||
|
{
|
||||||
|
public StaticStore.Plan Plan { get; init; }
|
||||||
|
public int PurchasedPasswordManagerSeats { get; init; }
|
||||||
|
public bool SubscribedToSecretsManager { get; set; }
|
||||||
|
public int? PurchasedSecretsManagerSeats { get; init; }
|
||||||
|
public int? PurchasedAdditionalSecretsManagerServiceAccounts { get; init; }
|
||||||
|
public int PurchasedAdditionalStorage { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class CompleteSubscriptionUpdate : SubscriptionUpdate
|
||||||
|
{
|
||||||
|
private readonly SubscriptionData _currentSubscription;
|
||||||
|
private readonly SubscriptionData _updatedSubscription;
|
||||||
|
|
||||||
|
private readonly Dictionary<string, SubscriptionUpdateType> _subscriptionUpdateMap = new();
|
||||||
|
|
||||||
|
private enum SubscriptionUpdateType
|
||||||
|
{
|
||||||
|
PasswordManagerSeats,
|
||||||
|
SecretsManagerSeats,
|
||||||
|
SecretsManagerServiceAccounts,
|
||||||
|
Storage
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A model used to generate the Stripe <see cref="SubscriptionItemOptions"/>
|
||||||
|
/// necessary to both upgrade an organization's subscription and revert that upgrade
|
||||||
|
/// in the case of an error.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organization">The <see cref="Organization"/> to upgrade.</param>
|
||||||
|
/// <param name="updatedSubscription">The updates you want to apply to the organization's subscription.</param>
|
||||||
|
public CompleteSubscriptionUpdate(
|
||||||
|
Organization organization,
|
||||||
|
SubscriptionData updatedSubscription)
|
||||||
|
{
|
||||||
|
_currentSubscription = GetSubscriptionDataFor(organization);
|
||||||
|
_updatedSubscription = updatedSubscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override List<string> PlanIds => new()
|
||||||
|
{
|
||||||
|
GetPasswordManagerPlanId(_updatedSubscription.Plan),
|
||||||
|
_updatedSubscription.Plan.SecretsManager.StripeSeatPlanId,
|
||||||
|
_updatedSubscription.Plan.SecretsManager.StripeServiceAccountPlanId,
|
||||||
|
_updatedSubscription.Plan.PasswordManager.StripeStoragePlanId
|
||||||
|
};
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates the <see cref="SubscriptionItemOptions"/> necessary to revert an <see cref="Organization"/>'s
|
||||||
|
/// <see cref="Subscription"/> upgrade in the case of an error.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subscription">The organization's <see cref="Subscription"/>.</param>
|
||||||
|
public override List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription)
|
||||||
|
{
|
||||||
|
var subscriptionItemOptions = new List<SubscriptionItemOptions>
|
||||||
|
{
|
||||||
|
GetPasswordManagerOptions(subscription, _updatedSubscription, _currentSubscription)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_updatedSubscription.SubscribedToSecretsManager || _currentSubscription.SubscribedToSecretsManager)
|
||||||
|
{
|
||||||
|
subscriptionItemOptions.Add(GetSecretsManagerOptions(subscription, _updatedSubscription, _currentSubscription));
|
||||||
|
|
||||||
|
if (_updatedSubscription.PurchasedAdditionalSecretsManagerServiceAccounts != 0 ||
|
||||||
|
_currentSubscription.PurchasedAdditionalSecretsManagerServiceAccounts != 0)
|
||||||
|
{
|
||||||
|
subscriptionItemOptions.Add(GetServiceAccountsOptions(subscription, _updatedSubscription, _currentSubscription));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_updatedSubscription.PurchasedAdditionalStorage != 0 || _currentSubscription.PurchasedAdditionalStorage != 0)
|
||||||
|
{
|
||||||
|
subscriptionItemOptions.Add(GetStorageOptions(subscription, _updatedSubscription, _currentSubscription));
|
||||||
|
}
|
||||||
|
|
||||||
|
return subscriptionItemOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This is almost certainly overkill. If we trust the data in the Vault DB, we should just be able to
|
||||||
|
* compare the _currentSubscription against the _updatedSubscription to see if there are any differences.
|
||||||
|
* However, for the sake of ensuring we're checking against the Stripe subscription itself, I'll leave this
|
||||||
|
* included for now.
|
||||||
|
*/
|
||||||
|
/// <summary>
|
||||||
|
/// Checks whether the updates provided in the <see cref="CompleteSubscriptionUpdate"/>'s constructor
|
||||||
|
/// are actually different than the organization's current <see cref="Subscription"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subscription">The organization's <see cref="Subscription"/>.</param>
|
||||||
|
public override bool UpdateNeeded(Subscription subscription)
|
||||||
|
{
|
||||||
|
var upgradeItemsOptions = UpgradeItemsOptions(subscription);
|
||||||
|
|
||||||
|
foreach (var subscriptionItemOptions in upgradeItemsOptions)
|
||||||
|
{
|
||||||
|
var success = _subscriptionUpdateMap.TryGetValue(subscriptionItemOptions.Price, out var updateType);
|
||||||
|
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var updateNeeded = updateType switch
|
||||||
|
{
|
||||||
|
SubscriptionUpdateType.PasswordManagerSeats => ContainsUpdatesBetween(
|
||||||
|
GetPasswordManagerPlanId(_currentSubscription.Plan),
|
||||||
|
subscriptionItemOptions),
|
||||||
|
SubscriptionUpdateType.SecretsManagerSeats => ContainsUpdatesBetween(
|
||||||
|
_currentSubscription.Plan.SecretsManager.StripeSeatPlanId,
|
||||||
|
subscriptionItemOptions),
|
||||||
|
SubscriptionUpdateType.SecretsManagerServiceAccounts => ContainsUpdatesBetween(
|
||||||
|
_currentSubscription.Plan.SecretsManager.StripeServiceAccountPlanId,
|
||||||
|
subscriptionItemOptions),
|
||||||
|
SubscriptionUpdateType.Storage => ContainsUpdatesBetween(
|
||||||
|
_currentSubscription.Plan.PasswordManager.StripeStoragePlanId,
|
||||||
|
subscriptionItemOptions),
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
|
||||||
|
if (updateNeeded)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
|
||||||
|
bool ContainsUpdatesBetween(string currentPlanId, SubscriptionItemOptions options)
|
||||||
|
{
|
||||||
|
var subscriptionItem = FindSubscriptionItem(subscription, currentPlanId);
|
||||||
|
|
||||||
|
return (subscriptionItem.Plan.Id != options.Plan && subscriptionItem.Price.Id != options.Plan) ||
|
||||||
|
subscriptionItem.Quantity != options.Quantity ||
|
||||||
|
subscriptionItem.Deleted != options.Deleted;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates the <see cref="SubscriptionItemOptions"/> necessary to upgrade an <see cref="Organization"/>'s
|
||||||
|
/// <see cref="Subscription"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="subscription">The organization's <see cref="Subscription"/>.</param>
|
||||||
|
public override List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription)
|
||||||
|
{
|
||||||
|
var subscriptionItemOptions = new List<SubscriptionItemOptions>
|
||||||
|
{
|
||||||
|
GetPasswordManagerOptions(subscription, _currentSubscription, _updatedSubscription)
|
||||||
|
};
|
||||||
|
|
||||||
|
if (_currentSubscription.SubscribedToSecretsManager || _updatedSubscription.SubscribedToSecretsManager)
|
||||||
|
{
|
||||||
|
subscriptionItemOptions.Add(GetSecretsManagerOptions(subscription, _currentSubscription, _updatedSubscription));
|
||||||
|
|
||||||
|
if (_currentSubscription.PurchasedAdditionalSecretsManagerServiceAccounts != 0 ||
|
||||||
|
_updatedSubscription.PurchasedAdditionalSecretsManagerServiceAccounts != 0)
|
||||||
|
{
|
||||||
|
subscriptionItemOptions.Add(GetServiceAccountsOptions(subscription, _currentSubscription, _updatedSubscription));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_currentSubscription.PurchasedAdditionalStorage != 0 || _updatedSubscription.PurchasedAdditionalStorage != 0)
|
||||||
|
{
|
||||||
|
subscriptionItemOptions.Add(GetStorageOptions(subscription, _currentSubscription, _updatedSubscription));
|
||||||
|
}
|
||||||
|
|
||||||
|
return subscriptionItemOptions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SubscriptionItemOptions GetPasswordManagerOptions(
|
||||||
|
Subscription subscription,
|
||||||
|
SubscriptionData from,
|
||||||
|
SubscriptionData to)
|
||||||
|
{
|
||||||
|
var currentPlanId = GetPasswordManagerPlanId(from.Plan);
|
||||||
|
|
||||||
|
var subscriptionItem = FindSubscriptionItem(subscription, currentPlanId);
|
||||||
|
|
||||||
|
if (subscriptionItem == null)
|
||||||
|
{
|
||||||
|
throw new GatewayException("Could not find Password Manager subscription");
|
||||||
|
}
|
||||||
|
|
||||||
|
var updatedPlanId = GetPasswordManagerPlanId(to.Plan);
|
||||||
|
|
||||||
|
_subscriptionUpdateMap[updatedPlanId] = SubscriptionUpdateType.PasswordManagerSeats;
|
||||||
|
|
||||||
|
return new SubscriptionItemOptions
|
||||||
|
{
|
||||||
|
Id = subscriptionItem.Id,
|
||||||
|
Price = updatedPlanId,
|
||||||
|
Quantity = IsNonSeatBasedPlan(to.Plan) ? 1 : to.PurchasedPasswordManagerSeats
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private SubscriptionItemOptions GetSecretsManagerOptions(
|
||||||
|
Subscription subscription,
|
||||||
|
SubscriptionData from,
|
||||||
|
SubscriptionData to)
|
||||||
|
{
|
||||||
|
var currentPlanId = from.Plan?.SecretsManager?.StripeSeatPlanId;
|
||||||
|
|
||||||
|
var subscriptionItem = !string.IsNullOrEmpty(currentPlanId)
|
||||||
|
? FindSubscriptionItem(subscription, currentPlanId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
var updatedPlanId = to.Plan.SecretsManager.StripeSeatPlanId;
|
||||||
|
|
||||||
|
_subscriptionUpdateMap[updatedPlanId] = SubscriptionUpdateType.SecretsManagerSeats;
|
||||||
|
|
||||||
|
return new SubscriptionItemOptions
|
||||||
|
{
|
||||||
|
Id = subscriptionItem?.Id,
|
||||||
|
Price = updatedPlanId,
|
||||||
|
Quantity = to.PurchasedSecretsManagerSeats,
|
||||||
|
Deleted = subscriptionItem?.Id != null && to.PurchasedSecretsManagerSeats == 0
|
||||||
|
? true
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private SubscriptionItemOptions GetServiceAccountsOptions(
|
||||||
|
Subscription subscription,
|
||||||
|
SubscriptionData from,
|
||||||
|
SubscriptionData to)
|
||||||
|
{
|
||||||
|
var currentPlanId = from.Plan?.SecretsManager?.StripeServiceAccountPlanId;
|
||||||
|
|
||||||
|
var subscriptionItem = !string.IsNullOrEmpty(currentPlanId)
|
||||||
|
? FindSubscriptionItem(subscription, currentPlanId)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
var updatedPlanId = to.Plan.SecretsManager.StripeServiceAccountPlanId;
|
||||||
|
|
||||||
|
_subscriptionUpdateMap[updatedPlanId] = SubscriptionUpdateType.SecretsManagerServiceAccounts;
|
||||||
|
|
||||||
|
return new SubscriptionItemOptions
|
||||||
|
{
|
||||||
|
Id = subscriptionItem?.Id,
|
||||||
|
Price = updatedPlanId,
|
||||||
|
Quantity = to.PurchasedAdditionalSecretsManagerServiceAccounts,
|
||||||
|
Deleted = subscriptionItem?.Id != null && to.PurchasedAdditionalSecretsManagerServiceAccounts == 0
|
||||||
|
? true
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private SubscriptionItemOptions GetStorageOptions(
|
||||||
|
Subscription subscription,
|
||||||
|
SubscriptionData from,
|
||||||
|
SubscriptionData to)
|
||||||
|
{
|
||||||
|
var currentPlanId = from.Plan.PasswordManager.StripeStoragePlanId;
|
||||||
|
|
||||||
|
var subscriptionItem = FindSubscriptionItem(subscription, currentPlanId);
|
||||||
|
|
||||||
|
var updatedPlanId = to.Plan.PasswordManager.StripeStoragePlanId;
|
||||||
|
|
||||||
|
_subscriptionUpdateMap[updatedPlanId] = SubscriptionUpdateType.Storage;
|
||||||
|
|
||||||
|
return new SubscriptionItemOptions
|
||||||
|
{
|
||||||
|
Id = subscriptionItem?.Id,
|
||||||
|
Price = updatedPlanId,
|
||||||
|
Quantity = to.PurchasedAdditionalStorage,
|
||||||
|
Deleted = subscriptionItem?.Id != null && to.PurchasedAdditionalStorage == 0
|
||||||
|
? true
|
||||||
|
: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SubscriptionItem FindSubscriptionItem(Subscription subscription, string planId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(planId))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var data = subscription.Items.Data;
|
||||||
|
|
||||||
|
var subscriptionItem = data.FirstOrDefault(item => item.Plan?.Id == planId) ?? data.FirstOrDefault(item => item.Price?.Id == planId);
|
||||||
|
|
||||||
|
return subscriptionItem;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetPasswordManagerPlanId(StaticStore.Plan plan)
|
||||||
|
=> IsNonSeatBasedPlan(plan)
|
||||||
|
? plan.PasswordManager.StripePlanId
|
||||||
|
: plan.PasswordManager.StripeSeatPlanId;
|
||||||
|
|
||||||
|
private static SubscriptionData GetSubscriptionDataFor(Organization organization)
|
||||||
|
{
|
||||||
|
var plan = Utilities.StaticStore.GetPlan(organization.PlanType);
|
||||||
|
|
||||||
|
return new SubscriptionData
|
||||||
|
{
|
||||||
|
Plan = plan,
|
||||||
|
PurchasedPasswordManagerSeats = organization.Seats.HasValue
|
||||||
|
? organization.Seats.Value - plan.PasswordManager.BaseSeats
|
||||||
|
: 0,
|
||||||
|
SubscribedToSecretsManager = organization.UseSecretsManager,
|
||||||
|
PurchasedSecretsManagerSeats = plan.SecretsManager is not null
|
||||||
|
? organization.SmSeats - plan.SecretsManager.BaseSeats
|
||||||
|
: 0,
|
||||||
|
PurchasedAdditionalSecretsManagerServiceAccounts = plan.SecretsManager is not null
|
||||||
|
? organization.SmServiceAccounts - plan.SecretsManager.BaseServiceAccount
|
||||||
|
: 0,
|
||||||
|
PurchasedAdditionalStorage = organization.MaxStorageGb.HasValue
|
||||||
|
? organization.MaxStorageGb.Value - (plan.PasswordManager.BaseStorageGb ?? 0) :
|
||||||
|
0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsNonSeatBasedPlan(StaticStore.Plan plan)
|
||||||
|
=> plan.Type is
|
||||||
|
>= PlanType.FamiliesAnnually2019 and <= PlanType.EnterpriseAnnually2019
|
||||||
|
or PlanType.FamiliesAnnually
|
||||||
|
or PlanType.TeamsStarter;
|
||||||
|
}
|
@ -53,6 +53,7 @@ public class OrganizationLicense : ILicense
|
|||||||
SmSeats = org.SmSeats;
|
SmSeats = org.SmSeats;
|
||||||
SmServiceAccounts = org.SmServiceAccounts;
|
SmServiceAccounts = org.SmServiceAccounts;
|
||||||
LimitCollectionCreationDeletion = org.LimitCollectionCreationDeletion;
|
LimitCollectionCreationDeletion = org.LimitCollectionCreationDeletion;
|
||||||
|
AllowAdminAccessToAllCollectionItems = org.AllowAdminAccessToAllCollectionItems;
|
||||||
|
|
||||||
if (subscriptionInfo?.Subscription == null)
|
if (subscriptionInfo?.Subscription == null)
|
||||||
{
|
{
|
||||||
@ -137,6 +138,7 @@ public class OrganizationLicense : ILicense
|
|||||||
public int? SmSeats { get; set; }
|
public int? SmSeats { get; set; }
|
||||||
public int? SmServiceAccounts { get; set; }
|
public int? SmServiceAccounts { get; set; }
|
||||||
public bool LimitCollectionCreationDeletion { get; set; } = true;
|
public bool LimitCollectionCreationDeletion { get; set; } = true;
|
||||||
|
public bool AllowAdminAccessToAllCollectionItems { get; set; } = true;
|
||||||
public bool Trial { get; set; }
|
public bool Trial { get; set; }
|
||||||
public LicenseType? LicenseType { get; set; }
|
public LicenseType? LicenseType { get; set; }
|
||||||
public string Hash { get; set; }
|
public string Hash { get; set; }
|
||||||
@ -148,10 +150,10 @@ public class OrganizationLicense : ILicense
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Intentionally set one version behind to allow self hosted users some time to update before
|
/// <remarks>Intentionally set one version behind to allow self hosted users some time to update before
|
||||||
/// getting out of date license errors</remarks>
|
/// getting out of date license errors</remarks>
|
||||||
public const int CurrentLicenseFileVersion = 13;
|
public const int CurrentLicenseFileVersion = 14;
|
||||||
private bool ValidLicenseVersion
|
private bool ValidLicenseVersion
|
||||||
{
|
{
|
||||||
get => Version is >= 1 and <= 14;
|
get => Version is >= 1 and <= 15;
|
||||||
}
|
}
|
||||||
|
|
||||||
public byte[] GetDataBytes(bool forHash = false)
|
public byte[] GetDataBytes(bool forHash = false)
|
||||||
@ -194,6 +196,8 @@ public class OrganizationLicense : ILicense
|
|||||||
(Version >= 13 || !p.Name.Equals(nameof(SmServiceAccounts))) &&
|
(Version >= 13 || !p.Name.Equals(nameof(SmServiceAccounts))) &&
|
||||||
// LimitCollectionCreationDeletion was added in Version 14
|
// LimitCollectionCreationDeletion was added in Version 14
|
||||||
(Version >= 14 || !p.Name.Equals(nameof(LimitCollectionCreationDeletion))) &&
|
(Version >= 14 || !p.Name.Equals(nameof(LimitCollectionCreationDeletion))) &&
|
||||||
|
// AllowAdminAccessToAllCollectionItems was added in Version 15
|
||||||
|
(Version >= 15 || !p.Name.Equals(nameof(AllowAdminAccessToAllCollectionItems))) &&
|
||||||
(
|
(
|
||||||
!forHash ||
|
!forHash ||
|
||||||
(
|
(
|
||||||
@ -347,6 +351,10 @@ public class OrganizationLicense : ILicense
|
|||||||
// {
|
// {
|
||||||
// valid = organization.LimitCollectionCreationDeletion == LimitCollectionCreationDeletion;
|
// valid = organization.LimitCollectionCreationDeletion == LimitCollectionCreationDeletion;
|
||||||
// }
|
// }
|
||||||
|
// if (valid && Version >= 15)
|
||||||
|
// {
|
||||||
|
// valid = organization.AllowAdminAccessToAllCollectionItems == AllowAdminAccessToAllCollectionItems;
|
||||||
|
// }
|
||||||
|
|
||||||
return valid;
|
return valid;
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ public abstract class SubscriptionUpdate
|
|||||||
public abstract List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription);
|
public abstract List<SubscriptionItemOptions> RevertItemsOptions(Subscription subscription);
|
||||||
public abstract List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription);
|
public abstract List<SubscriptionItemOptions> UpgradeItemsOptions(Subscription subscription);
|
||||||
|
|
||||||
public bool UpdateNeeded(Subscription subscription)
|
public virtual bool UpdateNeeded(Subscription subscription)
|
||||||
{
|
{
|
||||||
var upgradeItemsOptions = UpgradeItemsOptions(subscription);
|
var upgradeItemsOptions = UpgradeItemsOptions(subscription);
|
||||||
foreach (var upgradeItemOptions in upgradeItemsOptions)
|
foreach (var upgradeItemOptions in upgradeItemsOptions)
|
||||||
|
@ -31,8 +31,8 @@ public record Enterprise2019Plan : Models.StaticStore.Plan
|
|||||||
UsersGetPremium = true;
|
UsersGetPremium = true;
|
||||||
HasCustomPermissions = true;
|
HasCustomPermissions = true;
|
||||||
|
|
||||||
UpgradeSortOrder = 3;
|
UpgradeSortOrder = 4;
|
||||||
DisplaySortOrder = 3;
|
DisplaySortOrder = 4;
|
||||||
LegacyYear = 2020;
|
LegacyYear = 2020;
|
||||||
|
|
||||||
SecretsManager = new Enterprise2019SecretsManagerFeatures(isAnnual);
|
SecretsManager = new Enterprise2019SecretsManagerFeatures(isAnnual);
|
||||||
|
@ -31,8 +31,8 @@ public record Enterprise2020Plan : Models.StaticStore.Plan
|
|||||||
UsersGetPremium = true;
|
UsersGetPremium = true;
|
||||||
HasCustomPermissions = true;
|
HasCustomPermissions = true;
|
||||||
|
|
||||||
UpgradeSortOrder = 3;
|
UpgradeSortOrder = 4;
|
||||||
DisplaySortOrder = 3;
|
DisplaySortOrder = 4;
|
||||||
LegacyYear = 2023;
|
LegacyYear = 2023;
|
||||||
|
|
||||||
PasswordManager = new Enterprise2020PasswordManagerFeatures(isAnnual);
|
PasswordManager = new Enterprise2020PasswordManagerFeatures(isAnnual);
|
||||||
|
@ -31,8 +31,8 @@ public record EnterprisePlan : Models.StaticStore.Plan
|
|||||||
UsersGetPremium = true;
|
UsersGetPremium = true;
|
||||||
HasCustomPermissions = true;
|
HasCustomPermissions = true;
|
||||||
|
|
||||||
UpgradeSortOrder = 3;
|
UpgradeSortOrder = 4;
|
||||||
DisplaySortOrder = 3;
|
DisplaySortOrder = 4;
|
||||||
|
|
||||||
PasswordManager = new EnterprisePasswordManagerFeatures(isAnnual);
|
PasswordManager = new EnterprisePasswordManagerFeatures(isAnnual);
|
||||||
SecretsManager = new EnterpriseSecretsManagerFeatures(isAnnual);
|
SecretsManager = new EnterpriseSecretsManagerFeatures(isAnnual);
|
||||||
|
@ -24,8 +24,8 @@ public record Teams2019Plan : Models.StaticStore.Plan
|
|||||||
HasApi = true;
|
HasApi = true;
|
||||||
UsersGetPremium = true;
|
UsersGetPremium = true;
|
||||||
|
|
||||||
UpgradeSortOrder = 2;
|
UpgradeSortOrder = 3;
|
||||||
DisplaySortOrder = 2;
|
DisplaySortOrder = 3;
|
||||||
LegacyYear = 2020;
|
LegacyYear = 2020;
|
||||||
|
|
||||||
SecretsManager = new Teams2019SecretsManagerFeatures(isAnnual);
|
SecretsManager = new Teams2019SecretsManagerFeatures(isAnnual);
|
||||||
|
@ -24,8 +24,8 @@ public record Teams2020Plan : Models.StaticStore.Plan
|
|||||||
HasApi = true;
|
HasApi = true;
|
||||||
UsersGetPremium = true;
|
UsersGetPremium = true;
|
||||||
|
|
||||||
UpgradeSortOrder = 2;
|
UpgradeSortOrder = 3;
|
||||||
DisplaySortOrder = 2;
|
DisplaySortOrder = 3;
|
||||||
LegacyYear = 2023;
|
LegacyYear = 2023;
|
||||||
|
|
||||||
PasswordManager = new Teams2020PasswordManagerFeatures(isAnnual);
|
PasswordManager = new Teams2020PasswordManagerFeatures(isAnnual);
|
||||||
|
@ -24,8 +24,8 @@ public record TeamsPlan : Models.StaticStore.Plan
|
|||||||
HasApi = true;
|
HasApi = true;
|
||||||
UsersGetPremium = true;
|
UsersGetPremium = true;
|
||||||
|
|
||||||
UpgradeSortOrder = 2;
|
UpgradeSortOrder = 3;
|
||||||
DisplaySortOrder = 2;
|
DisplaySortOrder = 3;
|
||||||
|
|
||||||
PasswordManager = new TeamsPasswordManagerFeatures(isAnnual);
|
PasswordManager = new TeamsPasswordManagerFeatures(isAnnual);
|
||||||
SecretsManager = new TeamsSecretsManagerFeatures(isAnnual);
|
SecretsManager = new TeamsSecretsManagerFeatures(isAnnual);
|
||||||
|
@ -64,6 +64,7 @@ public record TeamsStarterPlan : Plan
|
|||||||
HasAdditionalStorageOption = true;
|
HasAdditionalStorageOption = true;
|
||||||
|
|
||||||
StripePlanId = "teams-org-starter";
|
StripePlanId = "teams-org-starter";
|
||||||
|
StripeStoragePlanId = "storage-gb-monthly";
|
||||||
AdditionalStoragePricePerGb = 0.5M;
|
AdditionalStoragePricePerGb = 0.5M;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -65,9 +65,10 @@ public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseComman
|
|||||||
|
|
||||||
private async Task UpdateOrganizationAsync(SelfHostedOrganizationDetails selfHostedOrganizationDetails, OrganizationLicense license)
|
private async Task UpdateOrganizationAsync(SelfHostedOrganizationDetails selfHostedOrganizationDetails, OrganizationLicense license)
|
||||||
{
|
{
|
||||||
var flexibleCollectionsIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
var flexibleCollectionsMvpIsEnabled = _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
||||||
|
var flexibleCollectionsV1IsEnabled = _featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1, _currentContext);
|
||||||
var organization = selfHostedOrganizationDetails.ToOrganization();
|
var organization = selfHostedOrganizationDetails.ToOrganization();
|
||||||
organization.UpdateFromLicense(license, flexibleCollectionsIsEnabled);
|
organization.UpdateFromLicense(license, flexibleCollectionsMvpIsEnabled, flexibleCollectionsV1IsEnabled);
|
||||||
|
|
||||||
await _organizationService.ReplaceAndUpdateCacheAsync(organization);
|
await _organizationService.ReplaceAndUpdateCacheAsync(organization);
|
||||||
}
|
}
|
||||||
|
@ -97,11 +97,6 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
|
|||||||
throw new BadRequestException("You cannot upgrade to this plan.");
|
throw new BadRequestException("You cannot upgrade to this plan.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingPlan.Type != PlanType.Free)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("You can only upgrade from the free plan. Contact support.");
|
|
||||||
}
|
|
||||||
|
|
||||||
_organizationService.ValidatePasswordManagerPlan(newPlan, upgrade);
|
_organizationService.ValidatePasswordManagerPlan(newPlan, upgrade);
|
||||||
|
|
||||||
if (upgrade.UseSecretsManager)
|
if (upgrade.UseSecretsManager)
|
||||||
@ -226,8 +221,16 @@ public class UpgradeOrganizationPlanCommand : IUpgradeOrganizationPlanCommand
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// TODO: Update existing sub
|
paymentIntentClientSecret = await _paymentService.AdjustSubscription(
|
||||||
throw new BadRequestException("You can only upgrade from the free plan. Contact support.");
|
organization,
|
||||||
|
newPlan,
|
||||||
|
upgrade.AdditionalSeats,
|
||||||
|
upgrade.UseSecretsManager,
|
||||||
|
upgrade.AdditionalSmSeats,
|
||||||
|
upgrade.AdditionalServiceAccounts,
|
||||||
|
upgrade.AdditionalStorageGb);
|
||||||
|
|
||||||
|
success = string.IsNullOrEmpty(paymentIntentClientSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
organization.BusinessName = upgrade.BusinessName;
|
organization.BusinessName = upgrade.BusinessName;
|
||||||
|
@ -18,6 +18,15 @@ public interface IPaymentService
|
|||||||
Task<string> UpgradeFreeOrganizationAsync(Organization org, Plan plan, OrganizationUpgrade upgrade);
|
Task<string> UpgradeFreeOrganizationAsync(Organization org, Plan plan, OrganizationUpgrade upgrade);
|
||||||
Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
|
Task<string> PurchasePremiumAsync(User user, PaymentMethodType paymentMethodType, string paymentToken,
|
||||||
short additionalStorageGb, TaxInfo taxInfo);
|
short additionalStorageGb, TaxInfo taxInfo);
|
||||||
|
Task<string> AdjustSubscription(
|
||||||
|
Organization organization,
|
||||||
|
Plan updatedPlan,
|
||||||
|
int newlyPurchasedPasswordManagerSeats,
|
||||||
|
bool subscribedToSecretsManager,
|
||||||
|
int? newlyPurchasedSecretsManagerSeats,
|
||||||
|
int? newlyPurchasedAdditionalSecretsManagerServiceAccounts,
|
||||||
|
int newlyPurchasedAdditionalStorage,
|
||||||
|
DateTime? prorationDate = null);
|
||||||
Task<string> AdjustSeatsAsync(Organization organization, Plan plan, int additionalSeats, DateTime? prorationDate = null);
|
Task<string> AdjustSeatsAsync(Organization organization, Plan plan, int additionalSeats, DateTime? prorationDate = null);
|
||||||
Task<string> AdjustSmSeatsAsync(Organization organization, Plan plan, int additionalSeats, DateTime? prorationDate = null);
|
Task<string> AdjustSmSeatsAsync(Organization organization, Plan plan, int additionalSeats, DateTime? prorationDate = null);
|
||||||
Task<string> AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId, DateTime? prorationDate = null);
|
Task<string> AdjustStorageAsync(IStorableSubscriber storableSubscriber, int additionalStorage, string storagePlanId, DateTime? prorationDate = null);
|
||||||
|
@ -43,9 +43,6 @@ public class CollectionService : ICollectionService
|
|||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool UseFlexibleCollections =>
|
|
||||||
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext);
|
|
||||||
|
|
||||||
public async Task SaveAsync(Collection collection, IEnumerable<CollectionAccessSelection> groups = null,
|
public async Task SaveAsync(Collection collection, IEnumerable<CollectionAccessSelection> groups = null,
|
||||||
IEnumerable<CollectionAccessSelection> users = null)
|
IEnumerable<CollectionAccessSelection> users = null)
|
||||||
{
|
{
|
||||||
@ -59,11 +56,11 @@ public class CollectionService : ICollectionService
|
|||||||
var usersList = users?.ToList();
|
var usersList = users?.ToList();
|
||||||
|
|
||||||
// If using Flexible Collections - a collection should always have someone with Can Manage permissions
|
// If using Flexible Collections - a collection should always have someone with Can Manage permissions
|
||||||
if (UseFlexibleCollections)
|
if (_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1, _currentContext))
|
||||||
{
|
{
|
||||||
var groupHasManageAccess = groupsList?.Any(g => g.Manage) ?? false;
|
var groupHasManageAccess = groupsList?.Any(g => g.Manage) ?? false;
|
||||||
var userHasManageAccess = usersList?.Any(u => u.Manage) ?? false;
|
var userHasManageAccess = usersList?.Any(u => u.Manage) ?? false;
|
||||||
if (!groupHasManageAccess && !userHasManageAccess)
|
if (!groupHasManageAccess && !userHasManageAccess && !org.AllowAdminAccessToAllCollectionItems)
|
||||||
{
|
{
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
"At least one member or group must have can manage permission.");
|
"At least one member or group must have can manage permission.");
|
||||||
@ -125,7 +122,10 @@ public class CollectionService : ICollectionService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
var collections = await _collectionRepository.GetManyByUserIdAsync(_currentContext.UserId.Value, UseFlexibleCollections);
|
var collections = await _collectionRepository.GetManyByUserIdAsync(
|
||||||
|
_currentContext.UserId.Value,
|
||||||
|
_featureService.IsEnabled(FeatureFlagKeys.FlexibleCollections, _currentContext)
|
||||||
|
);
|
||||||
orgCollections = collections.Where(c => c.OrganizationId == organizationId);
|
orgCollections = collections.Where(c => c.OrganizationId == organizationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -741,7 +741,6 @@ public class StripePaymentService : IPaymentService
|
|||||||
SubscriptionUpdate subscriptionUpdate, DateTime? prorationDate)
|
SubscriptionUpdate subscriptionUpdate, DateTime? prorationDate)
|
||||||
{
|
{
|
||||||
// remember, when in doubt, throw
|
// remember, when in doubt, throw
|
||||||
|
|
||||||
var sub = await _stripeAdapter.SubscriptionGetAsync(storableSubscriber.GatewaySubscriptionId);
|
var sub = await _stripeAdapter.SubscriptionGetAsync(storableSubscriber.GatewaySubscriptionId);
|
||||||
if (sub == null)
|
if (sub == null)
|
||||||
{
|
{
|
||||||
@ -860,6 +859,30 @@ public class StripePaymentService : IPaymentService
|
|||||||
return paymentIntentClientSecret;
|
return paymentIntentClientSecret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<string> AdjustSubscription(
|
||||||
|
Organization organization,
|
||||||
|
StaticStore.Plan updatedPlan,
|
||||||
|
int newlyPurchasedPasswordManagerSeats,
|
||||||
|
bool subscribedToSecretsManager,
|
||||||
|
int? newlyPurchasedSecretsManagerSeats,
|
||||||
|
int? newlyPurchasedAdditionalSecretsManagerServiceAccounts,
|
||||||
|
int newlyPurchasedAdditionalStorage,
|
||||||
|
DateTime? prorationDate = null)
|
||||||
|
=> FinalizeSubscriptionChangeAsync(
|
||||||
|
organization,
|
||||||
|
new CompleteSubscriptionUpdate(
|
||||||
|
organization,
|
||||||
|
new SubscriptionData
|
||||||
|
{
|
||||||
|
Plan = updatedPlan,
|
||||||
|
PurchasedPasswordManagerSeats = newlyPurchasedPasswordManagerSeats,
|
||||||
|
SubscribedToSecretsManager = subscribedToSecretsManager,
|
||||||
|
PurchasedSecretsManagerSeats = newlyPurchasedSecretsManagerSeats,
|
||||||
|
PurchasedAdditionalSecretsManagerServiceAccounts = newlyPurchasedAdditionalSecretsManagerServiceAccounts,
|
||||||
|
PurchasedAdditionalStorage = newlyPurchasedAdditionalStorage
|
||||||
|
}),
|
||||||
|
prorationDate);
|
||||||
|
|
||||||
public Task<string> AdjustSeatsAsync(Organization organization, StaticStore.Plan plan, int additionalSeats, DateTime? prorationDate = null)
|
public Task<string> AdjustSeatsAsync(Organization organization, StaticStore.Plan plan, int additionalSeats, DateTime? prorationDate = null)
|
||||||
{
|
{
|
||||||
return FinalizeSubscriptionChangeAsync(organization, new SeatSubscriptionUpdate(organization, plan, additionalSeats), prorationDate);
|
return FinalizeSubscriptionChangeAsync(organization, new SeatSubscriptionUpdate(organization, plan, additionalSeats), prorationDate);
|
||||||
|
@ -213,7 +213,11 @@ public class Startup
|
|||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
|
||||||
// Add Cors
|
// Add Cors
|
||||||
app.UseCors(policy => policy.SetIsOriginAllowed(o => CoreHelpers.IsCorsOriginAllowed(o, globalSettings))
|
app.UseCors(policy => policy.SetIsOriginAllowed(o =>
|
||||||
|
CoreHelpers.IsCorsOriginAllowed(o, globalSettings) ||
|
||||||
|
|
||||||
|
// If development - allow requests from the Swagger UI so it can authorize
|
||||||
|
(Environment.IsDevelopment() && o == globalSettings.BaseServiceUri.Api))
|
||||||
.AllowAnyMethod().AllowAnyHeader().AllowCredentials());
|
.AllowAnyMethod().AllowAnyHeader().AllowCredentials());
|
||||||
|
|
||||||
// Add current context
|
// Add current context
|
||||||
|
@ -51,7 +51,19 @@ public class GrantRepository : BaseRepository, IGrantRepository
|
|||||||
{
|
{
|
||||||
var results = await connection.ExecuteAsync(
|
var results = await connection.ExecuteAsync(
|
||||||
"[dbo].[Grant_Save]",
|
"[dbo].[Grant_Save]",
|
||||||
obj,
|
new
|
||||||
|
{
|
||||||
|
obj.Key,
|
||||||
|
obj.Type,
|
||||||
|
obj.SubjectId,
|
||||||
|
obj.SessionId,
|
||||||
|
obj.ClientId,
|
||||||
|
obj.Description,
|
||||||
|
obj.CreationDate,
|
||||||
|
obj.ExpirationDate,
|
||||||
|
obj.ConsumedDate,
|
||||||
|
obj.Data
|
||||||
|
},
|
||||||
commandType: CommandType.StoredProcedure);
|
commandType: CommandType.StoredProcedure);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,9 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
|
|||||||
UseResetPassword = e.UseResetPassword,
|
UseResetPassword = e.UseResetPassword,
|
||||||
UseScim = e.UseScim,
|
UseScim = e.UseScim,
|
||||||
UseCustomPermissions = e.UseCustomPermissions,
|
UseCustomPermissions = e.UseCustomPermissions,
|
||||||
UsePolicies = e.UsePolicies
|
UsePolicies = e.UsePolicies,
|
||||||
|
LimitCollectionCreationDeletion = e.LimitCollectionCreationDeletion,
|
||||||
|
AllowAdminAccessToAllCollectionItems = e.AllowAdminAccessToAllCollectionItems
|
||||||
}).ToListAsync();
|
}).ToListAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -157,6 +159,8 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
|
|||||||
await deleteCiphersTransaction.CommitAsync();
|
await deleteCiphersTransaction.CommitAsync();
|
||||||
|
|
||||||
var organizationDeleteTransaction = await dbContext.Database.BeginTransactionAsync();
|
var organizationDeleteTransaction = await dbContext.Database.BeginTransactionAsync();
|
||||||
|
await dbContext.AuthRequests.Where(ar => ar.OrganizationId == organization.Id)
|
||||||
|
.ExecuteDeleteAsync();
|
||||||
await dbContext.SsoUsers.Where(su => su.OrganizationId == organization.Id)
|
await dbContext.SsoUsers.Where(su => su.OrganizationId == organization.Id)
|
||||||
.ExecuteDeleteAsync();
|
.ExecuteDeleteAsync();
|
||||||
await dbContext.SsoConfigs.Where(sc => sc.OrganizationId == organization.Id)
|
await dbContext.SsoConfigs.Where(sc => sc.OrganizationId == organization.Id)
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
using Bit.Infrastructure.EntityFramework.Auth.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.EntityFramework.Auth.Configurations;
|
||||||
|
|
||||||
|
public class GrantEntityTypeConfiguration : IEntityTypeConfiguration<Grant>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<Grant> builder)
|
||||||
|
{
|
||||||
|
builder
|
||||||
|
.HasKey(s => s.Id)
|
||||||
|
.IsClustered();
|
||||||
|
|
||||||
|
builder
|
||||||
|
.HasIndex(s => s.Key)
|
||||||
|
.IsUnique(true);
|
||||||
|
|
||||||
|
builder.ToTable(nameof(Grant));
|
||||||
|
}
|
||||||
|
}
|
@ -81,6 +81,7 @@ public class GrantRepository : BaseEntityFrameworkRepository, IGrantRepository
|
|||||||
select g).FirstOrDefaultAsync();
|
select g).FirstOrDefaultAsync();
|
||||||
if (existingGrant != null)
|
if (existingGrant != null)
|
||||||
{
|
{
|
||||||
|
obj.Id = existingGrant.Id;
|
||||||
dbContext.Entry(existingGrant).CurrentValues.SetValues(obj);
|
dbContext.Entry(existingGrant).CurrentValues.SetValues(obj);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
@ -81,7 +81,6 @@ public class DatabaseContext : DbContext
|
|||||||
var eEmergencyAccess = builder.Entity<EmergencyAccess>();
|
var eEmergencyAccess = builder.Entity<EmergencyAccess>();
|
||||||
var eEvent = builder.Entity<Event>();
|
var eEvent = builder.Entity<Event>();
|
||||||
var eFolder = builder.Entity<Folder>();
|
var eFolder = builder.Entity<Folder>();
|
||||||
var eGrant = builder.Entity<Grant>();
|
|
||||||
var eGroup = builder.Entity<Group>();
|
var eGroup = builder.Entity<Group>();
|
||||||
var eGroupUser = builder.Entity<GroupUser>();
|
var eGroupUser = builder.Entity<GroupUser>();
|
||||||
var eInstallation = builder.Entity<Installation>();
|
var eInstallation = builder.Entity<Installation>();
|
||||||
@ -134,7 +133,6 @@ public class DatabaseContext : DbContext
|
|||||||
eCollectionCipher.HasKey(cc => new { cc.CollectionId, cc.CipherId });
|
eCollectionCipher.HasKey(cc => new { cc.CollectionId, cc.CipherId });
|
||||||
eCollectionUser.HasKey(cu => new { cu.CollectionId, cu.OrganizationUserId });
|
eCollectionUser.HasKey(cu => new { cu.CollectionId, cu.OrganizationUserId });
|
||||||
eCollectionGroup.HasKey(cg => new { cg.CollectionId, cg.GroupId });
|
eCollectionGroup.HasKey(cg => new { cg.CollectionId, cg.GroupId });
|
||||||
eGrant.HasKey(x => x.Key);
|
|
||||||
eGroupUser.HasKey(gu => new { gu.GroupId, gu.OrganizationUserId });
|
eGroupUser.HasKey(gu => new { gu.GroupId, gu.OrganizationUserId });
|
||||||
|
|
||||||
var dataProtector = this.GetService<DP.IDataProtectionProvider>().CreateProtector(
|
var dataProtector = this.GetService<DP.IDataProtectionProvider>().CreateProtector(
|
||||||
@ -161,7 +159,6 @@ public class DatabaseContext : DbContext
|
|||||||
eEmergencyAccess.ToTable(nameof(EmergencyAccess));
|
eEmergencyAccess.ToTable(nameof(EmergencyAccess));
|
||||||
eEvent.ToTable(nameof(Event));
|
eEvent.ToTable(nameof(Event));
|
||||||
eFolder.ToTable(nameof(Folder));
|
eFolder.ToTable(nameof(Folder));
|
||||||
eGrant.ToTable(nameof(Grant));
|
|
||||||
eGroup.ToTable(nameof(Group));
|
eGroup.ToTable(nameof(Group));
|
||||||
eGroupUser.ToTable(nameof(GroupUser));
|
eGroupUser.ToTable(nameof(GroupUser));
|
||||||
eInstallation.ToTable(nameof(Installation));
|
eInstallation.ToTable(nameof(Installation));
|
||||||
|
@ -210,6 +210,7 @@ public class UserRepository : Repository<Core.Entities.User, User, Guid>, IUserR
|
|||||||
|
|
||||||
var transaction = await dbContext.Database.BeginTransactionAsync();
|
var transaction = await dbContext.Database.BeginTransactionAsync();
|
||||||
|
|
||||||
|
dbContext.WebAuthnCredentials.RemoveRange(dbContext.WebAuthnCredentials.Where(w => w.UserId == user.Id));
|
||||||
dbContext.Ciphers.RemoveRange(dbContext.Ciphers.Where(c => c.UserId == user.Id));
|
dbContext.Ciphers.RemoveRange(dbContext.Ciphers.Where(c => c.UserId == user.Id));
|
||||||
dbContext.Folders.RemoveRange(dbContext.Folders.Where(f => f.UserId == user.Id));
|
dbContext.Folders.RemoveRange(dbContext.Folders.Where(f => f.UserId == user.Id));
|
||||||
dbContext.AuthRequests.RemoveRange(dbContext.AuthRequests.Where(s => s.UserId == user.Id));
|
dbContext.AuthRequests.RemoveRange(dbContext.AuthRequests.Where(s => s.UserId == user.Id));
|
||||||
|
@ -70,8 +70,8 @@ public class AzureQueueHostedService : IHostedService, IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.LogError("Error processing dequeued message: " +
|
_logger.LogError(e, "Error processing dequeued message: {MessageId} x{DequeueCount}.",
|
||||||
$"{message.MessageId} x{message.DequeueCount}. {e.Message}", e);
|
message.MessageId, message.DequeueCount);
|
||||||
if (message.DequeueCount > 2)
|
if (message.DequeueCount > 2)
|
||||||
{
|
{
|
||||||
await _queueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt);
|
await _queueClient.DeleteMessageAsync(message.MessageId, message.PopReceipt);
|
||||||
@ -86,7 +86,7 @@ public class AzureQueueHostedService : IHostedService, IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
_logger.LogError("Error processing messages.", e);
|
_logger.LogError(e, "Error processing messages.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,27 +1,25 @@
|
|||||||
CREATE TABLE [dbo].[Grant] (
|
CREATE TABLE [dbo].[Grant]
|
||||||
[Key] NVARCHAR (200) NOT NULL,
|
(
|
||||||
[Type] NVARCHAR (50) NOT NULL,
|
[Id] INT NOT NULL IDENTITY,
|
||||||
[SubjectId] NVARCHAR (200) NULL,
|
[Key] NVARCHAR (200) NOT NULL,
|
||||||
[SessionId] NVARCHAR (100) NULL,
|
[Type] NVARCHAR (50) NOT NULL,
|
||||||
[ClientId] NVARCHAR (200) NOT NULL,
|
[SubjectId] NVARCHAR (200) NULL,
|
||||||
[Description] NVARCHAR (200) NULL,
|
[SessionId] NVARCHAR (100) NULL,
|
||||||
[CreationDate] DATETIME2 (7) NOT NULL,
|
[ClientId] NVARCHAR (200) NOT NULL,
|
||||||
[ExpirationDate] DATETIME2 (7) NULL,
|
[Description] NVARCHAR (200) NULL,
|
||||||
[ConsumedDate] DATETIME2 (7) NULL,
|
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||||
[Data] NVARCHAR (MAX) NOT NULL,
|
[ExpirationDate] DATETIME2 (7) NULL,
|
||||||
CONSTRAINT [PK_Grant] PRIMARY KEY CLUSTERED ([Key] ASC)
|
[ConsumedDate] DATETIME2 (7) NULL,
|
||||||
|
[Data] NVARCHAR (MAX) NOT NULL,
|
||||||
|
CONSTRAINT [PK_Grant] PRIMARY KEY CLUSTERED ([Id] ASC)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
GO
|
|
||||||
CREATE NONCLUSTERED INDEX [IX_Grant_SubjectId_ClientId_Type]
|
|
||||||
ON [dbo].[Grant]([SubjectId] ASC, [ClientId] ASC, [Type] ASC);
|
|
||||||
|
|
||||||
GO
|
|
||||||
CREATE NONCLUSTERED INDEX [IX_Grant_SubjectId_SessionId_Type]
|
|
||||||
ON [dbo].[Grant]([SubjectId] ASC, [SessionId] ASC, [Type] ASC);
|
|
||||||
|
|
||||||
GO
|
GO
|
||||||
CREATE NONCLUSTERED INDEX [IX_Grant_ExpirationDate]
|
CREATE NONCLUSTERED INDEX [IX_Grant_ExpirationDate]
|
||||||
ON [dbo].[Grant]([ExpirationDate] ASC);
|
ON [dbo].[Grant]([ExpirationDate] ASC);
|
||||||
|
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX [IX_Grant_Key]
|
||||||
|
ON [dbo].[Grant]([Key]);
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
CREATE PROCEDURE [dbo].[Organization_DeleteById]
|
CREATE PROCEDURE [dbo].[Organization_DeleteById]
|
||||||
@Id UNIQUEIDENTIFIER
|
@Id UNIQUEIDENTIFIER
|
||||||
|
WITH RECOMPILE
|
||||||
AS
|
AS
|
||||||
BEGIN
|
BEGIN
|
||||||
SET NOCOUNT ON
|
SET NOCOUNT ON
|
||||||
@ -25,6 +26,12 @@ BEGIN
|
|||||||
|
|
||||||
BEGIN TRANSACTION Organization_DeleteById
|
BEGIN TRANSACTION Organization_DeleteById
|
||||||
|
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[AuthRequest]
|
||||||
|
WHERE
|
||||||
|
[OrganizationId] = @Id
|
||||||
|
|
||||||
DELETE
|
DELETE
|
||||||
FROM
|
FROM
|
||||||
[dbo].[SsoUser]
|
[dbo].[SsoUser]
|
||||||
|
@ -20,7 +20,9 @@ BEGIN
|
|||||||
[UseScim],
|
[UseScim],
|
||||||
[UseResetPassword],
|
[UseResetPassword],
|
||||||
[UsePolicies],
|
[UsePolicies],
|
||||||
[Enabled]
|
[Enabled],
|
||||||
|
[LimitCollectionCreationDeletion],
|
||||||
|
[AllowAdminAccessToAllCollectionItems]
|
||||||
FROM
|
FROM
|
||||||
[dbo].[Organization]
|
[dbo].[Organization]
|
||||||
END
|
END
|
||||||
|
@ -24,6 +24,13 @@ BEGIN
|
|||||||
|
|
||||||
BEGIN TRANSACTION User_DeleteById
|
BEGIN TRANSACTION User_DeleteById
|
||||||
|
|
||||||
|
-- Delete WebAuthnCredentials
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[WebAuthnCredential]
|
||||||
|
WHERE
|
||||||
|
[UserId] = @Id
|
||||||
|
|
||||||
-- Delete folders
|
-- Delete folders
|
||||||
DELETE
|
DELETE
|
||||||
FROM
|
FROM
|
||||||
|
@ -142,7 +142,7 @@ public class CollectionsControllerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task GetOrganizationCollectionsWithGroups_MissingReadPermissions_ThrowsNotFound(Organization organization, Guid userId, SutProvider<CollectionsController> sutProvider)
|
public async Task GetOrganizationCollections_WithReadAllPermissions_GetsAllCollections(Organization organization, ICollection<Collection> collections, Guid userId, SutProvider<CollectionsController> sutProvider)
|
||||||
{
|
{
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||||
|
|
||||||
@ -152,7 +152,37 @@ public class CollectionsControllerTests
|
|||||||
Arg.Any<object>(),
|
Arg.Any<object>(),
|
||||||
Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements =>
|
Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements =>
|
||||||
requirements.Cast<CollectionOperationRequirement>().All(operation =>
|
requirements.Cast<CollectionOperationRequirement>().All(operation =>
|
||||||
operation.Name == nameof(CollectionOperations.ReadAllWithAccess)
|
operation.Name == nameof(CollectionOperations.ReadAll)
|
||||||
|
&& operation.OrganizationId == organization.Id)))
|
||||||
|
.Returns(AuthorizationResult.Success());
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICollectionRepository>()
|
||||||
|
.GetManyByOrganizationIdAsync(organization.Id)
|
||||||
|
.Returns(collections);
|
||||||
|
|
||||||
|
var response = await sutProvider.Sut.Get(organization.Id);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByOrganizationIdAsync(organization.Id);
|
||||||
|
|
||||||
|
Assert.Equal(collections.Count, response.Data.Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task GetOrganizationCollections_MissingReadAllPermissions_GetsManageableCollections(Organization organization, ICollection<CollectionDetails> collections, Guid userId, SutProvider<CollectionsController> sutProvider)
|
||||||
|
{
|
||||||
|
collections.First().OrganizationId = organization.Id;
|
||||||
|
collections.First().Manage = true;
|
||||||
|
collections.Skip(1).First().OrganizationId = organization.Id;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IAuthorizationService>()
|
||||||
|
.AuthorizeAsync(
|
||||||
|
Arg.Any<ClaimsPrincipal>(),
|
||||||
|
Arg.Any<object>(),
|
||||||
|
Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements =>
|
||||||
|
requirements.Cast<CollectionOperationRequirement>().All(operation =>
|
||||||
|
operation.Name == nameof(CollectionOperations.ReadAll)
|
||||||
&& operation.OrganizationId == organization.Id)))
|
&& operation.OrganizationId == organization.Id)))
|
||||||
.Returns(AuthorizationResult.Failed());
|
.Returns(AuthorizationResult.Failed());
|
||||||
|
|
||||||
@ -162,10 +192,20 @@ public class CollectionsControllerTests
|
|||||||
Arg.Any<object>(),
|
Arg.Any<object>(),
|
||||||
Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements =>
|
Arg.Is<IEnumerable<IAuthorizationRequirement>>(requirements =>
|
||||||
requirements.Cast<BulkCollectionOperationRequirement>().All(operation =>
|
requirements.Cast<BulkCollectionOperationRequirement>().All(operation =>
|
||||||
operation.Name == nameof(BulkCollectionOperations.ReadWithAccess))))
|
operation.Name == nameof(BulkCollectionOperations.Read))))
|
||||||
.Returns(AuthorizationResult.Failed());
|
.Returns(AuthorizationResult.Success());
|
||||||
|
|
||||||
_ = await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.GetManyWithDetails(organization.Id));
|
sutProvider.GetDependency<ICollectionRepository>()
|
||||||
|
.GetManyByUserIdAsync(userId, true)
|
||||||
|
.Returns(collections);
|
||||||
|
|
||||||
|
var result = await sutProvider.Sut.Get(organization.Id);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<ICollectionRepository>().DidNotReceive().GetManyByOrganizationIdAsync(organization.Id);
|
||||||
|
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdAsync(userId, true);
|
||||||
|
|
||||||
|
Assert.Single(result.Data);
|
||||||
|
Assert.All(result.Data, c => Assert.Equal(organization.Id, c.OrganizationId));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
|
@ -6,7 +6,9 @@ using Bit.Core.Entities;
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Models.Data.Organizations;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Test.AutoFixture;
|
using Bit.Core.Test.AutoFixture;
|
||||||
using Bit.Core.Test.Vault.AutoFixture;
|
using Bit.Core.Test.Vault.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
@ -31,9 +33,10 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
CurrentContextOrganization organization)
|
CurrentContextOrganization organization)
|
||||||
{
|
{
|
||||||
organization.Type = userType;
|
organization.Type = userType;
|
||||||
organization.LimitCollectionCreationDeletion = true;
|
|
||||||
organization.Permissions = new Permissions();
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
|
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, true);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { BulkCollectionOperations.Create },
|
new[] { BulkCollectionOperations.Create },
|
||||||
new ClaimsPrincipal(),
|
new ClaimsPrincipal(),
|
||||||
@ -41,6 +44,7 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
@ -56,7 +60,8 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
var actingUserId = Guid.NewGuid();
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
organization.Type = OrganizationUserType.User;
|
organization.Type = OrganizationUserType.User;
|
||||||
organization.LimitCollectionCreationDeletion = false;
|
|
||||||
|
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, false);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { BulkCollectionOperations.Create },
|
new[] { BulkCollectionOperations.Create },
|
||||||
@ -65,6 +70,7 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
@ -83,7 +89,6 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
var actingUserId = Guid.NewGuid();
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
organization.Type = userType;
|
organization.Type = userType;
|
||||||
organization.LimitCollectionCreationDeletion = true;
|
|
||||||
organization.Permissions = new Permissions
|
organization.Permissions = new Permissions
|
||||||
{
|
{
|
||||||
EditAnyCollection = false,
|
EditAnyCollection = false,
|
||||||
@ -92,6 +97,8 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
ManageUsers = false
|
ManageUsers = false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, true);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { BulkCollectionOperations.Create },
|
new[] { BulkCollectionOperations.Create },
|
||||||
new ClaimsPrincipal(),
|
new ClaimsPrincipal(),
|
||||||
@ -99,6 +106,8 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
@ -111,6 +120,8 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
ICollection<Collection> collections,
|
ICollection<Collection> collections,
|
||||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider)
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider)
|
||||||
{
|
{
|
||||||
|
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(collections.First().OrganizationId, true);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { BulkCollectionOperations.Create },
|
new[] { BulkCollectionOperations.Create },
|
||||||
new ClaimsPrincipal(),
|
new ClaimsPrincipal(),
|
||||||
@ -119,6 +130,8 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
Assert.False(context.HasSucceeded);
|
Assert.False(context.HasSucceeded);
|
||||||
@ -134,7 +147,6 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
CurrentContextOrganization organization)
|
CurrentContextOrganization organization)
|
||||||
{
|
{
|
||||||
organization.Type = userType;
|
organization.Type = userType;
|
||||||
organization.LimitCollectionCreationDeletion = true;
|
|
||||||
organization.Permissions = new Permissions();
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
var operationsToTest = new[]
|
var operationsToTest = new[]
|
||||||
@ -204,15 +216,19 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData, CollectionCustomization]
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
public async Task CanReadAsync_WhenUserIsAssignedToCollections_Success(
|
public async Task CanReadAsync_WhenUserCanManageCollections_Success(
|
||||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
ICollection<CollectionDetails> collections,
|
ICollection<CollectionDetails> collections,
|
||||||
CurrentContextOrganization organization)
|
CurrentContextOrganization organization)
|
||||||
{
|
{
|
||||||
var actingUserId = Guid.NewGuid();
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
foreach (var c in collections)
|
||||||
|
{
|
||||||
|
c.Manage = true;
|
||||||
|
}
|
||||||
|
|
||||||
organization.Type = OrganizationUserType.User;
|
organization.Type = OrganizationUserType.User;
|
||||||
organization.LimitCollectionCreationDeletion = false;
|
|
||||||
organization.Permissions = new Permissions();
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
var operationsToTest = new[]
|
var operationsToTest = new[]
|
||||||
@ -249,7 +265,6 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
var actingUserId = Guid.NewGuid();
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
organization.Type = OrganizationUserType.User;
|
organization.Type = OrganizationUserType.User;
|
||||||
organization.LimitCollectionCreationDeletion = false;
|
|
||||||
organization.Permissions = new Permissions();
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
var operationsToTest = new[]
|
var operationsToTest = new[]
|
||||||
@ -288,7 +303,6 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
var actingUserId = Guid.NewGuid();
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
organization.Type = userType;
|
organization.Type = userType;
|
||||||
organization.LimitCollectionCreationDeletion = true;
|
|
||||||
organization.Permissions = new Permissions
|
organization.Permissions = new Permissions
|
||||||
{
|
{
|
||||||
EditAnyCollection = false,
|
EditAnyCollection = false,
|
||||||
@ -362,7 +376,6 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
CurrentContextOrganization organization)
|
CurrentContextOrganization organization)
|
||||||
{
|
{
|
||||||
organization.Type = userType;
|
organization.Type = userType;
|
||||||
organization.LimitCollectionCreationDeletion = true;
|
|
||||||
organization.Permissions = new Permissions();
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||||
@ -426,7 +439,6 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
organization.Type = OrganizationUserType.User;
|
organization.Type = OrganizationUserType.User;
|
||||||
organization.LimitCollectionCreationDeletion = false;
|
|
||||||
organization.Permissions = new Permissions();
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
@ -457,7 +469,6 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
organization.Type = OrganizationUserType.User;
|
organization.Type = OrganizationUserType.User;
|
||||||
organization.LimitCollectionCreationDeletion = false;
|
|
||||||
organization.Permissions = new Permissions();
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
@ -486,7 +497,6 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
var actingUserId = Guid.NewGuid();
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
organization.Type = userType;
|
organization.Type = userType;
|
||||||
organization.LimitCollectionCreationDeletion = true;
|
|
||||||
organization.Permissions = new Permissions
|
organization.Permissions = new Permissions
|
||||||
{
|
{
|
||||||
EditAnyCollection = false,
|
EditAnyCollection = false,
|
||||||
@ -538,7 +548,6 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
CurrentContextOrganization organization)
|
CurrentContextOrganization organization)
|
||||||
{
|
{
|
||||||
organization.Type = userType;
|
organization.Type = userType;
|
||||||
organization.LimitCollectionCreationDeletion = true;
|
|
||||||
organization.Permissions = new Permissions();
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
var operationsToTest = new[]
|
var operationsToTest = new[]
|
||||||
@ -656,7 +665,6 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
var actingUserId = Guid.NewGuid();
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
organization.Type = userType;
|
organization.Type = userType;
|
||||||
organization.LimitCollectionCreationDeletion = false;
|
|
||||||
organization.Permissions = new Permissions
|
organization.Permissions = new Permissions
|
||||||
{
|
{
|
||||||
EditAnyCollection = false,
|
EditAnyCollection = false,
|
||||||
@ -737,9 +745,10 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
CurrentContextOrganization organization)
|
CurrentContextOrganization organization)
|
||||||
{
|
{
|
||||||
organization.Type = userType;
|
organization.Type = userType;
|
||||||
organization.LimitCollectionCreationDeletion = true;
|
|
||||||
organization.Permissions = new Permissions();
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
|
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, true);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { BulkCollectionOperations.Delete },
|
new[] { BulkCollectionOperations.Delete },
|
||||||
new ClaimsPrincipal(),
|
new ClaimsPrincipal(),
|
||||||
@ -747,6 +756,8 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync()
|
||||||
|
.Returns(organizationAbilities);
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
@ -762,12 +773,13 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
var actingUserId = Guid.NewGuid();
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
organization.Type = OrganizationUserType.Custom;
|
organization.Type = OrganizationUserType.Custom;
|
||||||
organization.LimitCollectionCreationDeletion = false;
|
|
||||||
organization.Permissions = new Permissions
|
organization.Permissions = new Permissions
|
||||||
{
|
{
|
||||||
DeleteAnyCollection = true
|
DeleteAnyCollection = true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, true);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { BulkCollectionOperations.Delete },
|
new[] { BulkCollectionOperations.Delete },
|
||||||
new ClaimsPrincipal(),
|
new ClaimsPrincipal(),
|
||||||
@ -775,6 +787,8 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync()
|
||||||
|
.Returns(organizationAbilities);
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
@ -792,9 +806,13 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
organization.Type = OrganizationUserType.User;
|
organization.Type = OrganizationUserType.User;
|
||||||
organization.Permissions = new Permissions();
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
|
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, false);
|
||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId, Arg.Any<bool>()).Returns(collections);
|
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId, Arg.Any<bool>()).Returns(collections);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync()
|
||||||
|
.Returns(organizationAbilities);
|
||||||
|
|
||||||
foreach (var c in collections)
|
foreach (var c in collections)
|
||||||
{
|
{
|
||||||
@ -823,7 +841,6 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
var actingUserId = Guid.NewGuid();
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
organization.Type = userType;
|
organization.Type = userType;
|
||||||
organization.LimitCollectionCreationDeletion = true;
|
|
||||||
organization.Permissions = new Permissions
|
organization.Permissions = new Permissions
|
||||||
{
|
{
|
||||||
EditAnyCollection = false,
|
EditAnyCollection = false,
|
||||||
@ -832,6 +849,8 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
ManageUsers = false
|
ManageUsers = false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, true);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { BulkCollectionOperations.Delete },
|
new[] { BulkCollectionOperations.Delete },
|
||||||
new ClaimsPrincipal(),
|
new ClaimsPrincipal(),
|
||||||
@ -839,6 +858,9 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync()
|
||||||
|
.Returns(organizationAbilities);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
@ -859,6 +881,7 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
Assert.False(context.HasSucceeded);
|
Assert.False(context.HasSucceeded);
|
||||||
@ -914,6 +937,16 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
var actingUserId = Guid.NewGuid();
|
var actingUserId = Guid.NewGuid();
|
||||||
var orgId = collections.First().OrganizationId;
|
var orgId = collections.First().OrganizationId;
|
||||||
|
|
||||||
|
var organizationAbilities = new Dictionary<Guid, OrganizationAbility>
|
||||||
|
{
|
||||||
|
{ collections.First().OrganizationId,
|
||||||
|
new OrganizationAbility
|
||||||
|
{
|
||||||
|
LimitCollectionCreationDeletion = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
var operationsToTest = new[]
|
var operationsToTest = new[]
|
||||||
{
|
{
|
||||||
BulkCollectionOperations.Create,
|
BulkCollectionOperations.Create,
|
||||||
@ -928,6 +961,8 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
{
|
{
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(orgId).Returns((CurrentContextOrganization)null);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(orgId).Returns((CurrentContextOrganization)null);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync()
|
||||||
|
.Returns(organizationAbilities);
|
||||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(true);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
@ -945,4 +980,18 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
sutProvider.Recreate();
|
sutProvider.Recreate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Dictionary<Guid, OrganizationAbility> ArrangeOrganizationAbilitiesDictionary(Guid orgId,
|
||||||
|
bool limitCollectionCreationDeletion)
|
||||||
|
{
|
||||||
|
return new Dictionary<Guid, OrganizationAbility>
|
||||||
|
{
|
||||||
|
{ orgId,
|
||||||
|
new OrganizationAbility
|
||||||
|
{
|
||||||
|
LimitCollectionCreationDeletion = limitCollectionCreationDeletion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@ using Bit.Core;
|
|||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Models.Data.Organizations;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Test.AutoFixture;
|
using Bit.Core.Test.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
@ -26,9 +28,10 @@ public class GroupAuthorizationHandlerTests
|
|||||||
CurrentContextOrganization organization)
|
CurrentContextOrganization organization)
|
||||||
{
|
{
|
||||||
organization.Type = userType;
|
organization.Type = userType;
|
||||||
organization.LimitCollectionCreationDeletion = true;
|
|
||||||
organization.Permissions = new Permissions();
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
|
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, true);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { GroupOperations.ReadAll(organization.Id) },
|
new[] { GroupOperations.ReadAll(organization.Id) },
|
||||||
new ClaimsPrincipal(),
|
new ClaimsPrincipal(),
|
||||||
@ -36,6 +39,7 @@ public class GroupAuthorizationHandlerTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
@ -48,9 +52,10 @@ public class GroupAuthorizationHandlerTests
|
|||||||
SutProvider<GroupAuthorizationHandler> sutProvider, CurrentContextOrganization organization)
|
SutProvider<GroupAuthorizationHandler> sutProvider, CurrentContextOrganization organization)
|
||||||
{
|
{
|
||||||
organization.Type = OrganizationUserType.User;
|
organization.Type = OrganizationUserType.User;
|
||||||
organization.LimitCollectionCreationDeletion = true;
|
|
||||||
organization.Permissions = new Permissions();
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
|
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, true);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { GroupOperations.ReadAll(organization.Id) },
|
new[] { GroupOperations.ReadAll(organization.Id) },
|
||||||
new ClaimsPrincipal(),
|
new ClaimsPrincipal(),
|
||||||
@ -59,6 +64,7 @@ public class GroupAuthorizationHandlerTests
|
|||||||
sutProvider.GetDependency<ICurrentContext>()
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
.UserId
|
.UserId
|
||||||
.Returns(userId);
|
.Returns(userId);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
|
||||||
sutProvider.GetDependency<ICurrentContext>()
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
.ProviderUserForOrgAsync(organization.Id)
|
.ProviderUserForOrgAsync(organization.Id)
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
@ -83,7 +89,6 @@ public class GroupAuthorizationHandlerTests
|
|||||||
var actingUserId = Guid.NewGuid();
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
organization.Type = OrganizationUserType.Custom;
|
organization.Type = OrganizationUserType.Custom;
|
||||||
organization.LimitCollectionCreationDeletion = limitCollectionCreationDeletion;
|
|
||||||
organization.Permissions = new Permissions
|
organization.Permissions = new Permissions
|
||||||
{
|
{
|
||||||
EditAnyCollection = editAnyCollection,
|
EditAnyCollection = editAnyCollection,
|
||||||
@ -92,6 +97,8 @@ public class GroupAuthorizationHandlerTests
|
|||||||
ManageUsers = manageUsers
|
ManageUsers = manageUsers
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, limitCollectionCreationDeletion);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { GroupOperations.ReadAll(organization.Id) },
|
new[] { GroupOperations.ReadAll(organization.Id) },
|
||||||
new ClaimsPrincipal(),
|
new ClaimsPrincipal(),
|
||||||
@ -99,6 +106,7 @@ public class GroupAuthorizationHandlerTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
@ -116,7 +124,6 @@ public class GroupAuthorizationHandlerTests
|
|||||||
var actingUserId = Guid.NewGuid();
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
organization.Type = userType;
|
organization.Type = userType;
|
||||||
organization.LimitCollectionCreationDeletion = true;
|
|
||||||
organization.Permissions = new Permissions
|
organization.Permissions = new Permissions
|
||||||
{
|
{
|
||||||
EditAnyCollection = false,
|
EditAnyCollection = false,
|
||||||
@ -126,6 +133,8 @@ public class GroupAuthorizationHandlerTests
|
|||||||
AccessImportExport = false
|
AccessImportExport = false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, true);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { GroupOperations.ReadAll(organization.Id) },
|
new[] { GroupOperations.ReadAll(organization.Id) },
|
||||||
new ClaimsPrincipal(),
|
new ClaimsPrincipal(),
|
||||||
@ -133,6 +142,8 @@ public class GroupAuthorizationHandlerTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
@ -145,6 +156,8 @@ public class GroupAuthorizationHandlerTests
|
|||||||
Guid organizationId,
|
Guid organizationId,
|
||||||
SutProvider<GroupAuthorizationHandler> sutProvider)
|
SutProvider<GroupAuthorizationHandler> sutProvider)
|
||||||
{
|
{
|
||||||
|
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organizationId, true);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { GroupOperations.ReadAll(organizationId) },
|
new[] { GroupOperations.ReadAll(organizationId) },
|
||||||
new ClaimsPrincipal(),
|
new ClaimsPrincipal(),
|
||||||
@ -153,6 +166,8 @@ public class GroupAuthorizationHandlerTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
Assert.False(context.HasSucceeded);
|
Assert.False(context.HasSucceeded);
|
||||||
@ -194,4 +209,18 @@ public class GroupAuthorizationHandlerTests
|
|||||||
Assert.False(context.HasSucceeded);
|
Assert.False(context.HasSucceeded);
|
||||||
Assert.True(context.HasFailed);
|
Assert.True(context.HasFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Dictionary<Guid, OrganizationAbility> ArrangeOrganizationAbilitiesDictionary(Guid orgId,
|
||||||
|
bool limitCollectionCreationDeletion)
|
||||||
|
{
|
||||||
|
return new Dictionary<Guid, OrganizationAbility>
|
||||||
|
{
|
||||||
|
{ orgId,
|
||||||
|
new OrganizationAbility
|
||||||
|
{
|
||||||
|
LimitCollectionCreationDeletion = limitCollectionCreationDeletion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@ using Bit.Core;
|
|||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
|
using Bit.Core.Models.Data.Organizations;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Test.AutoFixture;
|
using Bit.Core.Test.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
@ -26,9 +28,10 @@ public class OrganizationUserAuthorizationHandlerTests
|
|||||||
CurrentContextOrganization organization)
|
CurrentContextOrganization organization)
|
||||||
{
|
{
|
||||||
organization.Type = userType;
|
organization.Type = userType;
|
||||||
organization.LimitCollectionCreationDeletion = true;
|
|
||||||
organization.Permissions = new Permissions();
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
|
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, true);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { OrganizationUserOperations.ReadAll(organization.Id) },
|
new[] { OrganizationUserOperations.ReadAll(organization.Id) },
|
||||||
new ClaimsPrincipal(),
|
new ClaimsPrincipal(),
|
||||||
@ -36,6 +39,7 @@ public class OrganizationUserAuthorizationHandlerTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
@ -48,9 +52,10 @@ public class OrganizationUserAuthorizationHandlerTests
|
|||||||
SutProvider<OrganizationUserAuthorizationHandler> sutProvider, CurrentContextOrganization organization)
|
SutProvider<OrganizationUserAuthorizationHandler> sutProvider, CurrentContextOrganization organization)
|
||||||
{
|
{
|
||||||
organization.Type = OrganizationUserType.User;
|
organization.Type = OrganizationUserType.User;
|
||||||
organization.LimitCollectionCreationDeletion = true;
|
|
||||||
organization.Permissions = new Permissions();
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
|
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, true);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { OrganizationUserOperations.ReadAll(organization.Id) },
|
new[] { OrganizationUserOperations.ReadAll(organization.Id) },
|
||||||
new ClaimsPrincipal(),
|
new ClaimsPrincipal(),
|
||||||
@ -59,6 +64,7 @@ public class OrganizationUserAuthorizationHandlerTests
|
|||||||
sutProvider.GetDependency<ICurrentContext>()
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
.UserId
|
.UserId
|
||||||
.Returns(userId);
|
.Returns(userId);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
|
||||||
sutProvider.GetDependency<ICurrentContext>()
|
sutProvider.GetDependency<ICurrentContext>()
|
||||||
.ProviderUserForOrgAsync(organization.Id)
|
.ProviderUserForOrgAsync(organization.Id)
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
@ -83,7 +89,6 @@ public class OrganizationUserAuthorizationHandlerTests
|
|||||||
var actingUserId = Guid.NewGuid();
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
organization.Type = OrganizationUserType.Custom;
|
organization.Type = OrganizationUserType.Custom;
|
||||||
organization.LimitCollectionCreationDeletion = limitCollectionCreationDeletion;
|
|
||||||
organization.Permissions = new Permissions
|
organization.Permissions = new Permissions
|
||||||
{
|
{
|
||||||
EditAnyCollection = editAnyCollection,
|
EditAnyCollection = editAnyCollection,
|
||||||
@ -92,6 +97,8 @@ public class OrganizationUserAuthorizationHandlerTests
|
|||||||
ManageUsers = manageUsers
|
ManageUsers = manageUsers
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, limitCollectionCreationDeletion);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { OrganizationUserOperations.ReadAll(organization.Id) },
|
new[] { OrganizationUserOperations.ReadAll(organization.Id) },
|
||||||
new ClaimsPrincipal(),
|
new ClaimsPrincipal(),
|
||||||
@ -99,6 +106,7 @@ public class OrganizationUserAuthorizationHandlerTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
@ -116,7 +124,6 @@ public class OrganizationUserAuthorizationHandlerTests
|
|||||||
var actingUserId = Guid.NewGuid();
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
organization.Type = userType;
|
organization.Type = userType;
|
||||||
organization.LimitCollectionCreationDeletion = true;
|
|
||||||
organization.Permissions = new Permissions
|
organization.Permissions = new Permissions
|
||||||
{
|
{
|
||||||
EditAnyCollection = false,
|
EditAnyCollection = false,
|
||||||
@ -125,6 +132,8 @@ public class OrganizationUserAuthorizationHandlerTests
|
|||||||
ManageUsers = false
|
ManageUsers = false
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organization.Id, true);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { OrganizationUserOperations.ReadAll(organization.Id) },
|
new[] { OrganizationUserOperations.ReadAll(organization.Id) },
|
||||||
new ClaimsPrincipal(),
|
new ClaimsPrincipal(),
|
||||||
@ -132,6 +141,8 @@ public class OrganizationUserAuthorizationHandlerTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
@ -144,6 +155,8 @@ public class OrganizationUserAuthorizationHandlerTests
|
|||||||
Guid organizationId,
|
Guid organizationId,
|
||||||
SutProvider<OrganizationUserAuthorizationHandler> sutProvider)
|
SutProvider<OrganizationUserAuthorizationHandler> sutProvider)
|
||||||
{
|
{
|
||||||
|
var organizationAbilities = ArrangeOrganizationAbilitiesDictionary(organizationId, true);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { OrganizationUserOperations.ReadAll(organizationId) },
|
new[] { OrganizationUserOperations.ReadAll(organizationId) },
|
||||||
new ClaimsPrincipal(),
|
new ClaimsPrincipal(),
|
||||||
@ -152,6 +165,8 @@ public class OrganizationUserAuthorizationHandlerTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync().Returns(organizationAbilities);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
Assert.False(context.HasSucceeded);
|
Assert.False(context.HasSucceeded);
|
||||||
@ -191,4 +206,18 @@ public class OrganizationUserAuthorizationHandlerTests
|
|||||||
|
|
||||||
Assert.True(context.HasFailed);
|
Assert.True(context.HasFailed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Dictionary<Guid, OrganizationAbility> ArrangeOrganizationAbilitiesDictionary(Guid orgId,
|
||||||
|
bool limitCollectionCreationDeletion)
|
||||||
|
{
|
||||||
|
return new Dictionary<Guid, OrganizationAbility>
|
||||||
|
{
|
||||||
|
{ orgId,
|
||||||
|
new OrganizationAbility
|
||||||
|
{
|
||||||
|
LimitCollectionCreationDeletion = limitCollectionCreationDeletion
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -147,6 +147,40 @@ public class SecretsManagerOrganizationCustomization : ICustomization
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal class TeamsStarterOrganizationCustomization : ICustomization
|
||||||
|
{
|
||||||
|
public void Customize(IFixture fixture)
|
||||||
|
{
|
||||||
|
var organizationId = Guid.NewGuid();
|
||||||
|
const PlanType planType = PlanType.TeamsStarter;
|
||||||
|
|
||||||
|
fixture.Customize<Organization>(composer =>
|
||||||
|
composer
|
||||||
|
.With(organization => organization.Id, organizationId)
|
||||||
|
.With(organization => organization.PlanType, planType)
|
||||||
|
.With(organization => organization.Seats, 10)
|
||||||
|
.Without(organization => organization.MaxStorageGb));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class TeamsMonthlyWithAddOnsOrganizationCustomization : ICustomization
|
||||||
|
{
|
||||||
|
public void Customize(IFixture fixture)
|
||||||
|
{
|
||||||
|
var organizationId = Guid.NewGuid();
|
||||||
|
const PlanType planType = PlanType.TeamsMonthly;
|
||||||
|
|
||||||
|
fixture.Customize<Organization>(composer =>
|
||||||
|
composer
|
||||||
|
.With(organization => organization.Id, organizationId)
|
||||||
|
.With(organization => organization.PlanType, planType)
|
||||||
|
.With(organization => organization.Seats, 20)
|
||||||
|
.With(organization => organization.UseSecretsManager, true)
|
||||||
|
.With(organization => organization.SmSeats, 5)
|
||||||
|
.With(organization => organization.SmServiceAccounts, 53));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal class OrganizationCustomizeAttribute : BitCustomizeAttribute
|
internal class OrganizationCustomizeAttribute : BitCustomizeAttribute
|
||||||
{
|
{
|
||||||
public bool UseGroups { get; set; }
|
public bool UseGroups { get; set; }
|
||||||
@ -189,6 +223,16 @@ internal class SecretsManagerOrganizationCustomizeAttribute : BitCustomizeAttrib
|
|||||||
new SecretsManagerOrganizationCustomization();
|
new SecretsManagerOrganizationCustomization();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal class TeamsStarterOrganizationCustomizeAttribute : BitCustomizeAttribute
|
||||||
|
{
|
||||||
|
public override ICustomization GetCustomization() => new TeamsStarterOrganizationCustomization();
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class TeamsMonthlyWithAddOnsOrganizationCustomizeAttribute : BitCustomizeAttribute
|
||||||
|
{
|
||||||
|
public override ICustomization GetCustomization() => new TeamsMonthlyWithAddOnsOrganizationCustomization();
|
||||||
|
}
|
||||||
|
|
||||||
internal class EphemeralDataProtectionCustomization : ICustomization
|
internal class EphemeralDataProtectionCustomization : ICustomization
|
||||||
{
|
{
|
||||||
public void Customize(IFixture fixture)
|
public void Customize(IFixture fixture)
|
||||||
|
@ -432,6 +432,55 @@ public class OrganizationServiceTests
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[OrganizationInviteCustomize, BitAutoData]
|
||||||
|
public async Task InviteUser_SsoOrgWithNullSsoConfig_Passes(Organization organization, OrganizationUser invitor,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
||||||
|
{
|
||||||
|
// Setup FakeDataProtectorTokenFactory for creating new tokens - this must come first in order to avoid resetting mocks
|
||||||
|
sutProvider.SetDependency(_orgUserInviteTokenDataFactory, "orgUserInviteTokenDataFactory");
|
||||||
|
sutProvider.Create();
|
||||||
|
|
||||||
|
// Org must be able to use SSO to trigger this proper test case as we currently only call to retrieve
|
||||||
|
// an org's SSO config if the org can use SSO
|
||||||
|
organization.UseSso = true;
|
||||||
|
|
||||||
|
// Return null for sso config
|
||||||
|
sutProvider.GetDependency<ISsoConfigRepository>().GetByOrganizationIdAsync(organization.Id).ReturnsNull();
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(true);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(true);
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
organizationUserRepository.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner)
|
||||||
|
.Returns(new[] { owner });
|
||||||
|
|
||||||
|
// Must set guids in order for dictionary of guids to not throw aggregate exceptions
|
||||||
|
SetupOrgUserRepositoryCreateManyAsyncMock(organizationUserRepository);
|
||||||
|
|
||||||
|
// Mock tokenable factory to return a token that expires in 5 days
|
||||||
|
sutProvider.GetDependency<IOrgUserInviteTokenableFactory>()
|
||||||
|
.CreateToken(Arg.Any<OrganizationUser>())
|
||||||
|
.Returns(
|
||||||
|
info => new OrgUserInviteTokenable(info.Arg<OrganizationUser>())
|
||||||
|
{
|
||||||
|
ExpirationDate = DateTime.UtcNow.Add(TimeSpan.FromDays(5))
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
await sutProvider.Sut.InviteUsersAsync(organization.Id, invitor.UserId, new (OrganizationUserInvite, string)[] { (invite, null) });
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IMailService>().Received(1)
|
||||||
|
.SendOrganizationInviteEmailsAsync(Arg.Is<OrganizationInvitesInfo>(info =>
|
||||||
|
info.OrgUserTokenPairs.Count() == invite.Emails.Distinct().Count() &&
|
||||||
|
info.IsFreeOrg == (organization.PlanType == PlanType.Free) &&
|
||||||
|
info.OrganizationName == organization.Name));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[OrganizationInviteCustomize(
|
[OrganizationInviteCustomize(
|
||||||
InviteeUserType = OrganizationUserType.Admin,
|
InviteeUserType = OrganizationUserType.Admin,
|
||||||
|
@ -0,0 +1,530 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.Test.AutoFixture.OrganizationFixtures;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Stripe;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.Models.Business;
|
||||||
|
|
||||||
|
public class CompleteSubscriptionUpdateTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[TeamsStarterOrganizationCustomize]
|
||||||
|
public void UpgradeItemOptions_TeamsStarterToTeams_ReturnsCorrectOptions(
|
||||||
|
Organization organization)
|
||||||
|
{
|
||||||
|
var teamsStarterPlan = StaticStore.GetPlan(PlanType.TeamsStarter);
|
||||||
|
|
||||||
|
var subscription = new Subscription
|
||||||
|
{
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "subscription_item",
|
||||||
|
Price = new Price { Id = teamsStarterPlan.PasswordManager.StripePlanId },
|
||||||
|
Quantity = 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||||
|
|
||||||
|
var updatedSubscriptionData = new SubscriptionData
|
||||||
|
{
|
||||||
|
Plan = teamsMonthlyPlan,
|
||||||
|
PurchasedPasswordManagerSeats = 20
|
||||||
|
};
|
||||||
|
|
||||||
|
var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData);
|
||||||
|
|
||||||
|
var upgradeItemOptions = subscriptionUpdate.UpgradeItemsOptions(subscription);
|
||||||
|
|
||||||
|
Assert.Single(upgradeItemOptions);
|
||||||
|
|
||||||
|
var passwordManagerOptions = upgradeItemOptions.First();
|
||||||
|
|
||||||
|
Assert.Equal(subscription.Items.Data.FirstOrDefault()?.Id, passwordManagerOptions.Id);
|
||||||
|
Assert.Equal(teamsMonthlyPlan.PasswordManager.StripeSeatPlanId, passwordManagerOptions.Price);
|
||||||
|
Assert.Equal(updatedSubscriptionData.PurchasedPasswordManagerSeats, passwordManagerOptions.Quantity);
|
||||||
|
Assert.Null(passwordManagerOptions.Deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[TeamsMonthlyWithAddOnsOrganizationCustomize]
|
||||||
|
public void UpgradeItemOptions_TeamsWithSMToEnterpriseWithSM_ReturnsCorrectOptions(
|
||||||
|
Organization organization)
|
||||||
|
{
|
||||||
|
// 5 purchased, 1 base
|
||||||
|
organization.MaxStorageGb = 6;
|
||||||
|
|
||||||
|
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||||
|
|
||||||
|
var subscription = new Subscription
|
||||||
|
{
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "password_manager_subscription_item",
|
||||||
|
Price = new Price { Id = teamsMonthlyPlan.PasswordManager.StripeSeatPlanId },
|
||||||
|
Quantity = organization.Seats!.Value
|
||||||
|
},
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "secrets_manager_subscription_item",
|
||||||
|
Price = new Price { Id = teamsMonthlyPlan.SecretsManager.StripeSeatPlanId },
|
||||||
|
Quantity = organization.SmSeats!.Value
|
||||||
|
},
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "secrets_manager_service_accounts_subscription_item",
|
||||||
|
Price = new Price { Id = teamsMonthlyPlan.SecretsManager.StripeServiceAccountPlanId },
|
||||||
|
Quantity = organization.SmServiceAccounts!.Value
|
||||||
|
},
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "password_manager_storage_subscription_item",
|
||||||
|
Price = new Price { Id = teamsMonthlyPlan.PasswordManager.StripeStoragePlanId },
|
||||||
|
Quantity = organization.Storage!.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var enterpriseMonthlyPlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
|
||||||
|
|
||||||
|
var updatedSubscriptionData = new SubscriptionData
|
||||||
|
{
|
||||||
|
Plan = enterpriseMonthlyPlan,
|
||||||
|
PurchasedPasswordManagerSeats = 50,
|
||||||
|
SubscribedToSecretsManager = true,
|
||||||
|
PurchasedSecretsManagerSeats = 30,
|
||||||
|
PurchasedAdditionalSecretsManagerServiceAccounts = 10,
|
||||||
|
PurchasedAdditionalStorage = 10
|
||||||
|
};
|
||||||
|
|
||||||
|
var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData);
|
||||||
|
|
||||||
|
var upgradeItemOptions = subscriptionUpdate.UpgradeItemsOptions(subscription);
|
||||||
|
|
||||||
|
Assert.Equal(4, upgradeItemOptions.Count);
|
||||||
|
|
||||||
|
var passwordManagerOptions = upgradeItemOptions.FirstOrDefault(options =>
|
||||||
|
options.Price == enterpriseMonthlyPlan.PasswordManager.StripeSeatPlanId);
|
||||||
|
|
||||||
|
var passwordManagerSubscriptionItem =
|
||||||
|
subscription.Items.Data.FirstOrDefault(item => item.Id == "password_manager_subscription_item");
|
||||||
|
|
||||||
|
Assert.Equal(passwordManagerSubscriptionItem?.Id, passwordManagerOptions!.Id);
|
||||||
|
Assert.Equal(enterpriseMonthlyPlan.PasswordManager.StripeSeatPlanId, passwordManagerOptions.Price);
|
||||||
|
Assert.Equal(updatedSubscriptionData.PurchasedPasswordManagerSeats, passwordManagerOptions.Quantity);
|
||||||
|
Assert.Null(passwordManagerOptions.Deleted);
|
||||||
|
|
||||||
|
var secretsManagerOptions = upgradeItemOptions.FirstOrDefault(options =>
|
||||||
|
options.Price == enterpriseMonthlyPlan.SecretsManager.StripeSeatPlanId);
|
||||||
|
|
||||||
|
var secretsManagerSubscriptionItem =
|
||||||
|
subscription.Items.Data.FirstOrDefault(item => item.Id == "secrets_manager_subscription_item");
|
||||||
|
|
||||||
|
Assert.Equal(secretsManagerSubscriptionItem?.Id, secretsManagerOptions!.Id);
|
||||||
|
Assert.Equal(enterpriseMonthlyPlan.SecretsManager.StripeSeatPlanId, secretsManagerOptions.Price);
|
||||||
|
Assert.Equal(updatedSubscriptionData.PurchasedSecretsManagerSeats, secretsManagerOptions.Quantity);
|
||||||
|
Assert.Null(secretsManagerOptions.Deleted);
|
||||||
|
|
||||||
|
var serviceAccountsOptions = upgradeItemOptions.FirstOrDefault(options =>
|
||||||
|
options.Price == enterpriseMonthlyPlan.SecretsManager.StripeServiceAccountPlanId);
|
||||||
|
|
||||||
|
var serviceAccountsSubscriptionItem = subscription.Items.Data.FirstOrDefault(item =>
|
||||||
|
item.Id == "secrets_manager_service_accounts_subscription_item");
|
||||||
|
|
||||||
|
Assert.Equal(serviceAccountsSubscriptionItem?.Id, serviceAccountsOptions!.Id);
|
||||||
|
Assert.Equal(enterpriseMonthlyPlan.SecretsManager.StripeServiceAccountPlanId, serviceAccountsOptions.Price);
|
||||||
|
Assert.Equal(updatedSubscriptionData.PurchasedAdditionalSecretsManagerServiceAccounts, serviceAccountsOptions.Quantity);
|
||||||
|
Assert.Null(serviceAccountsOptions.Deleted);
|
||||||
|
|
||||||
|
var storageOptions = upgradeItemOptions.FirstOrDefault(options =>
|
||||||
|
options.Price == enterpriseMonthlyPlan.PasswordManager.StripeStoragePlanId);
|
||||||
|
|
||||||
|
var storageSubscriptionItem = subscription.Items.Data.FirstOrDefault(item => item.Id == "password_manager_storage_subscription_item");
|
||||||
|
|
||||||
|
Assert.Equal(storageSubscriptionItem?.Id, storageOptions!.Id);
|
||||||
|
Assert.Equal(enterpriseMonthlyPlan.PasswordManager.StripeStoragePlanId, storageOptions.Price);
|
||||||
|
Assert.Equal(updatedSubscriptionData.PurchasedAdditionalStorage, storageOptions.Quantity);
|
||||||
|
Assert.Null(storageOptions.Deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[TeamsMonthlyWithAddOnsOrganizationCustomize]
|
||||||
|
public void UpgradeItemOptions_TeamsWithSMToEnterpriseWithoutSM_ReturnsCorrectOptions(
|
||||||
|
Organization organization)
|
||||||
|
{
|
||||||
|
// 5 purchased, 1 base
|
||||||
|
organization.MaxStorageGb = 6;
|
||||||
|
|
||||||
|
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||||
|
|
||||||
|
var subscription = new Subscription
|
||||||
|
{
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "password_manager_subscription_item",
|
||||||
|
Price = new Price { Id = teamsMonthlyPlan.PasswordManager.StripeSeatPlanId },
|
||||||
|
Quantity = organization.Seats!.Value
|
||||||
|
},
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "secrets_manager_subscription_item",
|
||||||
|
Price = new Price { Id = teamsMonthlyPlan.SecretsManager.StripeSeatPlanId },
|
||||||
|
Quantity = organization.SmSeats!.Value
|
||||||
|
},
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "secrets_manager_service_accounts_subscription_item",
|
||||||
|
Price = new Price { Id = teamsMonthlyPlan.SecretsManager.StripeServiceAccountPlanId },
|
||||||
|
Quantity = organization.SmServiceAccounts!.Value
|
||||||
|
},
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "password_manager_storage_subscription_item",
|
||||||
|
Price = new Price { Id = teamsMonthlyPlan.PasswordManager.StripeStoragePlanId },
|
||||||
|
Quantity = organization.Storage!.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var enterpriseMonthlyPlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
|
||||||
|
|
||||||
|
var updatedSubscriptionData = new SubscriptionData
|
||||||
|
{
|
||||||
|
Plan = enterpriseMonthlyPlan,
|
||||||
|
PurchasedPasswordManagerSeats = 50,
|
||||||
|
SubscribedToSecretsManager = false,
|
||||||
|
PurchasedSecretsManagerSeats = 0,
|
||||||
|
PurchasedAdditionalSecretsManagerServiceAccounts = 0,
|
||||||
|
PurchasedAdditionalStorage = 10
|
||||||
|
};
|
||||||
|
|
||||||
|
var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData);
|
||||||
|
|
||||||
|
var upgradeItemOptions = subscriptionUpdate.UpgradeItemsOptions(subscription);
|
||||||
|
|
||||||
|
Assert.Equal(4, upgradeItemOptions.Count);
|
||||||
|
|
||||||
|
var passwordManagerOptions = upgradeItemOptions.FirstOrDefault(options =>
|
||||||
|
options.Price == enterpriseMonthlyPlan.PasswordManager.StripeSeatPlanId);
|
||||||
|
|
||||||
|
var passwordManagerSubscriptionItem =
|
||||||
|
subscription.Items.Data.FirstOrDefault(item => item.Id == "password_manager_subscription_item");
|
||||||
|
|
||||||
|
Assert.Equal(passwordManagerSubscriptionItem?.Id, passwordManagerOptions!.Id);
|
||||||
|
Assert.Equal(enterpriseMonthlyPlan.PasswordManager.StripeSeatPlanId, passwordManagerOptions.Price);
|
||||||
|
Assert.Equal(updatedSubscriptionData.PurchasedPasswordManagerSeats, passwordManagerOptions.Quantity);
|
||||||
|
Assert.Null(passwordManagerOptions.Deleted);
|
||||||
|
|
||||||
|
var secretsManagerOptions = upgradeItemOptions.FirstOrDefault(options =>
|
||||||
|
options.Price == enterpriseMonthlyPlan.SecretsManager.StripeSeatPlanId);
|
||||||
|
|
||||||
|
var secretsManagerSubscriptionItem =
|
||||||
|
subscription.Items.Data.FirstOrDefault(item => item.Id == "secrets_manager_subscription_item");
|
||||||
|
|
||||||
|
Assert.Equal(secretsManagerSubscriptionItem?.Id, secretsManagerOptions!.Id);
|
||||||
|
Assert.Equal(enterpriseMonthlyPlan.SecretsManager.StripeSeatPlanId, secretsManagerOptions.Price);
|
||||||
|
Assert.Equal(updatedSubscriptionData.PurchasedSecretsManagerSeats, secretsManagerOptions.Quantity);
|
||||||
|
Assert.True(secretsManagerOptions.Deleted);
|
||||||
|
|
||||||
|
var serviceAccountsOptions = upgradeItemOptions.FirstOrDefault(options =>
|
||||||
|
options.Price == enterpriseMonthlyPlan.SecretsManager.StripeServiceAccountPlanId);
|
||||||
|
|
||||||
|
var serviceAccountsSubscriptionItem = subscription.Items.Data.FirstOrDefault(item =>
|
||||||
|
item.Id == "secrets_manager_service_accounts_subscription_item");
|
||||||
|
|
||||||
|
Assert.Equal(serviceAccountsSubscriptionItem?.Id, serviceAccountsOptions!.Id);
|
||||||
|
Assert.Equal(enterpriseMonthlyPlan.SecretsManager.StripeServiceAccountPlanId, serviceAccountsOptions.Price);
|
||||||
|
Assert.Equal(updatedSubscriptionData.PurchasedAdditionalSecretsManagerServiceAccounts, serviceAccountsOptions.Quantity);
|
||||||
|
Assert.True(serviceAccountsOptions.Deleted);
|
||||||
|
|
||||||
|
var storageOptions = upgradeItemOptions.FirstOrDefault(options =>
|
||||||
|
options.Price == enterpriseMonthlyPlan.PasswordManager.StripeStoragePlanId);
|
||||||
|
|
||||||
|
var storageSubscriptionItem = subscription.Items.Data.FirstOrDefault(item => item.Id == "password_manager_storage_subscription_item");
|
||||||
|
|
||||||
|
Assert.Equal(storageSubscriptionItem?.Id, storageOptions!.Id);
|
||||||
|
Assert.Equal(enterpriseMonthlyPlan.PasswordManager.StripeStoragePlanId, storageOptions.Price);
|
||||||
|
Assert.Equal(updatedSubscriptionData.PurchasedAdditionalStorage, storageOptions.Quantity);
|
||||||
|
Assert.Null(storageOptions.Deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[TeamsStarterOrganizationCustomize]
|
||||||
|
public void RevertItemOptions_TeamsStarterToTeams_ReturnsCorrectOptions(
|
||||||
|
Organization organization)
|
||||||
|
{
|
||||||
|
var teamsStarterPlan = StaticStore.GetPlan(PlanType.TeamsStarter);
|
||||||
|
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||||
|
|
||||||
|
var subscription = new Subscription
|
||||||
|
{
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "subscription_item",
|
||||||
|
Price = new Price { Id = teamsMonthlyPlan.PasswordManager.StripeSeatPlanId },
|
||||||
|
Quantity = 20
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var updatedSubscriptionData = new SubscriptionData
|
||||||
|
{
|
||||||
|
Plan = teamsMonthlyPlan,
|
||||||
|
PurchasedPasswordManagerSeats = 20
|
||||||
|
};
|
||||||
|
|
||||||
|
var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData);
|
||||||
|
|
||||||
|
var revertItemOptions = subscriptionUpdate.RevertItemsOptions(subscription);
|
||||||
|
|
||||||
|
Assert.Single(revertItemOptions);
|
||||||
|
|
||||||
|
var passwordManagerOptions = revertItemOptions.First();
|
||||||
|
|
||||||
|
Assert.Equal(subscription.Items.Data.FirstOrDefault()?.Id, passwordManagerOptions.Id);
|
||||||
|
Assert.Equal(teamsStarterPlan.PasswordManager.StripePlanId, passwordManagerOptions.Price);
|
||||||
|
Assert.Equal(1, passwordManagerOptions.Quantity);
|
||||||
|
Assert.Null(passwordManagerOptions.Deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[TeamsMonthlyWithAddOnsOrganizationCustomize]
|
||||||
|
public void RevertItemOptions_TeamsWithSMToEnterpriseWithSM_ReturnsCorrectOptions(
|
||||||
|
Organization organization)
|
||||||
|
{
|
||||||
|
// 5 purchased, 1 base
|
||||||
|
organization.MaxStorageGb = 6;
|
||||||
|
|
||||||
|
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||||
|
var enterpriseMonthlyPlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
|
||||||
|
|
||||||
|
var subscription = new Subscription
|
||||||
|
{
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "password_manager_subscription_item",
|
||||||
|
Price = new Price { Id = enterpriseMonthlyPlan.PasswordManager.StripeSeatPlanId },
|
||||||
|
Quantity = organization.Seats!.Value
|
||||||
|
},
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "secrets_manager_subscription_item",
|
||||||
|
Price = new Price { Id = enterpriseMonthlyPlan.SecretsManager.StripeSeatPlanId },
|
||||||
|
Quantity = organization.SmSeats!.Value
|
||||||
|
},
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "secrets_manager_service_accounts_subscription_item",
|
||||||
|
Price = new Price { Id = enterpriseMonthlyPlan.SecretsManager.StripeServiceAccountPlanId },
|
||||||
|
Quantity = organization.SmServiceAccounts!.Value
|
||||||
|
},
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "password_manager_storage_subscription_item",
|
||||||
|
Price = new Price { Id = enterpriseMonthlyPlan.PasswordManager.StripeStoragePlanId },
|
||||||
|
Quantity = organization.Storage!.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var updatedSubscriptionData = new SubscriptionData
|
||||||
|
{
|
||||||
|
Plan = enterpriseMonthlyPlan,
|
||||||
|
PurchasedPasswordManagerSeats = 50,
|
||||||
|
SubscribedToSecretsManager = true,
|
||||||
|
PurchasedSecretsManagerSeats = 30,
|
||||||
|
PurchasedAdditionalSecretsManagerServiceAccounts = 10,
|
||||||
|
PurchasedAdditionalStorage = 10
|
||||||
|
};
|
||||||
|
|
||||||
|
var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData);
|
||||||
|
|
||||||
|
var revertItemOptions = subscriptionUpdate.RevertItemsOptions(subscription);
|
||||||
|
|
||||||
|
Assert.Equal(4, revertItemOptions.Count);
|
||||||
|
|
||||||
|
var passwordManagerOptions = revertItemOptions.FirstOrDefault(options =>
|
||||||
|
options.Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId);
|
||||||
|
|
||||||
|
var passwordManagerSubscriptionItem =
|
||||||
|
subscription.Items.Data.FirstOrDefault(item => item.Id == "password_manager_subscription_item");
|
||||||
|
|
||||||
|
Assert.Equal(passwordManagerSubscriptionItem?.Id, passwordManagerOptions!.Id);
|
||||||
|
Assert.Equal(teamsMonthlyPlan.PasswordManager.StripeSeatPlanId, passwordManagerOptions.Price);
|
||||||
|
Assert.Equal(organization.Seats - teamsMonthlyPlan.PasswordManager.BaseSeats, passwordManagerOptions.Quantity);
|
||||||
|
Assert.Null(passwordManagerOptions.Deleted);
|
||||||
|
|
||||||
|
var secretsManagerOptions = revertItemOptions.FirstOrDefault(options =>
|
||||||
|
options.Price == teamsMonthlyPlan.SecretsManager.StripeSeatPlanId);
|
||||||
|
|
||||||
|
var secretsManagerSubscriptionItem =
|
||||||
|
subscription.Items.Data.FirstOrDefault(item => item.Id == "secrets_manager_subscription_item");
|
||||||
|
|
||||||
|
Assert.Equal(secretsManagerSubscriptionItem?.Id, secretsManagerOptions!.Id);
|
||||||
|
Assert.Equal(teamsMonthlyPlan.SecretsManager.StripeSeatPlanId, secretsManagerOptions.Price);
|
||||||
|
Assert.Equal(organization.SmSeats - teamsMonthlyPlan.SecretsManager.BaseSeats, secretsManagerOptions.Quantity);
|
||||||
|
Assert.Null(secretsManagerOptions.Deleted);
|
||||||
|
|
||||||
|
var serviceAccountsOptions = revertItemOptions.FirstOrDefault(options =>
|
||||||
|
options.Price == teamsMonthlyPlan.SecretsManager.StripeServiceAccountPlanId);
|
||||||
|
|
||||||
|
var serviceAccountsSubscriptionItem = subscription.Items.Data.FirstOrDefault(item =>
|
||||||
|
item.Id == "secrets_manager_service_accounts_subscription_item");
|
||||||
|
|
||||||
|
Assert.Equal(serviceAccountsSubscriptionItem?.Id, serviceAccountsOptions!.Id);
|
||||||
|
Assert.Equal(teamsMonthlyPlan.SecretsManager.StripeServiceAccountPlanId, serviceAccountsOptions.Price);
|
||||||
|
Assert.Equal(organization.SmServiceAccounts - teamsMonthlyPlan.SecretsManager.BaseServiceAccount, serviceAccountsOptions.Quantity);
|
||||||
|
Assert.Null(serviceAccountsOptions.Deleted);
|
||||||
|
|
||||||
|
var storageOptions = revertItemOptions.FirstOrDefault(options =>
|
||||||
|
options.Price == teamsMonthlyPlan.PasswordManager.StripeStoragePlanId);
|
||||||
|
|
||||||
|
var storageSubscriptionItem = subscription.Items.Data.FirstOrDefault(item => item.Id == "password_manager_storage_subscription_item");
|
||||||
|
|
||||||
|
Assert.Equal(storageSubscriptionItem?.Id, storageOptions!.Id);
|
||||||
|
Assert.Equal(teamsMonthlyPlan.PasswordManager.StripeStoragePlanId, storageOptions.Price);
|
||||||
|
Assert.Equal(organization.MaxStorageGb - teamsMonthlyPlan.PasswordManager.BaseStorageGb, storageOptions.Quantity);
|
||||||
|
Assert.Null(storageOptions.Deleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
[TeamsMonthlyWithAddOnsOrganizationCustomize]
|
||||||
|
public void RevertItemOptions_TeamsWithSMToEnterpriseWithoutSM_ReturnsCorrectOptions(
|
||||||
|
Organization organization)
|
||||||
|
{
|
||||||
|
// 5 purchased, 1 base
|
||||||
|
organization.MaxStorageGb = 6;
|
||||||
|
|
||||||
|
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
|
||||||
|
var enterpriseMonthlyPlan = StaticStore.GetPlan(PlanType.EnterpriseMonthly);
|
||||||
|
|
||||||
|
var subscription = new Subscription
|
||||||
|
{
|
||||||
|
Items = new StripeList<SubscriptionItem>
|
||||||
|
{
|
||||||
|
Data = new List<SubscriptionItem>
|
||||||
|
{
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "password_manager_subscription_item",
|
||||||
|
Price = new Price { Id = enterpriseMonthlyPlan.PasswordManager.StripeSeatPlanId },
|
||||||
|
Quantity = organization.Seats!.Value
|
||||||
|
},
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "secrets_manager_subscription_item",
|
||||||
|
Price = new Price { Id = enterpriseMonthlyPlan.SecretsManager.StripeSeatPlanId },
|
||||||
|
Quantity = organization.SmSeats!.Value
|
||||||
|
},
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "secrets_manager_service_accounts_subscription_item",
|
||||||
|
Price = new Price { Id = enterpriseMonthlyPlan.SecretsManager.StripeServiceAccountPlanId },
|
||||||
|
Quantity = organization.SmServiceAccounts!.Value
|
||||||
|
},
|
||||||
|
new ()
|
||||||
|
{
|
||||||
|
Id = "password_manager_storage_subscription_item",
|
||||||
|
Price = new Price { Id = enterpriseMonthlyPlan.PasswordManager.StripeStoragePlanId },
|
||||||
|
Quantity = organization.Storage!.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var updatedSubscriptionData = new SubscriptionData
|
||||||
|
{
|
||||||
|
Plan = enterpriseMonthlyPlan,
|
||||||
|
PurchasedPasswordManagerSeats = 50,
|
||||||
|
SubscribedToSecretsManager = false,
|
||||||
|
PurchasedSecretsManagerSeats = 0,
|
||||||
|
PurchasedAdditionalSecretsManagerServiceAccounts = 0,
|
||||||
|
PurchasedAdditionalStorage = 10
|
||||||
|
};
|
||||||
|
|
||||||
|
var subscriptionUpdate = new CompleteSubscriptionUpdate(organization, updatedSubscriptionData);
|
||||||
|
|
||||||
|
var revertItemOptions = subscriptionUpdate.RevertItemsOptions(subscription);
|
||||||
|
|
||||||
|
Assert.Equal(4, revertItemOptions.Count);
|
||||||
|
|
||||||
|
var passwordManagerOptions = revertItemOptions.FirstOrDefault(options =>
|
||||||
|
options.Price == teamsMonthlyPlan.PasswordManager.StripeSeatPlanId);
|
||||||
|
|
||||||
|
var passwordManagerSubscriptionItem =
|
||||||
|
subscription.Items.Data.FirstOrDefault(item => item.Id == "password_manager_subscription_item");
|
||||||
|
|
||||||
|
Assert.Equal(passwordManagerSubscriptionItem?.Id, passwordManagerOptions!.Id);
|
||||||
|
Assert.Equal(teamsMonthlyPlan.PasswordManager.StripeSeatPlanId, passwordManagerOptions.Price);
|
||||||
|
Assert.Equal(organization.Seats - teamsMonthlyPlan.PasswordManager.BaseSeats, passwordManagerOptions.Quantity);
|
||||||
|
Assert.Null(passwordManagerOptions.Deleted);
|
||||||
|
|
||||||
|
var secretsManagerOptions = revertItemOptions.FirstOrDefault(options =>
|
||||||
|
options.Price == teamsMonthlyPlan.SecretsManager.StripeSeatPlanId);
|
||||||
|
|
||||||
|
var secretsManagerSubscriptionItem =
|
||||||
|
subscription.Items.Data.FirstOrDefault(item => item.Id == "secrets_manager_subscription_item");
|
||||||
|
|
||||||
|
Assert.Equal(secretsManagerSubscriptionItem?.Id, secretsManagerOptions!.Id);
|
||||||
|
Assert.Equal(teamsMonthlyPlan.SecretsManager.StripeSeatPlanId, secretsManagerOptions.Price);
|
||||||
|
Assert.Equal(organization.SmSeats - teamsMonthlyPlan.SecretsManager.BaseSeats, secretsManagerOptions.Quantity);
|
||||||
|
Assert.Null(secretsManagerOptions.Deleted);
|
||||||
|
|
||||||
|
var serviceAccountsOptions = revertItemOptions.FirstOrDefault(options =>
|
||||||
|
options.Price == teamsMonthlyPlan.SecretsManager.StripeServiceAccountPlanId);
|
||||||
|
|
||||||
|
var serviceAccountsSubscriptionItem = subscription.Items.Data.FirstOrDefault(item =>
|
||||||
|
item.Id == "secrets_manager_service_accounts_subscription_item");
|
||||||
|
|
||||||
|
Assert.Equal(serviceAccountsSubscriptionItem?.Id, serviceAccountsOptions!.Id);
|
||||||
|
Assert.Equal(teamsMonthlyPlan.SecretsManager.StripeServiceAccountPlanId, serviceAccountsOptions.Price);
|
||||||
|
Assert.Equal(organization.SmServiceAccounts - teamsMonthlyPlan.SecretsManager.BaseServiceAccount, serviceAccountsOptions.Quantity);
|
||||||
|
Assert.Null(serviceAccountsOptions.Deleted);
|
||||||
|
|
||||||
|
var storageOptions = revertItemOptions.FirstOrDefault(options =>
|
||||||
|
options.Price == teamsMonthlyPlan.PasswordManager.StripeStoragePlanId);
|
||||||
|
|
||||||
|
var storageSubscriptionItem = subscription.Items.Data.FirstOrDefault(item => item.Id == "password_manager_storage_subscription_item");
|
||||||
|
|
||||||
|
Assert.Equal(storageSubscriptionItem?.Id, storageOptions!.Id);
|
||||||
|
Assert.Equal(teamsMonthlyPlan.PasswordManager.StripeStoragePlanId, storageOptions.Price);
|
||||||
|
Assert.Equal(organization.MaxStorageGb - teamsMonthlyPlan.PasswordManager.BaseStorageGb, storageOptions.Quantity);
|
||||||
|
Assert.Null(storageOptions.Deleted);
|
||||||
|
}
|
||||||
|
}
|
@ -24,7 +24,10 @@ public static class OrganizationLicenseFileFixtures
|
|||||||
private const string Version14 =
|
private const string Version14 =
|
||||||
"{\n 'LicenseKey': 'myLicenseKey',\n 'InstallationId': '78900000-0000-0000-0000-000000000123',\n 'Id': '12300000-0000-0000-0000-000000000456',\n 'Name': 'myOrg',\n 'BillingEmail': 'myBillingEmail',\n 'BusinessName': 'myBusinessName',\n 'Enabled': true,\n 'Plan': 'myPlan',\n 'PlanType': 11,\n 'Seats': 10,\n 'MaxCollections': 2,\n 'UsePolicies': true,\n 'UseSso': true,\n 'UseKeyConnector': true,\n 'UseScim': true,\n 'UseGroups': true,\n 'UseEvents': true,\n 'UseDirectory': true,\n 'UseTotp': true,\n 'Use2fa': true,\n 'UseApi': true,\n 'UseResetPassword': true,\n 'MaxStorageGb': 100,\n 'SelfHost': true,\n 'UsersGetPremium': true,\n 'UseCustomPermissions': true,\n 'Version': 13,\n 'Issued': '2023-11-29T22:42:33.970597Z',\n 'Refresh': '2023-12-06T22:42:33.970597Z',\n 'Expires': '2023-12-06T22:42:33.970597Z',\n 'ExpirationWithoutGracePeriod': null,\n 'UsePasswordManager': true,\n 'UseSecretsManager': true,\n 'SmSeats': 5,\n 'SmServiceAccounts': 8,\n 'LimitCollectionCreationDeletion': true,\n 'Trial': true,\n 'LicenseType': 1,\n 'Hash': '4G2u\\u002BWKO9EOiVnDVNr7uPxxRkv7TtaOmDl7kAYH05Fw=',\n 'Signature': ''\n}";
|
"{\n 'LicenseKey': 'myLicenseKey',\n 'InstallationId': '78900000-0000-0000-0000-000000000123',\n 'Id': '12300000-0000-0000-0000-000000000456',\n 'Name': 'myOrg',\n 'BillingEmail': 'myBillingEmail',\n 'BusinessName': 'myBusinessName',\n 'Enabled': true,\n 'Plan': 'myPlan',\n 'PlanType': 11,\n 'Seats': 10,\n 'MaxCollections': 2,\n 'UsePolicies': true,\n 'UseSso': true,\n 'UseKeyConnector': true,\n 'UseScim': true,\n 'UseGroups': true,\n 'UseEvents': true,\n 'UseDirectory': true,\n 'UseTotp': true,\n 'Use2fa': true,\n 'UseApi': true,\n 'UseResetPassword': true,\n 'MaxStorageGb': 100,\n 'SelfHost': true,\n 'UsersGetPremium': true,\n 'UseCustomPermissions': true,\n 'Version': 13,\n 'Issued': '2023-11-29T22:42:33.970597Z',\n 'Refresh': '2023-12-06T22:42:33.970597Z',\n 'Expires': '2023-12-06T22:42:33.970597Z',\n 'ExpirationWithoutGracePeriod': null,\n 'UsePasswordManager': true,\n 'UseSecretsManager': true,\n 'SmSeats': 5,\n 'SmServiceAccounts': 8,\n 'LimitCollectionCreationDeletion': true,\n 'Trial': true,\n 'LicenseType': 1,\n 'Hash': '4G2u\\u002BWKO9EOiVnDVNr7uPxxRkv7TtaOmDl7kAYH05Fw=',\n 'Signature': ''\n}";
|
||||||
|
|
||||||
private static readonly Dictionary<int, string> LicenseVersions = new() { { 12, Version12 }, { 13, Version13 }, { 14, Version14 } };
|
private const string Version15 =
|
||||||
|
"{\n 'LicenseKey': 'myLicenseKey',\n 'InstallationId': '78900000-0000-0000-0000-000000000123',\n 'Id': '12300000-0000-0000-0000-000000000456',\n 'Name': 'myOrg',\n 'BillingEmail': 'myBillingEmail',\n 'BusinessName': 'myBusinessName',\n 'Enabled': true,\n 'Plan': 'myPlan',\n 'PlanType': 11,\n 'Seats': 10,\n 'MaxCollections': 2,\n 'UsePolicies': true,\n 'UseSso': true,\n 'UseKeyConnector': true,\n 'UseScim': true,\n 'UseGroups': true,\n 'UseEvents': true,\n 'UseDirectory': true,\n 'UseTotp': true,\n 'Use2fa': true,\n 'UseApi': true,\n 'UseResetPassword': true,\n 'MaxStorageGb': 100,\n 'SelfHost': true,\n 'UsersGetPremium': true,\n 'UseCustomPermissions': true,\n 'Version': 14,\n 'Issued': '2023-12-14T02:03:33.374297Z',\n 'Refresh': '2023-12-07T22:42:33.970597Z',\n 'Expires': '2023-12-21T02:03:33.374297Z',\n 'ExpirationWithoutGracePeriod': null,\n 'UsePasswordManager': true,\n 'UseSecretsManager': true,\n 'SmSeats': 5,\n 'SmServiceAccounts': 8,\n 'LimitCollectionCreationDeletion': true,\n 'AllowAdminAccessToAllCollectionItems': true,\n 'Trial': true,\n 'LicenseType': 1,\n 'Hash': 'EZl4IvJaa1E5mPmlfp4p5twAtlmaxlF1yoZzVYP4vog=',\n 'Signature': ''\n}";
|
||||||
|
|
||||||
|
private static readonly Dictionary<int, string> LicenseVersions = new() { { 12, Version12 }, { 13, Version13 }, { 14, Version14 }, { 15, Version15 } };
|
||||||
|
|
||||||
public static OrganizationLicense GetVersion(int licenseVersion)
|
public static OrganizationLicense GetVersion(int licenseVersion)
|
||||||
{
|
{
|
||||||
@ -108,6 +111,7 @@ public static class OrganizationLicenseFileFixtures
|
|||||||
MaxAutoscaleSmSeats = 101,
|
MaxAutoscaleSmSeats = 101,
|
||||||
MaxAutoscaleSmServiceAccounts = 102,
|
MaxAutoscaleSmServiceAccounts = 102,
|
||||||
SecretsManagerBeta = true,
|
SecretsManagerBeta = true,
|
||||||
LimitCollectionCreationDeletion = true
|
LimitCollectionCreationDeletion = true,
|
||||||
|
AllowAdminAccessToAllCollectionItems = true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -63,29 +63,6 @@ public class UpgradeOrganizationPlanCommandTests
|
|||||||
Assert.Contains("already on this plan", exception.Message);
|
Assert.Contains("already on this plan", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, PaidOrganizationCustomize(CheckedPlanType = PlanType.Free), BitAutoData]
|
|
||||||
public async Task UpgradePlan_UpgradeFromPaidPlan_Throws(Organization organization, OrganizationUpgrade upgrade,
|
|
||||||
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
|
||||||
{
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
|
|
||||||
Assert.Contains("can only upgrade", exception.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, PaidOrganizationCustomize(CheckedPlanType = PlanType.Free), BitAutoData]
|
|
||||||
public async Task UpgradePlan_SM_UpgradeFromPaidPlan_Throws(Organization organization, OrganizationUpgrade upgrade,
|
|
||||||
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
|
||||||
{
|
|
||||||
upgrade.UseSecretsManager = true;
|
|
||||||
upgrade.AdditionalSmSeats = 10;
|
|
||||||
upgrade.AdditionalServiceAccounts = 10;
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
() => sutProvider.Sut.UpgradePlanAsync(organization.Id, upgrade));
|
|
||||||
Assert.Contains("can only upgrade", exception.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[FreeOrganizationUpgradeCustomize, BitAutoData]
|
[FreeOrganizationUpgradeCustomize, BitAutoData]
|
||||||
public async Task UpgradePlan_Passes(Organization organization, OrganizationUpgrade upgrade,
|
public async Task UpgradePlan_Passes(Organization organization, OrganizationUpgrade upgrade,
|
||||||
@ -99,6 +76,41 @@ public class UpgradeOrganizationPlanCommandTests
|
|||||||
await sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(organization);
|
await sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(organization);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(PlanType.TeamsStarter)]
|
||||||
|
[BitAutoData(PlanType.TeamsMonthly)]
|
||||||
|
[BitAutoData(PlanType.TeamsAnnually)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||||
|
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||||
|
public async Task UpgradePlan_FromFamilies_Passes(
|
||||||
|
PlanType planType,
|
||||||
|
Organization organization,
|
||||||
|
OrganizationUpgrade organizationUpgrade,
|
||||||
|
SutProvider<UpgradeOrganizationPlanCommand> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
|
||||||
|
organization.PlanType = PlanType.FamiliesAnnually;
|
||||||
|
|
||||||
|
organizationUpgrade.AdditionalSeats = 30;
|
||||||
|
organizationUpgrade.UseSecretsManager = true;
|
||||||
|
organizationUpgrade.AdditionalSmSeats = 20;
|
||||||
|
organizationUpgrade.AdditionalServiceAccounts = 5;
|
||||||
|
organizationUpgrade.AdditionalStorageGb = 3;
|
||||||
|
organizationUpgrade.Plan = planType;
|
||||||
|
|
||||||
|
await sutProvider.Sut.UpgradePlanAsync(organization.Id, organizationUpgrade);
|
||||||
|
await sutProvider.GetDependency<IPaymentService>().Received(1).AdjustSubscription(
|
||||||
|
organization,
|
||||||
|
StaticStore.GetPlan(planType),
|
||||||
|
organizationUpgrade.AdditionalSeats,
|
||||||
|
organizationUpgrade.UseSecretsManager,
|
||||||
|
organizationUpgrade.AdditionalSmSeats,
|
||||||
|
5,
|
||||||
|
3);
|
||||||
|
await sutProvider.GetDependency<IOrganizationService>().Received(1).ReplaceAndUpdateCacheAsync(organization);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, FreeOrganizationUpgradeCustomize]
|
[Theory, FreeOrganizationUpgradeCustomize]
|
||||||
[BitAutoData(PlanType.EnterpriseMonthly)]
|
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||||
[BitAutoData(PlanType.EnterpriseAnnually)]
|
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||||
@ -130,7 +142,6 @@ public class UpgradeOrganizationPlanCommandTests
|
|||||||
Assert.NotNull(result.Item2);
|
Assert.NotNull(result.Item2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[Theory, FreeOrganizationUpgradeCustomize]
|
[Theory, FreeOrganizationUpgradeCustomize]
|
||||||
[BitAutoData(PlanType.EnterpriseMonthly)]
|
[BitAutoData(PlanType.EnterpriseMonthly)]
|
||||||
[BitAutoData(PlanType.EnterpriseAnnually)]
|
[BitAutoData(PlanType.EnterpriseAnnually)]
|
||||||
|
@ -114,8 +114,9 @@ public class CollectionServiceTest
|
|||||||
collection.Id = default;
|
collection.Id = default;
|
||||||
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
.IsEnabled(FeatureFlagKeys.FlexibleCollections, Arg.Any<ICurrentContext>(), Arg.Any<bool>())
|
.IsEnabled(FeatureFlagKeys.FlexibleCollectionsV1, Arg.Any<ICurrentContext>(), Arg.Any<bool>())
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
|
organization.AllowAdminAccessToAllCollectionItems = false;
|
||||||
|
|
||||||
var ex = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SaveAsync(collection, null, users));
|
var ex = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.SaveAsync(collection, null, users));
|
||||||
Assert.Contains("At least one member or group must have can manage permission.", ex.Message);
|
Assert.Contains("At least one member or group must have can manage permission.", ex.Message);
|
||||||
|
@ -12,8 +12,8 @@ namespace Bit.Core.Test.Services;
|
|||||||
[SutProviderCustomize]
|
[SutProviderCustomize]
|
||||||
public class LaunchDarklyFeatureServiceTests
|
public class LaunchDarklyFeatureServiceTests
|
||||||
{
|
{
|
||||||
private const string _fakeKey = "somekey";
|
private const string _fakeFeatureKey = "somekey";
|
||||||
private const string _fakeValue = "somevalue";
|
private const string _fakeSdkKey = "somesdkkey";
|
||||||
|
|
||||||
private static SutProvider<LaunchDarklyFeatureService> GetSutProvider(IGlobalSettings globalSettings)
|
private static SutProvider<LaunchDarklyFeatureService> GetSutProvider(IGlobalSettings globalSettings)
|
||||||
{
|
{
|
||||||
@ -44,46 +44,46 @@ public class LaunchDarklyFeatureServiceTests
|
|||||||
var currentContext = Substitute.For<ICurrentContext>();
|
var currentContext = Substitute.For<ICurrentContext>();
|
||||||
currentContext.UserId.Returns(Guid.NewGuid());
|
currentContext.UserId.Returns(Guid.NewGuid());
|
||||||
|
|
||||||
Assert.False(sutProvider.Sut.IsEnabled(_fakeKey, currentContext));
|
Assert.False(sutProvider.Sut.IsEnabled(_fakeFeatureKey, currentContext));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "For local development")]
|
[Fact(Skip = "For local development")]
|
||||||
public void FeatureValue_Boolean()
|
public void FeatureValue_Boolean()
|
||||||
{
|
{
|
||||||
var settings = new Settings.GlobalSettings { LaunchDarkly = { SdkKey = _fakeValue } };
|
var settings = new Settings.GlobalSettings { LaunchDarkly = { SdkKey = _fakeSdkKey } };
|
||||||
|
|
||||||
var sutProvider = GetSutProvider(settings);
|
var sutProvider = GetSutProvider(settings);
|
||||||
|
|
||||||
var currentContext = Substitute.For<ICurrentContext>();
|
var currentContext = Substitute.For<ICurrentContext>();
|
||||||
currentContext.UserId.Returns(Guid.NewGuid());
|
currentContext.UserId.Returns(Guid.NewGuid());
|
||||||
|
|
||||||
Assert.False(sutProvider.Sut.IsEnabled(_fakeKey, currentContext));
|
Assert.False(sutProvider.Sut.IsEnabled(_fakeFeatureKey, currentContext));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "For local development")]
|
[Fact(Skip = "For local development")]
|
||||||
public void FeatureValue_Int()
|
public void FeatureValue_Int()
|
||||||
{
|
{
|
||||||
var settings = new Settings.GlobalSettings { LaunchDarkly = { SdkKey = _fakeValue } };
|
var settings = new Settings.GlobalSettings { LaunchDarkly = { SdkKey = _fakeSdkKey } };
|
||||||
|
|
||||||
var sutProvider = GetSutProvider(settings);
|
var sutProvider = GetSutProvider(settings);
|
||||||
|
|
||||||
var currentContext = Substitute.For<ICurrentContext>();
|
var currentContext = Substitute.For<ICurrentContext>();
|
||||||
currentContext.UserId.Returns(Guid.NewGuid());
|
currentContext.UserId.Returns(Guid.NewGuid());
|
||||||
|
|
||||||
Assert.Equal(0, sutProvider.Sut.GetIntVariation(_fakeKey, currentContext));
|
Assert.Equal(0, sutProvider.Sut.GetIntVariation(_fakeFeatureKey, currentContext));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "For local development")]
|
[Fact(Skip = "For local development")]
|
||||||
public void FeatureValue_String()
|
public void FeatureValue_String()
|
||||||
{
|
{
|
||||||
var settings = new Settings.GlobalSettings { LaunchDarkly = { SdkKey = _fakeValue } };
|
var settings = new Settings.GlobalSettings { LaunchDarkly = { SdkKey = _fakeSdkKey } };
|
||||||
|
|
||||||
var sutProvider = GetSutProvider(settings);
|
var sutProvider = GetSutProvider(settings);
|
||||||
|
|
||||||
var currentContext = Substitute.For<ICurrentContext>();
|
var currentContext = Substitute.For<ICurrentContext>();
|
||||||
currentContext.UserId.Returns(Guid.NewGuid());
|
currentContext.UserId.Returns(Guid.NewGuid());
|
||||||
|
|
||||||
Assert.Null(sutProvider.Sut.GetStringVariation(_fakeKey, currentContext));
|
Assert.Null(sutProvider.Sut.GetStringVariation(_fakeFeatureKey, currentContext));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact(Skip = "For local development")]
|
[Fact(Skip = "For local development")]
|
||||||
|
@ -75,7 +75,19 @@
|
|||||||
"HS384",
|
"HS384",
|
||||||
"HS512"
|
"HS512"
|
||||||
],
|
],
|
||||||
|
"prompt_values_supported": ["none", "login", "consent", "select_account"],
|
||||||
"authorization_response_iss_parameter_supported": true,
|
"authorization_response_iss_parameter_supported": true,
|
||||||
"backchannel_token_delivery_modes_supported": ["poll"],
|
"backchannel_token_delivery_modes_supported": ["poll"],
|
||||||
"backchannel_user_code_parameter_supported": true
|
"backchannel_user_code_parameter_supported": true,
|
||||||
|
"dpop_signing_alg_values_supported": [
|
||||||
|
"RS256",
|
||||||
|
"RS384",
|
||||||
|
"RS512",
|
||||||
|
"PS256",
|
||||||
|
"PS384",
|
||||||
|
"PS512",
|
||||||
|
"ES256",
|
||||||
|
"ES384",
|
||||||
|
"ES512"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
param(
|
|
||||||
[string][Alias('c')]$Configuration = "Release",
|
|
||||||
[string][Alias('o')]$Output = "CoverageOutput",
|
|
||||||
[string][Alias('rt')]$ReportType = "lcov"
|
|
||||||
)
|
|
||||||
|
|
||||||
function Install-Tools {
|
|
||||||
dotnet tool restore
|
|
||||||
}
|
|
||||||
|
|
||||||
function Print-Environment {
|
|
||||||
dotnet --version
|
|
||||||
}
|
|
||||||
|
|
||||||
function Prepare-Output {
|
|
||||||
if (Test-Path -Path $Output) {
|
|
||||||
Remove-Item $Output -Recurse
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Run-Tests {
|
|
||||||
dotnet test $PSScriptRoot/bitwarden.tests.sln /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage" --results-directory:"$Output" -c $Configuration
|
|
||||||
|
|
||||||
dotnet tool run reportgenerator -reports:$Output/**/*.cobertura.xml -targetdir:$Output -reporttypes:"$ReportType"
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "Collecting Code Coverage"
|
|
||||||
Install-Tools
|
|
||||||
Print-Environment
|
|
||||||
Prepare-Output
|
|
||||||
Run-Tests
|
|
@ -1,53 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Set defaults if no values supplied
|
|
||||||
CONFIGURATION="Release"
|
|
||||||
OUTPUT="CoverageOutput"
|
|
||||||
REPORT_TYPE="lcov"
|
|
||||||
|
|
||||||
|
|
||||||
# Read in arguments
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
key="$1"
|
|
||||||
|
|
||||||
case $key in
|
|
||||||
-c|--configuration)
|
|
||||||
|
|
||||||
CONFIGURATION="$2"
|
|
||||||
shift
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-o|--output)
|
|
||||||
OUTPUT="$2"
|
|
||||||
shift
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
-rt|--reportType)
|
|
||||||
REPORT_TYPE="$2"
|
|
||||||
shift
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "CONFIGURATION = ${CONFIGURATION}"
|
|
||||||
echo "OUTPUT = ${OUTPUT}"
|
|
||||||
echo "REPORT_TYPE = ${REPORT_TYPE}"
|
|
||||||
|
|
||||||
echo "Collectiong Code Coverage"
|
|
||||||
# Install tools
|
|
||||||
dotnet tool restore
|
|
||||||
# Print Environment
|
|
||||||
dotnet --version
|
|
||||||
|
|
||||||
if [[ -d $OUTPUT ]]; then
|
|
||||||
echo "Cleaning output location"
|
|
||||||
rm -rf $OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
dotnet test "./bitwarden.tests.sln" /p:CoverletOutputFormatter="cobertura" --collect:"XPlat Code Coverage" --results-directory:"$OUTPUT" -c $CONFIGURATION
|
|
||||||
|
|
||||||
dotnet tool run reportgenerator -reports:$OUTPUT/**/*.cobertura.xml -targetdir:$OUTPUT -reporttype:"$REPORT_TYPE"
|
|
47
util/Migrator/DbScripts/2023-12-04_00_GrantIndexes.sql
Normal file
47
util/Migrator/DbScripts/2023-12-04_00_GrantIndexes.sql
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
IF EXISTS(SELECT name
|
||||||
|
FROM sys.indexes
|
||||||
|
WHERE name = 'IX_Grant_SubjectId_ClientId_Type')
|
||||||
|
BEGIN
|
||||||
|
DROP INDEX [IX_Grant_SubjectId_ClientId_Type] ON [dbo].[Grant]
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF EXISTS(SELECT name
|
||||||
|
FROM sys.indexes
|
||||||
|
WHERE name = 'IX_Grant_SubjectId_SessionId_Type')
|
||||||
|
BEGIN
|
||||||
|
DROP INDEX [IX_Grant_SubjectId_SessionId_Type] ON [dbo].[Grant]
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF COL_LENGTH('[dbo].[Grant]', 'Id') IS NULL
|
||||||
|
BEGIN
|
||||||
|
ALTER TABLE [dbo].[Grant]
|
||||||
|
ADD [Id] INT NOT NULL IDENTITY
|
||||||
|
|
||||||
|
ALTER TABLE [dbo].[Grant]
|
||||||
|
DROP CONSTRAINT [PK_Grant];
|
||||||
|
|
||||||
|
ALTER TABLE [dbo].[Grant]
|
||||||
|
ADD CONSTRAINT [PK_Grant] PRIMARY KEY CLUSTERED ([Id] ASC);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX [IX_Grant_Key]
|
||||||
|
ON [dbo].[Grant]([Key]);
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF EXISTS(SELECT *
|
||||||
|
FROM sys.views
|
||||||
|
WHERE [Name] = 'GrantView')
|
||||||
|
BEGIN
|
||||||
|
DROP VIEW [dbo].[GrantView];
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE VIEW [dbo].[GrantView]
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[Grant]
|
||||||
|
GO
|
@ -0,0 +1,136 @@
|
|||||||
|
IF OBJECT_ID('[dbo].[Organization_DeleteById]') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
DROP PROCEDURE [dbo].[Organization_DeleteById]
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE PROCEDURE [dbo].[Organization_DeleteById]
|
||||||
|
@Id UNIQUEIDENTIFIER
|
||||||
|
WITH
|
||||||
|
RECOMPILE
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @Id
|
||||||
|
|
||||||
|
DECLARE @BatchSize INT = 100
|
||||||
|
WHILE @BatchSize > 0
|
||||||
|
BEGIN
|
||||||
|
BEGIN TRANSACTION Organization_DeleteById_Ciphers
|
||||||
|
|
||||||
|
DELETE TOP(@BatchSize)
|
||||||
|
FROM
|
||||||
|
[dbo].[Cipher]
|
||||||
|
WHERE
|
||||||
|
[UserId] IS NULL
|
||||||
|
AND [OrganizationId] = @Id
|
||||||
|
|
||||||
|
SET @BatchSize = @@ROWCOUNT
|
||||||
|
|
||||||
|
COMMIT TRANSACTION Organization_DeleteById_Ciphers
|
||||||
|
END
|
||||||
|
|
||||||
|
BEGIN TRANSACTION Organization_DeleteById
|
||||||
|
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[AuthRequest]
|
||||||
|
WHERE
|
||||||
|
[OrganizationId] = @Id
|
||||||
|
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[SsoUser]
|
||||||
|
WHERE
|
||||||
|
[OrganizationId] = @Id
|
||||||
|
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[SsoConfig]
|
||||||
|
WHERE
|
||||||
|
[OrganizationId] = @Id
|
||||||
|
|
||||||
|
DELETE CU
|
||||||
|
FROM
|
||||||
|
[dbo].[CollectionUser] CU
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[OrganizationUser] OU ON [CU].[OrganizationUserId] = [OU].[Id]
|
||||||
|
WHERE
|
||||||
|
[OU].[OrganizationId] = @Id
|
||||||
|
|
||||||
|
DELETE AP
|
||||||
|
FROM
|
||||||
|
[dbo].[AccessPolicy] AP
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[OrganizationUser] OU ON [AP].[OrganizationUserId] = [OU].[Id]
|
||||||
|
WHERE
|
||||||
|
[OU].[OrganizationId] = @Id
|
||||||
|
|
||||||
|
DELETE GU
|
||||||
|
FROM
|
||||||
|
[dbo].[GroupUser] GU
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[OrganizationUser] OU ON [GU].[OrganizationUserId] = [OU].[Id]
|
||||||
|
WHERE
|
||||||
|
[OU].[OrganizationId] = @Id
|
||||||
|
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationUser]
|
||||||
|
WHERE
|
||||||
|
[OrganizationId] = @Id
|
||||||
|
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[ProviderOrganization]
|
||||||
|
WHERE
|
||||||
|
[OrganizationId] = @Id
|
||||||
|
|
||||||
|
EXEC [dbo].[OrganizationApiKey_OrganizationDeleted] @Id
|
||||||
|
EXEC [dbo].[OrganizationConnection_OrganizationDeleted] @Id
|
||||||
|
EXEC [dbo].[OrganizationSponsorship_OrganizationDeleted] @Id
|
||||||
|
EXEC [dbo].[OrganizationDomain_OrganizationDeleted] @Id
|
||||||
|
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[Project]
|
||||||
|
WHERE
|
||||||
|
[OrganizationId] = @Id
|
||||||
|
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[Secret]
|
||||||
|
WHERE
|
||||||
|
[OrganizationId] = @Id
|
||||||
|
|
||||||
|
DELETE AK
|
||||||
|
FROM
|
||||||
|
[dbo].[ApiKey] AK
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[ServiceAccount] SA ON [AK].[ServiceAccountId] = [SA].[Id]
|
||||||
|
WHERE
|
||||||
|
[SA].[OrganizationId] = @Id
|
||||||
|
|
||||||
|
DELETE AP
|
||||||
|
FROM
|
||||||
|
[dbo].[AccessPolicy] AP
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[ServiceAccount] SA ON [AP].[GrantedServiceAccountId] = [SA].[Id]
|
||||||
|
WHERE
|
||||||
|
[SA].[OrganizationId] = @Id
|
||||||
|
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[ServiceAccount]
|
||||||
|
WHERE
|
||||||
|
[OrganizationId] = @Id
|
||||||
|
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[Organization]
|
||||||
|
WHERE
|
||||||
|
[Id] = @Id
|
||||||
|
|
||||||
|
COMMIT TRANSACTION Organization_DeleteById
|
||||||
|
END
|
@ -0,0 +1,137 @@
|
|||||||
|
IF OBJECT_ID('[dbo].[User_DeleteById]') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
DROP PROCEDURE [dbo].[User_DeleteById]
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE PROCEDURE [dbo].[User_DeleteById]
|
||||||
|
@Id UNIQUEIDENTIFIER
|
||||||
|
WITH
|
||||||
|
RECOMPILE
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
DECLARE @BatchSize INT = 100
|
||||||
|
|
||||||
|
-- Delete ciphers
|
||||||
|
WHILE @BatchSize > 0
|
||||||
|
BEGIN
|
||||||
|
BEGIN TRANSACTION User_DeleteById_Ciphers
|
||||||
|
|
||||||
|
DELETE TOP(@BatchSize)
|
||||||
|
FROM
|
||||||
|
[dbo].[Cipher]
|
||||||
|
WHERE
|
||||||
|
[UserId] = @Id
|
||||||
|
|
||||||
|
SET @BatchSize = @@ROWCOUNT
|
||||||
|
|
||||||
|
COMMIT TRANSACTION User_DeleteById_Ciphers
|
||||||
|
END
|
||||||
|
|
||||||
|
BEGIN TRANSACTION User_DeleteById
|
||||||
|
|
||||||
|
-- Delete WebAuthnCredentials
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[WebAuthnCredential]
|
||||||
|
WHERE
|
||||||
|
[UserId] = @Id
|
||||||
|
|
||||||
|
-- Delete folders
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[Folder]
|
||||||
|
WHERE
|
||||||
|
[UserId] = @Id
|
||||||
|
|
||||||
|
-- Delete AuthRequest, must be before Device
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[AuthRequest]
|
||||||
|
WHERE
|
||||||
|
[UserId] = @Id
|
||||||
|
|
||||||
|
-- Delete devices
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[Device]
|
||||||
|
WHERE
|
||||||
|
[UserId] = @Id
|
||||||
|
|
||||||
|
-- Delete collection users
|
||||||
|
DELETE
|
||||||
|
CU
|
||||||
|
FROM
|
||||||
|
[dbo].[CollectionUser] CU
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[OrganizationUser] OU ON OU.[Id] = CU.[OrganizationUserId]
|
||||||
|
WHERE
|
||||||
|
OU.[UserId] = @Id
|
||||||
|
|
||||||
|
-- Delete group users
|
||||||
|
DELETE
|
||||||
|
GU
|
||||||
|
FROM
|
||||||
|
[dbo].[GroupUser] GU
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[OrganizationUser] OU ON OU.[Id] = GU.[OrganizationUserId]
|
||||||
|
WHERE
|
||||||
|
OU.[UserId] = @Id
|
||||||
|
|
||||||
|
-- Delete AccessPolicy
|
||||||
|
DELETE
|
||||||
|
AP
|
||||||
|
FROM
|
||||||
|
[dbo].[AccessPolicy] AP
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[OrganizationUser] OU ON OU.[Id] = AP.[OrganizationUserId]
|
||||||
|
WHERE
|
||||||
|
[UserId] = @Id
|
||||||
|
|
||||||
|
-- Delete organization users
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationUser]
|
||||||
|
WHERE
|
||||||
|
[UserId] = @Id
|
||||||
|
|
||||||
|
-- Delete provider users
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[ProviderUser]
|
||||||
|
WHERE
|
||||||
|
[UserId] = @Id
|
||||||
|
|
||||||
|
-- Delete SSO Users
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[SsoUser]
|
||||||
|
WHERE
|
||||||
|
[UserId] = @Id
|
||||||
|
|
||||||
|
-- Delete Emergency Accesses
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[EmergencyAccess]
|
||||||
|
WHERE
|
||||||
|
[GrantorId] = @Id
|
||||||
|
OR
|
||||||
|
[GranteeId] = @Id
|
||||||
|
|
||||||
|
-- Delete Sends
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[Send]
|
||||||
|
WHERE
|
||||||
|
[UserId] = @Id
|
||||||
|
|
||||||
|
-- Finally, delete the user
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[User]
|
||||||
|
WHERE
|
||||||
|
[Id] = @Id
|
||||||
|
|
||||||
|
COMMIT TRANSACTION User_DeleteById
|
||||||
|
END
|
@ -0,0 +1,30 @@
|
|||||||
|
--Update stored procedure to include LimitCollectionCreationDeletion property
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Organization_ReadAbilities]
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
[Id],
|
||||||
|
[UseEvents],
|
||||||
|
[Use2fa],
|
||||||
|
CASE
|
||||||
|
WHEN [Use2fa] = 1 AND [TwoFactorProviders] IS NOT NULL AND [TwoFactorProviders] != '{}' THEN
|
||||||
|
1
|
||||||
|
ELSE
|
||||||
|
0
|
||||||
|
END AS [Using2fa],
|
||||||
|
[UsersGetPremium],
|
||||||
|
[UseCustomPermissions],
|
||||||
|
[UseSso],
|
||||||
|
[UseKeyConnector],
|
||||||
|
[UseScim],
|
||||||
|
[UseResetPassword],
|
||||||
|
[UsePolicies],
|
||||||
|
[Enabled],
|
||||||
|
[LimitCollectionCreationDeletion],
|
||||||
|
[AllowAdminAccessToAllCollectionItems]
|
||||||
|
FROM
|
||||||
|
[dbo].[Organization]
|
||||||
|
END
|
||||||
|
GO
|
@ -0,0 +1,347 @@
|
|||||||
|
-- 2023-11-28_00_DeprecateAccessAll_UserCipherDetails introduced a bug in UserCipherDetails_V2 which inverted
|
||||||
|
-- the ReadOnly and HidePasswords logic. That should have been fixed in 2023-11-29_00_FixUserCipherDetails_V2
|
||||||
|
-- but for some reason that sproc was not being run in cloud environments, and/or may not have refreshed
|
||||||
|
-- metadata of sprocs that used that function.
|
||||||
|
|
||||||
|
-- This migration just replicates the original 2023-11-28 migration but with the fix from 2023-11-29.
|
||||||
|
|
||||||
|
CREATE OR ALTER FUNCTION [dbo].[UserCipherDetails_V2](@UserId UNIQUEIDENTIFIER)
|
||||||
|
RETURNS TABLE
|
||||||
|
AS RETURN
|
||||||
|
WITH [CTE] AS (
|
||||||
|
SELECT
|
||||||
|
[Id],
|
||||||
|
[OrganizationId]
|
||||||
|
FROM
|
||||||
|
[OrganizationUser]
|
||||||
|
WHERE
|
||||||
|
[UserId] = @UserId
|
||||||
|
AND [Status] = 2 -- Confirmed
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
C.*,
|
||||||
|
CASE
|
||||||
|
WHEN COALESCE(CU.[ReadOnly], CG.[ReadOnly], 0) = 0
|
||||||
|
THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END [Edit],
|
||||||
|
CASE
|
||||||
|
WHEN COALESCE(CU.[HidePasswords], CG.[HidePasswords], 0) = 0
|
||||||
|
THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END [ViewPassword],
|
||||||
|
CASE
|
||||||
|
WHEN O.[UseTotp] = 1
|
||||||
|
THEN 1
|
||||||
|
ELSE 0
|
||||||
|
END [OrganizationUseTotp]
|
||||||
|
FROM
|
||||||
|
[dbo].[CipherDetails](@UserId) C
|
||||||
|
INNER JOIN
|
||||||
|
[CTE] OU ON C.[UserId] IS NULL AND C.[OrganizationId] IN (SELECT [OrganizationId] FROM [CTE])
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[Organization] O ON O.[Id] = OU.[OrganizationId] AND O.[Id] = C.[OrganizationId] AND O.[Enabled] = 1
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[CollectionCipher] CC ON CC.[CipherId] = C.[Id]
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[CollectionUser] CU ON CU.[CollectionId] = CC.[CollectionId] AND CU.[OrganizationUserId] = OU.[Id]
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id]
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[Group] G ON G.[Id] = GU.[GroupId]
|
||||||
|
LEFT JOIN
|
||||||
|
[dbo].[CollectionGroup] CG ON CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId]
|
||||||
|
WHERE
|
||||||
|
CU.[CollectionId] IS NOT NULL
|
||||||
|
OR CG.[CollectionId] IS NOT NULL
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
*,
|
||||||
|
1 [Edit],
|
||||||
|
1 [ViewPassword],
|
||||||
|
0 [OrganizationUseTotp]
|
||||||
|
FROM
|
||||||
|
[dbo].[CipherDetails](@UserId)
|
||||||
|
WHERE
|
||||||
|
[UserId] = @UserId
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- Create v2 sprocs for all sprocs that call UserCipherDetails
|
||||||
|
|
||||||
|
-- CipherDetails_ReadByIdUserId_V2
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[CipherDetails_ReadByIdUserId_V2]
|
||||||
|
@Id UNIQUEIDENTIFIER,
|
||||||
|
@UserId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT TOP 1
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[UserCipherDetails_V2](@UserId)
|
||||||
|
WHERE
|
||||||
|
[Id] = @Id
|
||||||
|
ORDER BY
|
||||||
|
[Edit] DESC
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- CipherDetails_ReadByUserId_V2
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[CipherDetails_ReadByUserId_V2]
|
||||||
|
@UserId UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[UserCipherDetails_V2](@UserId)
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- Cipher_Delete_V2
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Cipher_Delete_V2]
|
||||||
|
@Ids AS [dbo].[GuidIdArray] READONLY,
|
||||||
|
@UserId AS UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
CREATE TABLE #Temp
|
||||||
|
(
|
||||||
|
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
[UserId] UNIQUEIDENTIFIER NULL,
|
||||||
|
[OrganizationId] UNIQUEIDENTIFIER NULL,
|
||||||
|
[Attachments] BIT NOT NULL
|
||||||
|
)
|
||||||
|
|
||||||
|
INSERT INTO #Temp
|
||||||
|
SELECT
|
||||||
|
[Id],
|
||||||
|
[UserId],
|
||||||
|
[OrganizationId],
|
||||||
|
CASE WHEN [Attachments] IS NULL THEN 0 ELSE 1 END
|
||||||
|
FROM
|
||||||
|
[dbo].[UserCipherDetails_V2](@UserId)
|
||||||
|
WHERE
|
||||||
|
[Edit] = 1
|
||||||
|
AND [Id] IN (SELECT * FROM @Ids)
|
||||||
|
|
||||||
|
-- Delete ciphers
|
||||||
|
DELETE
|
||||||
|
FROM
|
||||||
|
[dbo].[Cipher]
|
||||||
|
WHERE
|
||||||
|
[Id] IN (SELECT [Id] FROM #Temp)
|
||||||
|
|
||||||
|
-- Cleanup orgs
|
||||||
|
DECLARE @OrgId UNIQUEIDENTIFIER
|
||||||
|
DECLARE [OrgCursor] CURSOR FORWARD_ONLY FOR
|
||||||
|
SELECT
|
||||||
|
[OrganizationId]
|
||||||
|
FROM
|
||||||
|
#Temp
|
||||||
|
WHERE
|
||||||
|
[OrganizationId] IS NOT NULL
|
||||||
|
GROUP BY
|
||||||
|
[OrganizationId]
|
||||||
|
OPEN [OrgCursor]
|
||||||
|
FETCH NEXT FROM [OrgCursor] INTO @OrgId
|
||||||
|
WHILE @@FETCH_STATUS = 0 BEGIN
|
||||||
|
EXEC [dbo].[Organization_UpdateStorage] @OrgId
|
||||||
|
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId
|
||||||
|
FETCH NEXT FROM [OrgCursor] INTO @OrgId
|
||||||
|
END
|
||||||
|
CLOSE [OrgCursor]
|
||||||
|
DEALLOCATE [OrgCursor]
|
||||||
|
|
||||||
|
-- Cleanup user
|
||||||
|
DECLARE @UserCiphersWithStorageCount INT
|
||||||
|
SELECT
|
||||||
|
@UserCiphersWithStorageCount = COUNT(1)
|
||||||
|
FROM
|
||||||
|
#Temp
|
||||||
|
WHERE
|
||||||
|
[UserId] IS NOT NULL
|
||||||
|
AND [Attachments] = 1
|
||||||
|
|
||||||
|
IF @UserCiphersWithStorageCount > 0
|
||||||
|
BEGIN
|
||||||
|
EXEC [dbo].[User_UpdateStorage] @UserId
|
||||||
|
END
|
||||||
|
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
|
||||||
|
|
||||||
|
DROP TABLE #Temp
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- Cipher_Move_V2
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Cipher_Move_V2]
|
||||||
|
@Ids AS [dbo].[GuidIdArray] READONLY,
|
||||||
|
@FolderId AS UNIQUEIDENTIFIER,
|
||||||
|
@UserId AS UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
DECLARE @UserIdKey VARCHAR(50) = CONCAT('"', @UserId, '"')
|
||||||
|
DECLARE @UserIdPath VARCHAR(50) = CONCAT('$.', @UserIdKey)
|
||||||
|
|
||||||
|
;WITH [IdsToMoveCTE] AS (
|
||||||
|
SELECT
|
||||||
|
[Id]
|
||||||
|
FROM
|
||||||
|
[dbo].[UserCipherDetails_V2](@UserId)
|
||||||
|
WHERE
|
||||||
|
[Id] IN (SELECT * FROM @Ids)
|
||||||
|
)
|
||||||
|
UPDATE
|
||||||
|
[dbo].[Cipher]
|
||||||
|
SET
|
||||||
|
[Folders] =
|
||||||
|
CASE
|
||||||
|
WHEN @FolderId IS NOT NULL AND [Folders] IS NULL THEN
|
||||||
|
CONCAT('{', @UserIdKey, ':"', @FolderId, '"', '}')
|
||||||
|
WHEN @FolderId IS NOT NULL THEN
|
||||||
|
JSON_MODIFY([Folders], @UserIdPath, CAST(@FolderId AS VARCHAR(50)))
|
||||||
|
ELSE
|
||||||
|
JSON_MODIFY([Folders], @UserIdPath, NULL)
|
||||||
|
END
|
||||||
|
WHERE
|
||||||
|
[Id] IN (SELECT * FROM [IdsToMoveCTE])
|
||||||
|
|
||||||
|
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- Cipher_Restore_V2
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Cipher_Restore_V2]
|
||||||
|
@Ids AS [dbo].[GuidIdArray] READONLY,
|
||||||
|
@UserId AS UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
CREATE TABLE #Temp
|
||||||
|
(
|
||||||
|
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
[UserId] UNIQUEIDENTIFIER NULL,
|
||||||
|
[OrganizationId] UNIQUEIDENTIFIER NULL
|
||||||
|
)
|
||||||
|
|
||||||
|
INSERT INTO #Temp
|
||||||
|
SELECT
|
||||||
|
[Id],
|
||||||
|
[UserId],
|
||||||
|
[OrganizationId]
|
||||||
|
FROM
|
||||||
|
[dbo].[UserCipherDetails_V2](@UserId)
|
||||||
|
WHERE
|
||||||
|
[Edit] = 1
|
||||||
|
AND [DeletedDate] IS NOT NULL
|
||||||
|
AND [Id] IN (SELECT * FROM @Ids)
|
||||||
|
|
||||||
|
DECLARE @UtcNow DATETIME2(7) = GETUTCDATE();
|
||||||
|
UPDATE
|
||||||
|
[dbo].[Cipher]
|
||||||
|
SET
|
||||||
|
[DeletedDate] = NULL,
|
||||||
|
[RevisionDate] = @UtcNow
|
||||||
|
WHERE
|
||||||
|
[Id] IN (SELECT [Id] FROM #Temp)
|
||||||
|
|
||||||
|
-- Bump orgs
|
||||||
|
DECLARE @OrgId UNIQUEIDENTIFIER
|
||||||
|
DECLARE [OrgCursor] CURSOR FORWARD_ONLY FOR
|
||||||
|
SELECT
|
||||||
|
[OrganizationId]
|
||||||
|
FROM
|
||||||
|
#Temp
|
||||||
|
WHERE
|
||||||
|
[OrganizationId] IS NOT NULL
|
||||||
|
GROUP BY
|
||||||
|
[OrganizationId]
|
||||||
|
OPEN [OrgCursor]
|
||||||
|
FETCH NEXT FROM [OrgCursor] INTO @OrgId
|
||||||
|
WHILE @@FETCH_STATUS = 0 BEGIN
|
||||||
|
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId
|
||||||
|
FETCH NEXT FROM [OrgCursor] INTO @OrgId
|
||||||
|
END
|
||||||
|
CLOSE [OrgCursor]
|
||||||
|
DEALLOCATE [OrgCursor]
|
||||||
|
|
||||||
|
-- Bump user
|
||||||
|
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
|
||||||
|
|
||||||
|
DROP TABLE #Temp
|
||||||
|
|
||||||
|
SELECT @UtcNow
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- Cipher_SoftDelete_V2
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Cipher_SoftDelete_V2]
|
||||||
|
@Ids AS [dbo].[GuidIdArray] READONLY,
|
||||||
|
@UserId AS UNIQUEIDENTIFIER
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
CREATE TABLE #Temp
|
||||||
|
(
|
||||||
|
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
[UserId] UNIQUEIDENTIFIER NULL,
|
||||||
|
[OrganizationId] UNIQUEIDENTIFIER NULL
|
||||||
|
)
|
||||||
|
|
||||||
|
INSERT INTO #Temp
|
||||||
|
SELECT
|
||||||
|
[Id],
|
||||||
|
[UserId],
|
||||||
|
[OrganizationId]
|
||||||
|
FROM
|
||||||
|
[dbo].[UserCipherDetails_V2](@UserId)
|
||||||
|
WHERE
|
||||||
|
[Edit] = 1
|
||||||
|
AND [DeletedDate] IS NULL
|
||||||
|
AND [Id] IN (SELECT * FROM @Ids)
|
||||||
|
|
||||||
|
-- Delete ciphers
|
||||||
|
DECLARE @UtcNow DATETIME2(7) = GETUTCDATE();
|
||||||
|
UPDATE
|
||||||
|
[dbo].[Cipher]
|
||||||
|
SET
|
||||||
|
[DeletedDate] = @UtcNow,
|
||||||
|
[RevisionDate] = @UtcNow
|
||||||
|
WHERE
|
||||||
|
[Id] IN (SELECT [Id] FROM #Temp)
|
||||||
|
|
||||||
|
-- Cleanup orgs
|
||||||
|
DECLARE @OrgId UNIQUEIDENTIFIER
|
||||||
|
DECLARE [OrgCursor] CURSOR FORWARD_ONLY FOR
|
||||||
|
SELECT
|
||||||
|
[OrganizationId]
|
||||||
|
FROM
|
||||||
|
#Temp
|
||||||
|
WHERE
|
||||||
|
[OrganizationId] IS NOT NULL
|
||||||
|
GROUP BY
|
||||||
|
[OrganizationId]
|
||||||
|
OPEN [OrgCursor]
|
||||||
|
FETCH NEXT FROM [OrgCursor] INTO @OrgId
|
||||||
|
WHILE @@FETCH_STATUS = 0 BEGIN
|
||||||
|
EXEC [dbo].[User_BumpAccountRevisionDateByOrganizationId] @OrgId
|
||||||
|
FETCH NEXT FROM [OrgCursor] INTO @OrgId
|
||||||
|
END
|
||||||
|
CLOSE [OrgCursor]
|
||||||
|
DEALLOCATE [OrgCursor]
|
||||||
|
|
||||||
|
EXEC [dbo].[User_BumpAccountRevisionDate] @UserId
|
||||||
|
|
||||||
|
DROP TABLE #Temp
|
||||||
|
END
|
||||||
|
GO
|
2334
util/MySqlMigrations/Migrations/20231214162533_GrantIdWithIndexes.Designer.cs
generated
Normal file
2334
util/MySqlMigrations/Migrations/20231214162533_GrantIdWithIndexes.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,150 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Metadata;
|
||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.MySqlMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class GrantIdWithIndexes : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropPrimaryKey(
|
||||||
|
name: "PK_Grant",
|
||||||
|
table: "Grant");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "Grant",
|
||||||
|
keyColumn: "Type",
|
||||||
|
keyValue: null,
|
||||||
|
column: "Type",
|
||||||
|
value: "");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Type",
|
||||||
|
table: "Grant",
|
||||||
|
type: "varchar(50)",
|
||||||
|
maxLength: 50,
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "varchar(50)",
|
||||||
|
oldMaxLength: 50,
|
||||||
|
oldNullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "Grant",
|
||||||
|
keyColumn: "Data",
|
||||||
|
keyValue: null,
|
||||||
|
column: "Data",
|
||||||
|
value: "");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Data",
|
||||||
|
table: "Grant",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext",
|
||||||
|
oldNullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.UpdateData(
|
||||||
|
table: "Grant",
|
||||||
|
keyColumn: "ClientId",
|
||||||
|
keyValue: null,
|
||||||
|
column: "ClientId",
|
||||||
|
value: "");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "ClientId",
|
||||||
|
table: "Grant",
|
||||||
|
type: "varchar(200)",
|
||||||
|
maxLength: 200,
|
||||||
|
nullable: false,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "varchar(200)",
|
||||||
|
oldMaxLength: 200,
|
||||||
|
oldNullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "Id",
|
||||||
|
table: "Grant",
|
||||||
|
type: "int",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0)
|
||||||
|
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn);
|
||||||
|
|
||||||
|
migrationBuilder.AddPrimaryKey(
|
||||||
|
name: "PK_Grant",
|
||||||
|
table: "Grant",
|
||||||
|
column: "Id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Grant_Key",
|
||||||
|
table: "Grant",
|
||||||
|
column: "Key",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropPrimaryKey(
|
||||||
|
name: "PK_Grant",
|
||||||
|
table: "Grant");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Grant_Key",
|
||||||
|
table: "Grant");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Id",
|
||||||
|
table: "Grant");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Type",
|
||||||
|
table: "Grant",
|
||||||
|
type: "varchar(50)",
|
||||||
|
maxLength: 50,
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "varchar(50)",
|
||||||
|
oldMaxLength: 50)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Data",
|
||||||
|
table: "Grant",
|
||||||
|
type: "longtext",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "longtext")
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "ClientId",
|
||||||
|
table: "Grant",
|
||||||
|
type: "varchar(200)",
|
||||||
|
maxLength: 200,
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "varchar(200)",
|
||||||
|
oldMaxLength: 200)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4")
|
||||||
|
.OldAnnotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.AddPrimaryKey(
|
||||||
|
name: "PK_Grant",
|
||||||
|
table: "Grant",
|
||||||
|
column: "Key");
|
||||||
|
}
|
||||||
|
}
|
@ -475,11 +475,12 @@ namespace Bit.MySqlMigrations.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b =>
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Key")
|
b.Property<int>("Id")
|
||||||
.HasMaxLength(200)
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("varchar(200)");
|
.HasColumnType("int");
|
||||||
|
|
||||||
b.Property<string>("ClientId")
|
b.Property<string>("ClientId")
|
||||||
|
.IsRequired()
|
||||||
.HasMaxLength(200)
|
.HasMaxLength(200)
|
||||||
.HasColumnType("varchar(200)");
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
@ -490,6 +491,7 @@ namespace Bit.MySqlMigrations.Migrations
|
|||||||
.HasColumnType("datetime(6)");
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
b.Property<string>("Data")
|
b.Property<string>("Data")
|
||||||
|
.IsRequired()
|
||||||
.HasColumnType("longtext");
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
b.Property<string>("Description")
|
b.Property<string>("Description")
|
||||||
@ -499,6 +501,11 @@ namespace Bit.MySqlMigrations.Migrations
|
|||||||
b.Property<DateTime?>("ExpirationDate")
|
b.Property<DateTime?>("ExpirationDate")
|
||||||
.HasColumnType("datetime(6)");
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
b.Property<string>("SessionId")
|
b.Property<string>("SessionId")
|
||||||
.HasMaxLength(100)
|
.HasMaxLength(100)
|
||||||
.HasColumnType("varchar(100)");
|
.HasColumnType("varchar(100)");
|
||||||
@ -508,10 +515,15 @@ namespace Bit.MySqlMigrations.Migrations
|
|||||||
.HasColumnType("varchar(200)");
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
b.Property<string>("Type")
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
.HasMaxLength(50)
|
.HasMaxLength(50)
|
||||||
.HasColumnType("varchar(50)");
|
.HasColumnType("varchar(50)");
|
||||||
|
|
||||||
b.HasKey("Key");
|
b.HasKey("Id")
|
||||||
|
.HasAnnotation("SqlServer:Clustered", true);
|
||||||
|
|
||||||
|
b.HasIndex("Key")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("Grant", (string)null);
|
b.ToTable("Grant", (string)null);
|
||||||
});
|
});
|
||||||
|
2347
util/PostgresMigrations/Migrations/20231214162542_GrantIdWithIndexes.Designer.cs
generated
Normal file
2347
util/PostgresMigrations/Migrations/20231214162542_GrantIdWithIndexes.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,120 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.PostgresMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class GrantIdWithIndexes : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropPrimaryKey(
|
||||||
|
name: "PK_Grant",
|
||||||
|
table: "Grant");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Type",
|
||||||
|
table: "Grant",
|
||||||
|
type: "character varying(50)",
|
||||||
|
maxLength: 50,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "character varying(50)",
|
||||||
|
oldMaxLength: 50,
|
||||||
|
oldNullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Data",
|
||||||
|
table: "Grant",
|
||||||
|
type: "text",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "text",
|
||||||
|
oldNullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "ClientId",
|
||||||
|
table: "Grant",
|
||||||
|
type: "character varying(200)",
|
||||||
|
maxLength: 200,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "character varying(200)",
|
||||||
|
oldMaxLength: 200,
|
||||||
|
oldNullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "Id",
|
||||||
|
table: "Grant",
|
||||||
|
type: "integer",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0)
|
||||||
|
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn);
|
||||||
|
|
||||||
|
migrationBuilder.AddPrimaryKey(
|
||||||
|
name: "PK_Grant",
|
||||||
|
table: "Grant",
|
||||||
|
column: "Id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Grant_Key",
|
||||||
|
table: "Grant",
|
||||||
|
column: "Key",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropPrimaryKey(
|
||||||
|
name: "PK_Grant",
|
||||||
|
table: "Grant");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Grant_Key",
|
||||||
|
table: "Grant");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Id",
|
||||||
|
table: "Grant");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Type",
|
||||||
|
table: "Grant",
|
||||||
|
type: "character varying(50)",
|
||||||
|
maxLength: 50,
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "character varying(50)",
|
||||||
|
oldMaxLength: 50);
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Data",
|
||||||
|
table: "Grant",
|
||||||
|
type: "text",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "text");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "ClientId",
|
||||||
|
table: "Grant",
|
||||||
|
type: "character varying(200)",
|
||||||
|
maxLength: 200,
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "character varying(200)",
|
||||||
|
oldMaxLength: 200);
|
||||||
|
|
||||||
|
migrationBuilder.AddPrimaryKey(
|
||||||
|
name: "PK_Grant",
|
||||||
|
table: "Grant",
|
||||||
|
column: "Key");
|
||||||
|
}
|
||||||
|
}
|
@ -480,11 +480,14 @@ namespace Bit.PostgresMigrations.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b =>
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Key")
|
b.Property<int>("Id")
|
||||||
.HasMaxLength(200)
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("character varying(200)");
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
|
||||||
|
|
||||||
b.Property<string>("ClientId")
|
b.Property<string>("ClientId")
|
||||||
|
.IsRequired()
|
||||||
.HasMaxLength(200)
|
.HasMaxLength(200)
|
||||||
.HasColumnType("character varying(200)");
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
@ -495,6 +498,7 @@ namespace Bit.PostgresMigrations.Migrations
|
|||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<string>("Data")
|
b.Property<string>("Data")
|
||||||
|
.IsRequired()
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
b.Property<string>("Description")
|
b.Property<string>("Description")
|
||||||
@ -504,6 +508,11 @@ namespace Bit.PostgresMigrations.Migrations
|
|||||||
b.Property<DateTime?>("ExpirationDate")
|
b.Property<DateTime?>("ExpirationDate")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
b.Property<string>("SessionId")
|
b.Property<string>("SessionId")
|
||||||
.HasMaxLength(100)
|
.HasMaxLength(100)
|
||||||
.HasColumnType("character varying(100)");
|
.HasColumnType("character varying(100)");
|
||||||
@ -513,10 +522,15 @@ namespace Bit.PostgresMigrations.Migrations
|
|||||||
.HasColumnType("character varying(200)");
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
b.Property<string>("Type")
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
.HasMaxLength(50)
|
.HasMaxLength(50)
|
||||||
.HasColumnType("character varying(50)");
|
.HasColumnType("character varying(50)");
|
||||||
|
|
||||||
b.HasKey("Key");
|
b.HasKey("Id")
|
||||||
|
.HasAnnotation("SqlServer:Clustered", true);
|
||||||
|
|
||||||
|
b.HasIndex("Key")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("Grant", (string)null);
|
b.ToTable("Grant", (string)null);
|
||||||
});
|
});
|
||||||
|
2332
util/SqliteMigrations/Migrations/20231214162537_GrantIdWithIndexes.Designer.cs
generated
Normal file
2332
util/SqliteMigrations/Migrations/20231214162537_GrantIdWithIndexes.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,119 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.SqliteMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class GrantIdWithIndexes : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropPrimaryKey(
|
||||||
|
name: "PK_Grant",
|
||||||
|
table: "Grant");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Type",
|
||||||
|
table: "Grant",
|
||||||
|
type: "TEXT",
|
||||||
|
maxLength: 50,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "TEXT",
|
||||||
|
oldMaxLength: 50,
|
||||||
|
oldNullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Data",
|
||||||
|
table: "Grant",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "TEXT",
|
||||||
|
oldNullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "ClientId",
|
||||||
|
table: "Grant",
|
||||||
|
type: "TEXT",
|
||||||
|
maxLength: 200,
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: "",
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "TEXT",
|
||||||
|
oldMaxLength: 200,
|
||||||
|
oldNullable: true);
|
||||||
|
|
||||||
|
migrationBuilder.AddColumn<int>(
|
||||||
|
name: "Id",
|
||||||
|
table: "Grant",
|
||||||
|
type: "INTEGER",
|
||||||
|
nullable: false,
|
||||||
|
defaultValue: 0)
|
||||||
|
.Annotation("Sqlite:Autoincrement", true);
|
||||||
|
|
||||||
|
migrationBuilder.AddPrimaryKey(
|
||||||
|
name: "PK_Grant",
|
||||||
|
table: "Grant",
|
||||||
|
column: "Id");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Grant_Key",
|
||||||
|
table: "Grant",
|
||||||
|
column: "Key",
|
||||||
|
unique: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropPrimaryKey(
|
||||||
|
name: "PK_Grant",
|
||||||
|
table: "Grant");
|
||||||
|
|
||||||
|
migrationBuilder.DropIndex(
|
||||||
|
name: "IX_Grant_Key",
|
||||||
|
table: "Grant");
|
||||||
|
|
||||||
|
migrationBuilder.DropColumn(
|
||||||
|
name: "Id",
|
||||||
|
table: "Grant");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Type",
|
||||||
|
table: "Grant",
|
||||||
|
type: "TEXT",
|
||||||
|
maxLength: 50,
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "TEXT",
|
||||||
|
oldMaxLength: 50);
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "Data",
|
||||||
|
table: "Grant",
|
||||||
|
type: "TEXT",
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "TEXT");
|
||||||
|
|
||||||
|
migrationBuilder.AlterColumn<string>(
|
||||||
|
name: "ClientId",
|
||||||
|
table: "Grant",
|
||||||
|
type: "TEXT",
|
||||||
|
maxLength: 200,
|
||||||
|
nullable: true,
|
||||||
|
oldClrType: typeof(string),
|
||||||
|
oldType: "TEXT",
|
||||||
|
oldMaxLength: 200);
|
||||||
|
|
||||||
|
migrationBuilder.AddPrimaryKey(
|
||||||
|
name: "PK_Grant",
|
||||||
|
table: "Grant",
|
||||||
|
column: "Key");
|
||||||
|
}
|
||||||
|
}
|
@ -473,11 +473,12 @@ namespace Bit.SqliteMigrations.Migrations
|
|||||||
|
|
||||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b =>
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.Auth.Models.Grant", b =>
|
||||||
{
|
{
|
||||||
b.Property<string>("Key")
|
b.Property<int>("Id")
|
||||||
.HasMaxLength(200)
|
.ValueGeneratedOnAdd()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
b.Property<string>("ClientId")
|
b.Property<string>("ClientId")
|
||||||
|
.IsRequired()
|
||||||
.HasMaxLength(200)
|
.HasMaxLength(200)
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@ -488,6 +489,7 @@ namespace Bit.SqliteMigrations.Migrations
|
|||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("Data")
|
b.Property<string>("Data")
|
||||||
|
.IsRequired()
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("Description")
|
b.Property<string>("Description")
|
||||||
@ -497,6 +499,11 @@ namespace Bit.SqliteMigrations.Migrations
|
|||||||
b.Property<DateTime?>("ExpirationDate")
|
b.Property<DateTime?>("ExpirationDate")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Key")
|
||||||
|
.IsRequired()
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("SessionId")
|
b.Property<string>("SessionId")
|
||||||
.HasMaxLength(100)
|
.HasMaxLength(100)
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
@ -506,10 +513,15 @@ namespace Bit.SqliteMigrations.Migrations
|
|||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("Type")
|
b.Property<string>("Type")
|
||||||
|
.IsRequired()
|
||||||
.HasMaxLength(50)
|
.HasMaxLength(50)
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.HasKey("Key");
|
b.HasKey("Id")
|
||||||
|
.HasAnnotation("SqlServer:Clustered", true);
|
||||||
|
|
||||||
|
b.HasIndex("Key")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("Grant", (string)null);
|
b.ToTable("Grant", (string)null);
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user