1
0
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:
Rui Tome
2024-01-09 14:28:19 +00:00
90 changed files with 10376 additions and 583 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -20,7 +20,9 @@ BEGIN
[UseScim], [UseScim],
[UseResetPassword], [UseResetPassword],
[UsePolicies], [UsePolicies],
[Enabled] [Enabled],
[LimitCollectionCreationDeletion],
[AllowAdminAccessToAllCollectionItems]
FROM FROM
[dbo].[Organization] [dbo].[Organization]
END END

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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