1
0
mirror of https://github.com/bitwarden/server.git synced 2025-02-28 03:51:23 +01:00

Merge branch 'main' into ac/pm-10338/leave-endpoint-to-log-organizationuser_left

This commit is contained in:
Rui Tome 2024-10-23 10:24:38 +01:00
commit aa6b91452d
No known key found for this signature in database
GPG Key ID: 526239D96A8EC066
117 changed files with 3541 additions and 528 deletions

23
.github/CODEOWNERS vendored
View File

@ -4,13 +4,22 @@
# #
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners # https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
# DevOps for Actions and other workflow changes ## Docker files have shared ownership ##
.github/workflows @bitwarden/dept-devops **/Dockerfile
**/*.Dockerfile
**/.dockerignore
**/entrypoint.sh
# DevOps for Docker changes ## BRE team owns these workflows ##
**/Dockerfile @bitwarden/dept-devops .github/workflows/publish.yml @bitwarden/dept-bre
**/*.Dockerfile @bitwarden/dept-devops
**/.dockerignore @bitwarden/dept-devops ## These are shared workflows ##
.github/workflows/_move_finalization_db_scripts.yml
.github/workflows/build.yml
.github/workflows/cleanup-after-pr.yml
.github/workflows/cleanup-rc-branch.yml
.github/workflows/release.yml
.github/workflows/repository-management.yml
# Database Operations for database changes # Database Operations for database changes
src/Sql/** @bitwarden/dept-dbops src/Sql/** @bitwarden/dept-dbops
@ -60,6 +69,6 @@ src/EventsProcessor @bitwarden/team-admin-console-dev
src/Admin/Controllers/ToolsController.cs @bitwarden/team-billing-dev src/Admin/Controllers/ToolsController.cs @bitwarden/team-billing-dev
src/Admin/Views/Tools @bitwarden/team-billing-dev src/Admin/Views/Tools @bitwarden/team-billing-dev
# Multiple owners - DO NOT REMOVE (DevOps) # Multiple owners - DO NOT REMOVE (BRE)
**/packages.lock.json **/packages.lock.json
Directory.Build.props Directory.Build.props

View File

@ -1,4 +1,3 @@
---
name: _move_finalization_db_scripts name: _move_finalization_db_scripts
run-name: Move finalization database scripts run-name: Move finalization database scripts

View File

@ -1,4 +1,3 @@
---
name: Automatic responses name: Automatic responses
on: on:
issues: issues:

View File

@ -1,4 +1,3 @@
---
name: Build name: Build
on: on:
@ -595,7 +594,7 @@ jobs:
workflow_id: '_update_ephemeral_tags.yml', workflow_id: '_update_ephemeral_tags.yml',
ref: 'main', ref: 'main',
inputs: { inputs: {
ephemeral_env_branch: '${{ github.head_ref }}' ephemeral_env_branch: process.env.GITHUB_HEAD_REF
} }
}) })

View File

@ -1,4 +1,3 @@
---
name: Container registry cleanup name: Container registry cleanup
on: on:

View File

@ -0,0 +1,59 @@
name: Ephemeral environment cleanup
on:
pull_request:
types: [unlabeled]
jobs:
validate-pr:
name: Validate PR
runs-on: ubuntu-24.04
outputs:
config-exists: ${{ steps.validate-config.outputs.config-exists }}
steps:
- name: Checkout PR
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Validate config exists in path
id: validate-config
run: |
if [[ -f "ephemeral-environments/$GITHUB_HEAD_REF.yaml" ]]; then
echo "Ephemeral environment config found in path, continuing."
echo "config-exists=true" >> $GITHUB_OUTPUT
fi
cleanup-config:
name: Cleanup ephemeral environment
runs-on: ubuntu-24.04
needs: validate-pr
if: ${{ needs.validate-pr.outputs.config-exists }}
steps:
- name: Log in to Azure - CI subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
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 Ephemeral Environment cleanup
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.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: '_ephemeral_environment_pr_manager.yml',
ref: 'main',
inputs: {
ephemeral_env_branch: process.env.GITHUB_HEAD_REF,
cleanup_config: true,
project: 'server'
}
})

View File

@ -1,4 +1,3 @@
---
name: Cleanup RC Branch name: Cleanup RC Branch
on: on:

View File

@ -1,4 +1,3 @@
---
name: Enforce PR labels name: Enforce PR labels
on: on:
@ -7,13 +6,13 @@ on:
types: [labeled, unlabeled, opened, reopened, synchronize] types: [labeled, unlabeled, opened, reopened, synchronize]
jobs: jobs:
enforce-label: enforce-label:
if: ${{ contains(github.event.*.labels.*.name, 'hold') || contains(github.event.*.labels.*.name, 'needs-qa') || contains(github.event.*.labels.*.name, 'DB-migrations-changed') }} if: ${{ contains(github.event.*.labels.*.name, 'hold') || contains(github.event.*.labels.*.name, 'needs-qa') || contains(github.event.*.labels.*.name, 'DB-migrations-changed') || contains(github.event.*.labels.*.name, 'ephemeral-environment') }}
name: Enforce label name: Enforce label
runs-on: ubuntu-22.04 runs-on: ubuntu-22.04
steps: steps:
- name: Check for label - name: Check for label
run: | run: |
echo "PRs with the hold or needs-qa labels cannot be merged" echo "PRs with the hold, needs-qa or ephemeral-environment labels cannot be merged"
echo "### :x: PRs with the hold or needs-qa labels cannot be merged" >> $GITHUB_STEP_SUMMARY echo "### :x: PRs with the hold, needs-qa or ephemeral-environment labels cannot be merged" >> $GITHUB_STEP_SUMMARY
exit 1 exit 1

View File

@ -1,7 +1,6 @@
# Runs if there are changes to the paths: list. # Runs if there are changes to the paths: list.
# Starts a matrix job to check for modified files, then sets output based on the results. # Starts a matrix job to check for modified files, then sets output based on the results.
# The input decides if the label job is ran, adding a label to the PR. # The input decides if the label job is ran, adding a label to the PR.
---
name: Protect files name: Protect files
on: on:

View File

@ -1,4 +1,3 @@
---
name: Publish name: Publish
run-name: Publish ${{ inputs.publish_type }} run-name: Publish ${{ inputs.publish_type }}

View File

@ -1,4 +1,3 @@
---
name: Release name: Release
run-name: Release ${{ inputs.release_type }} run-name: Release ${{ inputs.release_type }}

View File

@ -1,4 +1,3 @@
---
name: Staleness name: Staleness
on: on:
workflow_dispatch: workflow_dispatch:

View File

@ -1,4 +1,3 @@
---
name: Database testing name: Database testing
on: on:

View File

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

View File

@ -11,7 +11,6 @@ using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Services; using Bit.Core.Billing.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.OrganizationConnectionConfigs; using Bit.Core.Models.OrganizationConnectionConfigs;
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces; using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
using Bit.Core.Repositories; using Bit.Core.Repositories;
@ -236,7 +235,8 @@ public class OrganizationsController : Controller
if (organization.UseSecretsManager && if (organization.UseSecretsManager &&
!StaticStore.GetPlan(organization.PlanType).SupportsSecretsManager) !StaticStore.GetPlan(organization.PlanType).SupportsSecretsManager)
{ {
throw new BadRequestException("Plan does not support Secrets Manager"); TempData["Error"] = "Plan does not support Secrets Manager";
return RedirectToAction("Edit", new { id });
} }
await _organizationRepository.ReplaceAsync(organization); await _organizationRepository.ReplaceAsync(organization);

View File

@ -181,7 +181,6 @@ public class OrganizationEditModel : OrganizationViewModel
*/ */
public object GetPlansHelper() => public object GetPlansHelper() =>
StaticStore.Plans StaticStore.Plans
.Where(p => p.SupportsSecretsManager)
.Select(p => .Select(p =>
{ {
var plan = new var plan = new

View File

@ -1,4 +1,6 @@
@model OrganizationViewModel @inject Bit.Core.Services.IFeatureService FeatureService
@model OrganizationViewModel
<dl class="row"> <dl class="row">
<dt class="col-sm-4 col-lg-3">Id</dt> <dt class="col-sm-4 col-lg-3">Id</dt>
<dd id="org-id" class="col-sm-8 col-lg-9"><code>@Model.Organization.Id</code></dd> <dd id="org-id" class="col-sm-8 col-lg-9"><code>@Model.Organization.Id</code></dd>
@ -53,8 +55,19 @@
<dt class="col-sm-4 col-lg-3">Administrators manage all collections</dt> <dt class="col-sm-4 col-lg-3">Administrators manage all collections</dt>
<dd id="pm-manage-collections" class="col-sm-8 col-lg-9">@(Model.Organization.AllowAdminAccessToAllCollectionItems ? "On" : "Off")</dd> <dd id="pm-manage-collections" class="col-sm-8 col-lg-9">@(Model.Organization.AllowAdminAccessToAllCollectionItems ? "On" : "Off")</dd>
<dt class="col-sm-4 col-lg-3">Limit collection creation to administrators</dt> @if (!FeatureService.IsEnabled(Bit.Core.FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
<dd id="pm-collection-creation" class="col-sm-8 col-lg-9">@(Model.Organization.LimitCollectionCreationDeletion ? "On" : "Off")</dd> {
<dt class="col-sm-4 col-lg-3">Limit collection creation to administrators</dt>
<dd id="pm-collection-creation" class="col-sm-8 col-lg-9">@(Model.Organization.LimitCollectionCreationDeletion ? "On" : "Off")</dd>
}
else
{
<dt class="col-sm-4 col-lg-3">Limit collection creation to administrators</dt>
<dd id="pm-collection-creation" class="col-sm-8 col-lg-9">@(Model.Organization.LimitCollectionCreation ? "On" : "Off")</dd>
<dt class="col-sm-4 col-lg-3">Limit collection deletion to administrators</dt>
<dd id="pm-collection-deletion" class="col-sm-8 col-lg-9">@(Model.Organization.LimitCollectionDeletion ? "On" : "Off")</dd>
}
</dl> </dl>
<h2>Secrets Manager</h2> <h2>Secrets Manager</h2>

View File

@ -101,7 +101,7 @@ public class OrganizationDomainController : Controller
throw new NotFoundException(); throw new NotFoundException();
} }
organizationDomain = await _verifyOrganizationDomainCommand.VerifyOrganizationDomainAsync(organizationDomain); organizationDomain = await _verifyOrganizationDomainCommand.UserVerifyOrganizationDomainAsync(organizationDomain);
return new OrganizationDomainResponseModel(organizationDomain); return new OrganizationDomainResponseModel(organizationDomain);
} }

View File

@ -520,9 +520,16 @@ public class OrganizationsController : Controller
} }
[HttpPut("{id}/collection-management")] [HttpPut("{id}/collection-management")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<OrganizationResponseModel> PutCollectionManagement(Guid id, [FromBody] OrganizationCollectionManagementUpdateRequestModel model) public async Task<OrganizationResponseModel> PutCollectionManagement(Guid id, [FromBody] OrganizationCollectionManagementUpdateRequestModel model)
{ {
if (
_globalSettings.SelfHosted &&
!_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)
)
{
throw new BadRequestException("Only allowed when not self hosted.");
}
var organization = await _organizationRepository.GetByIdAsync(id); var organization = await _organizationRepository.GetByIdAsync(id);
if (organization == null) if (organization == null)
{ {
@ -534,7 +541,7 @@ public class OrganizationsController : Controller
throw new NotFoundException(); throw new NotFoundException();
} }
await _organizationService.UpdateAsync(model.ToOrganization(organization), eventType: EventType.Organization_CollectionManagement_Updated); await _organizationService.UpdateAsync(model.ToOrganization(organization, _featureService), eventType: EventType.Organization_CollectionManagement_Updated);
return new OrganizationResponseModel(organization); return new OrganizationResponseModel(organization);
} }
} }

View File

@ -25,7 +25,6 @@ public class PoliciesController : Controller
{ {
private readonly IPolicyRepository _policyRepository; private readonly IPolicyRepository _policyRepository;
private readonly IPolicyService _policyService; private readonly IPolicyService _policyService;
private readonly IOrganizationService _organizationService;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IUserService _userService; private readonly IUserService _userService;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
@ -36,7 +35,6 @@ public class PoliciesController : Controller
public PoliciesController( public PoliciesController(
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
IPolicyService policyService, IPolicyService policyService,
IOrganizationService organizationService,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IUserService userService, IUserService userService,
ICurrentContext currentContext, ICurrentContext currentContext,
@ -46,7 +44,6 @@ public class PoliciesController : Controller
{ {
_policyRepository = policyRepository; _policyRepository = policyRepository;
_policyService = policyService; _policyService = policyService;
_organizationService = organizationService;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_userService = userService; _userService = userService;
_currentContext = currentContext; _currentContext = currentContext;
@ -185,7 +182,7 @@ public class PoliciesController : Controller
} }
var userId = _userService.GetProperUserId(User); var userId = _userService.GetProperUserId(User);
await _policyService.SaveAsync(policy, _organizationService, userId); await _policyService.SaveAsync(policy, userId);
return new PolicyResponseModel(policy); return new PolicyResponseModel(policy);
} }
} }

View File

@ -55,6 +55,9 @@ public class OrganizationResponseModel : ResponseModel
SmServiceAccounts = organization.SmServiceAccounts; SmServiceAccounts = organization.SmServiceAccounts;
MaxAutoscaleSmSeats = organization.MaxAutoscaleSmSeats; MaxAutoscaleSmSeats = organization.MaxAutoscaleSmSeats;
MaxAutoscaleSmServiceAccounts = organization.MaxAutoscaleSmServiceAccounts; MaxAutoscaleSmServiceAccounts = organization.MaxAutoscaleSmServiceAccounts;
LimitCollectionCreation = organization.LimitCollectionCreation;
LimitCollectionDeletion = organization.LimitCollectionDeletion;
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion; LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
} }
@ -98,6 +101,9 @@ public class OrganizationResponseModel : ResponseModel
public int? SmServiceAccounts { get; set; } public int? SmServiceAccounts { get; set; }
public int? MaxAutoscaleSmSeats { get; set; } public int? MaxAutoscaleSmSeats { get; set; }
public int? MaxAutoscaleSmServiceAccounts { get; set; } public int? MaxAutoscaleSmServiceAccounts { get; set; }
public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; }
// Deperectated: https://bitwarden.atlassian.net/browse/PM-10863
public bool LimitCollectionCreationDeletion { get; set; } public bool LimitCollectionCreationDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; }
} }

View File

@ -65,6 +65,9 @@ public class ProfileOrganizationResponseModel : ResponseModel
FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete; FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete;
FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil; FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil;
AccessSecretsManager = organization.AccessSecretsManager; AccessSecretsManager = organization.AccessSecretsManager;
LimitCollectionCreation = organization.LimitCollectionCreation;
LimitCollectionDeletion = organization.LimitCollectionDeletion;
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion; LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
UserIsManagedByOrganization = organizationIdsManagingUser.Contains(organization.OrganizationId); UserIsManagedByOrganization = organizationIdsManagingUser.Contains(organization.OrganizationId);
@ -124,6 +127,9 @@ public class ProfileOrganizationResponseModel : ResponseModel
public DateTime? FamilySponsorshipValidUntil { get; set; } public DateTime? FamilySponsorshipValidUntil { get; set; }
public bool? FamilySponsorshipToDelete { get; set; } public bool? FamilySponsorshipToDelete { get; set; }
public bool AccessSecretsManager { get; set; } public bool AccessSecretsManager { get; set; }
public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; }
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
public bool LimitCollectionCreationDeletion { get; set; } public bool LimitCollectionCreationDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; }
/// <summary> /// <summary>

View File

@ -44,6 +44,9 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
ProviderId = organization.ProviderId; ProviderId = organization.ProviderId;
ProviderName = organization.ProviderName; ProviderName = organization.ProviderName;
ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier; ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier;
LimitCollectionCreation = organization.LimitCollectionCreation;
LimitCollectionDeletion = organization.LimitCollectionDeletion;
// https://bitwarden.atlassian.net/browse/PM-10863
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion; LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
} }

View File

@ -6,7 +6,6 @@ using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
@ -18,18 +17,15 @@ public class PoliciesController : Controller
{ {
private readonly IPolicyRepository _policyRepository; private readonly IPolicyRepository _policyRepository;
private readonly IPolicyService _policyService; private readonly IPolicyService _policyService;
private readonly IOrganizationService _organizationService;
private readonly ICurrentContext _currentContext; private readonly ICurrentContext _currentContext;
public PoliciesController( public PoliciesController(
IPolicyRepository policyRepository, IPolicyRepository policyRepository,
IPolicyService policyService, IPolicyService policyService,
IOrganizationService organizationService,
ICurrentContext currentContext) ICurrentContext currentContext)
{ {
_policyRepository = policyRepository; _policyRepository = policyRepository;
_policyService = policyService; _policyService = policyService;
_organizationService = organizationService;
_currentContext = currentContext; _currentContext = currentContext;
} }
@ -96,7 +92,7 @@ public class PoliciesController : Controller
{ {
policy = model.ToPolicy(policy); policy = model.ToPolicy(policy);
} }
await _policyService.SaveAsync(policy, _organizationService, null); await _policyService.SaveAsync(policy, null);
var response = new PolicyResponseModel(policy); var response = new PolicyResponseModel(policy);
return new JsonResult(response); return new JsonResult(response);
} }

View File

@ -3,8 +3,11 @@
namespace Bit.Api.Billing.Models.Responses; namespace Bit.Api.Billing.Models.Responses;
public record OrganizationMetadataResponse( public record OrganizationMetadataResponse(
bool IsEligibleForSelfHost,
bool IsOnSecretsManagerStandalone) bool IsOnSecretsManagerStandalone)
{ {
public static OrganizationMetadataResponse From(OrganizationMetadata metadata) public static OrganizationMetadataResponse From(OrganizationMetadata metadata)
=> new(metadata.IsOnSecretsManagerStandalone); => new(
metadata.IsEligibleForSelfHost,
metadata.IsOnSecretsManagerStandalone);
} }

View File

@ -46,7 +46,7 @@ public class PushController : Controller
public async Task PostDelete([FromBody] PushDeviceRequestModel model) public async Task PostDelete([FromBody] PushDeviceRequestModel model)
{ {
CheckUsage(); CheckUsage();
await _pushRegistrationService.DeleteRegistrationAsync(Prefix(model.Id), model.Type); await _pushRegistrationService.DeleteRegistrationAsync(Prefix(model.Id));
} }
[HttpPut("add-organization")] [HttpPut("add-organization")]
@ -54,7 +54,7 @@ public class PushController : Controller
{ {
CheckUsage(); CheckUsage();
await _pushRegistrationService.AddUserRegistrationOrganizationAsync( await _pushRegistrationService.AddUserRegistrationOrganizationAsync(
model.Devices.Select(d => new KeyValuePair<string, Core.Enums.DeviceType>(Prefix(d.Id), d.Type)), model.Devices.Select(d => Prefix(d.Id)),
Prefix(model.OrganizationId)); Prefix(model.OrganizationId));
} }
@ -63,7 +63,7 @@ public class PushController : Controller
{ {
CheckUsage(); CheckUsage();
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync( await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(
model.Devices.Select(d => new KeyValuePair<string, Core.Enums.DeviceType>(Prefix(d.Id), d.Type)), model.Devices.Select(d => Prefix(d.Id)),
Prefix(model.OrganizationId)); Prefix(model.OrganizationId));
} }

View File

@ -1,15 +1,29 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Services;
namespace Bit.Api.Models.Request.Organizations; namespace Bit.Api.Models.Request.Organizations;
public class OrganizationCollectionManagementUpdateRequestModel public class OrganizationCollectionManagementUpdateRequestModel
{ {
public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; }
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
public bool LimitCreateDeleteOwnerAdmin { get; set; } public bool LimitCreateDeleteOwnerAdmin { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; }
public virtual Organization ToOrganization(Organization existingOrganization) public virtual Organization ToOrganization(Organization existingOrganization, IFeatureService featureService)
{ {
existingOrganization.LimitCollectionCreationDeletion = LimitCreateDeleteOwnerAdmin; if (featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
{
existingOrganization.LimitCollectionCreation = LimitCollectionCreation;
existingOrganization.LimitCollectionDeletion = LimitCollectionDeletion;
}
else
{
existingOrganization.LimitCollectionCreationDeletion = LimitCreateDeleteOwnerAdmin || LimitCollectionCreation || LimitCollectionDeletion;
}
existingOrganization.AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems; existingOrganization.AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems;
return existingOrganization; return existingOrganization;
} }

View File

@ -1,5 +1,6 @@
#nullable enable #nullable enable
using System.Diagnostics; using System.Diagnostics;
using Bit.Core;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -123,10 +124,24 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
return true; return true;
} }
// If the limit collection management setting is disabled, allow any user to create collections if (_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
if (await GetOrganizationAbilityAsync(org) is { LimitCollectionCreationDeletion: false })
{ {
return true; var userIsMemberOfOrg = org is not null;
var limitCollectionCreationEnabled = await GetOrganizationAbilityAsync(org) is { LimitCollectionCreation: true };
var userIsOrgOwnerOrAdmin = org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin };
// If the limit collection management setting is disabled, allow any user to create collections
if (userIsMemberOfOrg && (!limitCollectionCreationEnabled || userIsOrgOwnerOrAdmin))
{
return true;
}
}
else
{
// If the limit collection management setting is disabled, allow any user to create collections
if (await GetOrganizationAbilityAsync(org) is { LimitCollectionCreationDeletion: false })
{
return true;
}
} }
// 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
@ -246,21 +261,35 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
return true; return true;
} }
// If AllowAdminAccessToAllCollectionItems is true, Owners and Admins can delete any collection, regardless of LimitCollectionCreationDeletion setting // If AllowAdminAccessToAllCollectionItems is true, Owners and Admins can delete any collection, regardless of LimitCollectionDeletion setting
if (await AllowAdminAccessToAllCollectionItems(org) && org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin }) if (await AllowAdminAccessToAllCollectionItems(org) && org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin })
{ {
return true; return true;
} }
// If LimitCollectionCreationDeletion is false, AllowAdminAccessToAllCollectionItems setting is irrelevant. if (_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
// Ensure acting user has manage permissions for all collections being deleted
// If LimitCollectionCreationDeletion is true, only Owners and Admins can delete collections they manage
var organizationAbility = await GetOrganizationAbilityAsync(org);
var canDeleteManagedCollections = organizationAbility is { LimitCollectionCreationDeletion: false } ||
org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin };
if (canDeleteManagedCollections && await CanManageCollectionsAsync(resources, org))
{ {
return true; var userIsMemberOfOrg = org is not null;
var limitCollectionDeletionEnabled = await GetOrganizationAbilityAsync(org) is { LimitCollectionDeletion: true };
var userIsOrgOwnerOrAdmin = org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin };
// If the limit collection management setting is disabled, allow any user to delete collections
if (userIsMemberOfOrg && (!limitCollectionDeletionEnabled || userIsOrgOwnerOrAdmin) && await CanManageCollectionsAsync(resources, org))
{
return true;
}
}
else
{
// If LimitCollectionCreationDeletion is false, AllowAdminAccessToAllCollectionItems setting is irrelevant.
// Ensure acting user has manage permissions for all collections being deleted
// If LimitCollectionCreationDeletion is true, only Owners and Admins can delete collections they manage
var organizationAbility = await GetOrganizationAbilityAsync(org);
var canDeleteManagedCollections = organizationAbility is { LimitCollectionCreationDeletion: false } ||
org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin };
if (canDeleteManagedCollections && await CanManageCollectionsAsync(resources, org))
{
return true;
}
} }
// Allow providers to delete collections if they are a provider for the target organization // Allow providers to delete collections if they are a provider for the target organization

View File

@ -7,6 +7,7 @@ using Bit.Core.Billing.Enums;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Business; using Bit.Core.Models.Business;
using Bit.Core.Services;
using Bit.Core.Tools.Entities; using Bit.Core.Tools.Entities;
using Bit.Core.Utilities; using Bit.Core.Utilities;
@ -93,7 +94,20 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
/// If set to false, any organization member can create a collection, and any member can delete a collection that /// If set to false, any organization member can create a collection, and any member can delete a collection that
/// they have Can Manage permissions for. /// they have Can Manage permissions for.
/// </summary> /// </summary>
public bool LimitCollectionCreationDeletion { get; set; } public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; }
// Deprecated by https://bitwarden.atlassian.net/browse/PM-10863. This
// was replaced with `LimitCollectionCreation` and
// `LimitCollectionDeletion`.
public bool LimitCollectionCreationDeletion
{
get => LimitCollectionCreation || LimitCollectionDeletion;
set
{
LimitCollectionCreation = value;
LimitCollectionDeletion = value;
}
}
/// <summary> /// <summary>
/// If set to true, admins, owners, and some custom users can read/write all collections and items in the Admin Console. /// If set to true, admins, owners, and some custom users can read/write all collections and items in the Admin Console.
@ -265,7 +279,7 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
return providers[provider]; return providers[provider];
} }
public void UpdateFromLicense(OrganizationLicense license) public void UpdateFromLicense(OrganizationLicense license, IFeatureService featureService)
{ {
// The following properties are intentionally excluded from being updated: // The following properties are intentionally excluded from being updated:
// - Id - self-hosted org will have its own unique Guid // - Id - self-hosted org will have its own unique Guid
@ -300,7 +314,11 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
UseSecretsManager = license.UseSecretsManager; UseSecretsManager = license.UseSecretsManager;
SmSeats = license.SmSeats; SmSeats = license.SmSeats;
SmServiceAccounts = license.SmServiceAccounts; SmServiceAccounts = license.SmServiceAccounts;
LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion;
AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems; if (!featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
{
LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion;
AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems;
}
} }
} }

View File

@ -16,3 +16,30 @@ public enum PolicyType : byte
ActivateAutofill = 11, ActivateAutofill = 11,
AutomaticAppLogIn = 12, AutomaticAppLogIn = 12,
} }
public static class PolicyTypeExtensions
{
/// <summary>
/// Returns the name of the policy for display to the user.
/// Do not include the word "policy" in the return value.
/// </summary>
public static string GetName(this PolicyType type)
{
return type switch
{
PolicyType.TwoFactorAuthentication => "Require two-step login",
PolicyType.MasterPassword => "Master password requirements",
PolicyType.PasswordGenerator => "Password generator",
PolicyType.SingleOrg => "Single organization",
PolicyType.RequireSso => "Require single sign-on authentication",
PolicyType.PersonalOwnership => "Remove individual vault",
PolicyType.DisableSend => "Remove Send",
PolicyType.SendOptions => "Send options",
PolicyType.ResetPassword => "Account recovery administration",
PolicyType.MaximumVaultTimeout => "Vault timeout",
PolicyType.DisablePersonalVaultExport => "Remove individual vault export",
PolicyType.ActivateAutofill => "Active auto-fill",
PolicyType.AutomaticAppLogIn => "Automatically log in users for allowed applications",
};
}
}

View File

@ -21,6 +21,9 @@ public class OrganizationAbility
UseResetPassword = organization.UseResetPassword; UseResetPassword = organization.UseResetPassword;
UseCustomPermissions = organization.UseCustomPermissions; UseCustomPermissions = organization.UseCustomPermissions;
UsePolicies = organization.UsePolicies; UsePolicies = organization.UsePolicies;
LimitCollectionCreation = organization.LimitCollectionCreation;
LimitCollectionDeletion = organization.LimitCollectionDeletion;
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion; LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems; AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
} }
@ -37,6 +40,9 @@ 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 LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; }
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
public bool LimitCollectionCreationDeletion { get; set; } public bool LimitCollectionCreationDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; }
} }

View File

@ -54,6 +54,9 @@ public class OrganizationUserOrganizationDetails
public bool UsePasswordManager { get; set; } public bool UsePasswordManager { get; set; }
public int? SmSeats { get; set; } public int? SmSeats { get; set; }
public int? SmServiceAccounts { get; set; } public int? SmServiceAccounts { get; set; }
public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; }
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
public bool LimitCollectionCreationDeletion { get; set; } public bool LimitCollectionCreationDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; }
} }

View File

@ -144,6 +144,9 @@ public class SelfHostedOrganizationDetails : Organization
RevisionDate = RevisionDate, RevisionDate = RevisionDate,
MaxAutoscaleSeats = MaxAutoscaleSeats, MaxAutoscaleSeats = MaxAutoscaleSeats,
OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling, OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling,
LimitCollectionCreation = LimitCollectionCreation,
LimitCollectionDeletion = LimitCollectionDeletion,
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
LimitCollectionCreationDeletion = LimitCollectionCreationDeletion, LimitCollectionCreationDeletion = LimitCollectionCreationDeletion,
AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems, AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems,
Status = Status Status = Status

View File

@ -40,6 +40,8 @@ public class ProviderUserOrganizationDetails
[JsonConverter(typeof(HtmlEncodingStringConverter))] [JsonConverter(typeof(HtmlEncodingStringConverter))]
public string ProviderName { get; set; } public string ProviderName { get; set; }
public PlanType PlanType { get; set; } public PlanType PlanType { get; set; }
public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; }
public bool LimitCollectionCreationDeletion { get; set; } public bool LimitCollectionCreationDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; } public bool AllowAdminAccessToAllCollectionItems { get; set; }
} }

View File

@ -6,7 +6,6 @@ using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Utilities; using Bit.Core.Utilities;
using Microsoft.Extensions.Logging;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
@ -14,21 +13,15 @@ public class CreateOrganizationDomainCommand : ICreateOrganizationDomainCommand
{ {
private readonly IOrganizationDomainRepository _organizationDomainRepository; private readonly IOrganizationDomainRepository _organizationDomainRepository;
private readonly IEventService _eventService; private readonly IEventService _eventService;
private readonly IDnsResolverService _dnsResolverService;
private readonly ILogger<VerifyOrganizationDomainCommand> _logger;
private readonly IGlobalSettings _globalSettings; private readonly IGlobalSettings _globalSettings;
public CreateOrganizationDomainCommand( public CreateOrganizationDomainCommand(
IOrganizationDomainRepository organizationDomainRepository, IOrganizationDomainRepository organizationDomainRepository,
IEventService eventService, IEventService eventService,
IDnsResolverService dnsResolverService,
ILogger<VerifyOrganizationDomainCommand> logger,
IGlobalSettings globalSettings) IGlobalSettings globalSettings)
{ {
_organizationDomainRepository = organizationDomainRepository; _organizationDomainRepository = organizationDomainRepository;
_eventService = eventService; _eventService = eventService;
_dnsResolverService = dnsResolverService;
_logger = logger;
_globalSettings = globalSettings; _globalSettings = globalSettings;
} }

View File

@ -4,5 +4,6 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfa
public interface IVerifyOrganizationDomainCommand public interface IVerifyOrganizationDomainCommand
{ {
Task<OrganizationDomain> VerifyOrganizationDomainAsync(OrganizationDomain organizationDomain); Task<OrganizationDomain> UserVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain);
Task<OrganizationDomain> SystemVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain);
} }

View File

@ -4,6 +4,7 @@ using Bit.Core.Enums;
using Bit.Core.Exceptions; using Bit.Core.Exceptions;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains; namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
@ -13,34 +14,85 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
private readonly IOrganizationDomainRepository _organizationDomainRepository; private readonly IOrganizationDomainRepository _organizationDomainRepository;
private readonly IDnsResolverService _dnsResolverService; private readonly IDnsResolverService _dnsResolverService;
private readonly IEventService _eventService; private readonly IEventService _eventService;
private readonly IGlobalSettings _globalSettings;
private readonly ILogger<VerifyOrganizationDomainCommand> _logger; private readonly ILogger<VerifyOrganizationDomainCommand> _logger;
public VerifyOrganizationDomainCommand( public VerifyOrganizationDomainCommand(
IOrganizationDomainRepository organizationDomainRepository, IOrganizationDomainRepository organizationDomainRepository,
IDnsResolverService dnsResolverService, IDnsResolverService dnsResolverService,
IEventService eventService, IEventService eventService,
IGlobalSettings globalSettings,
ILogger<VerifyOrganizationDomainCommand> logger) ILogger<VerifyOrganizationDomainCommand> logger)
{ {
_organizationDomainRepository = organizationDomainRepository; _organizationDomainRepository = organizationDomainRepository;
_dnsResolverService = dnsResolverService; _dnsResolverService = dnsResolverService;
_eventService = eventService; _eventService = eventService;
_globalSettings = globalSettings;
_logger = logger; _logger = logger;
} }
public async Task<OrganizationDomain> VerifyOrganizationDomainAsync(OrganizationDomain domain)
public async Task<OrganizationDomain> UserVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain)
{ {
var domainVerificationResult = await VerifyOrganizationDomainAsync(organizationDomain);
await _eventService.LogOrganizationDomainEventAsync(domainVerificationResult,
domainVerificationResult.VerifiedDate != null
? EventType.OrganizationDomain_Verified
: EventType.OrganizationDomain_NotVerified);
await _organizationDomainRepository.ReplaceAsync(domainVerificationResult);
return domainVerificationResult;
}
public async Task<OrganizationDomain> SystemVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain)
{
organizationDomain.SetJobRunCount();
var domainVerificationResult = await VerifyOrganizationDomainAsync(organizationDomain);
if (domainVerificationResult.VerifiedDate is not null)
{
_logger.LogInformation(Constants.BypassFiltersEventId, "Successfully validated domain");
await _eventService.LogOrganizationDomainEventAsync(domainVerificationResult,
EventType.OrganizationDomain_Verified,
EventSystemUser.DomainVerification);
}
else
{
domainVerificationResult.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval);
await _eventService.LogOrganizationDomainEventAsync(domainVerificationResult,
EventType.OrganizationDomain_NotVerified,
EventSystemUser.DomainVerification);
_logger.LogInformation(Constants.BypassFiltersEventId,
"Verification for organization {OrgId} with domain {Domain} failed",
domainVerificationResult.OrganizationId, domainVerificationResult.DomainName);
}
await _organizationDomainRepository.ReplaceAsync(domainVerificationResult);
return domainVerificationResult;
}
private async Task<OrganizationDomain> VerifyOrganizationDomainAsync(OrganizationDomain domain)
{
domain.SetLastCheckedDate();
if (domain.VerifiedDate is not null) if (domain.VerifiedDate is not null)
{ {
domain.SetLastCheckedDate();
await _organizationDomainRepository.ReplaceAsync(domain); await _organizationDomainRepository.ReplaceAsync(domain);
throw new ConflictException("Domain has already been verified."); throw new ConflictException("Domain has already been verified.");
} }
var claimedDomain = var claimedDomain =
await _organizationDomainRepository.GetClaimedDomainsByDomainNameAsync(domain.DomainName); await _organizationDomainRepository.GetClaimedDomainsByDomainNameAsync(domain.DomainName);
if (claimedDomain.Any())
if (claimedDomain.Count > 0)
{ {
domain.SetLastCheckedDate();
await _organizationDomainRepository.ReplaceAsync(domain); await _organizationDomainRepository.ReplaceAsync(domain);
throw new ConflictException("The domain is not available to be claimed."); throw new ConflictException("The domain is not available to be claimed.");
} }
@ -58,11 +110,6 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
domain.DomainName, e.Message); domain.DomainName, e.Message);
} }
domain.SetLastCheckedDate();
await _organizationDomainRepository.ReplaceAsync(domain);
await _eventService.LogOrganizationDomainEventAsync(domain,
domain.VerifiedDate != null ? EventType.OrganizationDomain_Verified : EventType.OrganizationDomain_NotVerified);
return domain; return domain;
} }
} }

View File

@ -180,12 +180,12 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
} }
} }
private async Task<IEnumerable<KeyValuePair<string, DeviceType>>> GetUserDeviceIdsAsync(Guid userId) private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)
{ {
var devices = await _deviceRepository.GetManyByUserIdAsync(userId); var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
return devices return devices
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken)) .Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
.Select(d => new KeyValuePair<string, DeviceType>(d.Id.ToString(), d.Type)); .Select(d => d.Id.ToString());
} }
private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId) private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)

View File

@ -0,0 +1,43 @@
#nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;
/// <summary>
/// Defines behavior and functionality for a given PolicyType.
/// </summary>
public interface IPolicyValidator
{
/// <summary>
/// The PolicyType that this definition relates to.
/// </summary>
public PolicyType Type { get; }
/// <summary>
/// PolicyTypes that must be enabled before this policy can be enabled, if any.
/// These dependencies will be checked when this policy is enabled and when any required policy is disabled.
/// </summary>
public IEnumerable<PolicyType> RequiredPolicies { get; }
/// <summary>
/// Validates a policy before saving it.
/// Do not use this for simple dependencies between different policies - see <see cref="RequiredPolicies"/> instead.
/// Implementation is optional; by default it will not perform any validation.
/// </summary>
/// <param name="policyUpdate">The policy update request</param>
/// <param name="currentPolicy">The current policy, if any</param>
/// <returns>A validation error if validation was unsuccessful, otherwise an empty string</returns>
public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy);
/// <summary>
/// Performs side effects after a policy is validated but before it is saved.
/// For example, this can be used to remove non-compliant users from the organization.
/// Implementation is optional; by default it will not perform any side effects.
/// </summary>
/// <param name="policyUpdate">The policy update request</param>
/// <param name="currentPolicy">The current policy, if any</param>
public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy);
}

View File

@ -0,0 +1,8 @@
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;
public interface ISavePolicyCommand
{
Task SaveAsync(PolicyUpdate policy);
}

View File

@ -0,0 +1,129 @@
#nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Services;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
public class SavePolicyCommand : ISavePolicyCommand
{
private readonly IApplicationCacheService _applicationCacheService;
private readonly IEventService _eventService;
private readonly IPolicyRepository _policyRepository;
private readonly IReadOnlyDictionary<PolicyType, IPolicyValidator> _policyValidators;
private readonly TimeProvider _timeProvider;
public SavePolicyCommand(
IApplicationCacheService applicationCacheService,
IEventService eventService,
IPolicyRepository policyRepository,
IEnumerable<IPolicyValidator> policyValidators,
TimeProvider timeProvider)
{
_applicationCacheService = applicationCacheService;
_eventService = eventService;
_policyRepository = policyRepository;
_timeProvider = timeProvider;
var policyValidatorsDict = new Dictionary<PolicyType, IPolicyValidator>();
foreach (var policyValidator in policyValidators)
{
if (!policyValidatorsDict.TryAdd(policyValidator.Type, policyValidator))
{
throw new Exception($"Duplicate PolicyValidator for {policyValidator.Type} policy.");
}
}
_policyValidators = policyValidatorsDict;
}
public async Task SaveAsync(PolicyUpdate policyUpdate)
{
var org = await _applicationCacheService.GetOrganizationAbilityAsync(policyUpdate.OrganizationId);
if (org == null)
{
throw new BadRequestException("Organization not found");
}
if (!org.UsePolicies)
{
throw new BadRequestException("This organization cannot use policies.");
}
if (_policyValidators.TryGetValue(policyUpdate.Type, out var validator))
{
await RunValidatorAsync(validator, policyUpdate);
}
var policy = await _policyRepository.GetByOrganizationIdTypeAsync(policyUpdate.OrganizationId, policyUpdate.Type)
?? new Policy
{
OrganizationId = policyUpdate.OrganizationId,
Type = policyUpdate.Type,
CreationDate = _timeProvider.GetUtcNow().UtcDateTime
};
policy.Enabled = policyUpdate.Enabled;
policy.Data = policyUpdate.Data;
policy.RevisionDate = _timeProvider.GetUtcNow().UtcDateTime;
await _policyRepository.UpsertAsync(policy);
await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated);
}
private async Task RunValidatorAsync(IPolicyValidator validator, PolicyUpdate policyUpdate)
{
var savedPolicies = await _policyRepository.GetManyByOrganizationIdAsync(policyUpdate.OrganizationId);
// Note: policies may be missing from this dict if they have never been enabled
var savedPoliciesDict = savedPolicies.ToDictionary(p => p.Type);
var currentPolicy = savedPoliciesDict.GetValueOrDefault(policyUpdate.Type);
// If enabling this policy - check that all policy requirements are satisfied
if (currentPolicy is not { Enabled: true } && policyUpdate.Enabled)
{
var missingRequiredPolicyTypes = validator.RequiredPolicies
.Where(requiredPolicyType =>
savedPoliciesDict.GetValueOrDefault(requiredPolicyType) is not { Enabled: true })
.ToList();
if (missingRequiredPolicyTypes.Count != 0)
{
throw new BadRequestException($"Turn on the {missingRequiredPolicyTypes.First().GetName()} policy because it is required for the {validator.Type.GetName()} policy.");
}
}
// If disabling this policy - ensure it's not required by any other policy
if (currentPolicy is { Enabled: true } && !policyUpdate.Enabled)
{
var dependentPolicyTypes = _policyValidators.Values
.Where(otherValidator => otherValidator.RequiredPolicies.Contains(policyUpdate.Type))
.Select(otherValidator => otherValidator.Type)
.Where(otherPolicyType => savedPoliciesDict.ContainsKey(otherPolicyType) &&
savedPoliciesDict[otherPolicyType].Enabled)
.ToList();
switch (dependentPolicyTypes)
{
case { Count: 1 }:
throw new BadRequestException($"Turn off the {dependentPolicyTypes.First().GetName()} policy because it requires the {validator.Type.GetName()} policy.");
case { Count: > 1 }:
throw new BadRequestException($"Turn off all of the policies that require the {validator.Type.GetName()} policy.");
}
}
// Run other validation
var validationError = await validator.ValidateAsync(policyUpdate, currentPolicy);
if (!string.IsNullOrEmpty(validationError))
{
throw new BadRequestException(validationError);
}
// Run side effects
await validator.OnSaveSideEffectsAsync(policyUpdate, currentPolicy);
}
}

View File

@ -0,0 +1,28 @@
#nullable enable
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.Utilities;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
/// <summary>
/// A request for SavePolicyCommand to update a policy
/// </summary>
public record PolicyUpdate
{
public Guid OrganizationId { get; set; }
public PolicyType Type { get; set; }
public string? Data { get; set; }
public bool Enabled { get; set; }
public T GetDataModel<T>() where T : IPolicyDataModel, new()
{
return CoreHelpers.LoadClassFromJsonData<T>(Data);
}
public void SetDataModel<T>(T dataModel) where T : IPolicyDataModel, new()
{
Data = CoreHelpers.ClassToJsonData(dataModel);
}
}

View File

@ -0,0 +1,22 @@
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Implementations;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
using Bit.Core.AdminConsole.Services;
using Bit.Core.AdminConsole.Services.Implementations;
using Microsoft.Extensions.DependencyInjection;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;
public static class PolicyServiceCollectionExtensions
{
public static void AddPolicyServices(this IServiceCollection services)
{
services.AddScoped<IPolicyService, PolicyService>();
services.AddScoped<ISavePolicyCommand, SavePolicyCommand>();
services.AddScoped<IPolicyValidator, TwoFactorAuthenticationPolicyValidator>();
services.AddScoped<IPolicyValidator, SingleOrgPolicyValidator>();
services.AddScoped<IPolicyValidator, RequireSsoPolicyValidator>();
services.AddScoped<IPolicyValidator, ResetPasswordPolicyValidator>();
services.AddScoped<IPolicyValidator, MaximumVaultTimeoutPolicyValidator>();
}
}

View File

@ -0,0 +1,15 @@
#nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
public class MaximumVaultTimeoutPolicyValidator : IPolicyValidator
{
public PolicyType Type => PolicyType.MaximumVaultTimeout;
public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];
public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult("");
public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(0);
}

View File

@ -0,0 +1,33 @@
#nullable enable
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
public static class PolicyValidatorHelpers
{
/// <summary>
/// Validate that given Member Decryption Options are not enabled.
/// Used for validation when disabling a policy that is required by certain Member Decryption Options.
/// </summary>
/// <param name="decryptionOptions">The Member Decryption Options that require the policy to be enabled.</param>
/// <returns>A validation error if validation was unsuccessful, otherwise an empty string</returns>
public static string ValidateDecryptionOptionsNotEnabled(this SsoConfig? ssoConfig,
MemberDecryptionType[] decryptionOptions)
{
if (ssoConfig is not { Enabled: true })
{
return "";
}
return ssoConfig.GetData().MemberDecryptionType switch
{
MemberDecryptionType.KeyConnector when decryptionOptions.Contains(MemberDecryptionType.KeyConnector)
=> "Key Connector is enabled and requires this policy.",
MemberDecryptionType.TrustedDeviceEncryption when decryptionOptions.Contains(MemberDecryptionType
.TrustedDeviceEncryption) => "Trusted device encryption is on and requires this policy.",
_ => ""
};
}
}

View File

@ -0,0 +1,38 @@
#nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
public class RequireSsoPolicyValidator : IPolicyValidator
{
private readonly ISsoConfigRepository _ssoConfigRepository;
public RequireSsoPolicyValidator(ISsoConfigRepository ssoConfigRepository)
{
_ssoConfigRepository = ssoConfigRepository;
}
public PolicyType Type => PolicyType.RequireSso;
public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];
public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
{
if (policyUpdate is not { Enabled: true })
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(policyUpdate.OrganizationId);
return ssoConfig.ValidateDecryptionOptionsNotEnabled([
MemberDecryptionType.KeyConnector,
MemberDecryptionType.TrustedDeviceEncryption
]);
}
return "";
}
public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(0);
}

View File

@ -0,0 +1,36 @@
#nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
public class ResetPasswordPolicyValidator : IPolicyValidator
{
private readonly ISsoConfigRepository _ssoConfigRepository;
public PolicyType Type => PolicyType.ResetPassword;
public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];
public ResetPasswordPolicyValidator(ISsoConfigRepository ssoConfigRepository)
{
_ssoConfigRepository = ssoConfigRepository;
}
public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
{
if (policyUpdate is not { Enabled: true } ||
policyUpdate.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled == false)
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(policyUpdate.OrganizationId);
return ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.TrustedDeviceEncryption]);
}
return "";
}
public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(0);
}

View File

@ -0,0 +1,101 @@
#nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
public class SingleOrgPolicyValidator : IPolicyValidator
{
public PolicyType Type => PolicyType.SingleOrg;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IMailService _mailService;
private readonly IOrganizationRepository _organizationRepository;
private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly ICurrentContext _currentContext;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
public SingleOrgPolicyValidator(
IOrganizationUserRepository organizationUserRepository,
IMailService mailService,
IOrganizationRepository organizationRepository,
ISsoConfigRepository ssoConfigRepository,
ICurrentContext currentContext,
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
{
_organizationUserRepository = organizationUserRepository;
_mailService = mailService;
_organizationRepository = organizationRepository;
_ssoConfigRepository = ssoConfigRepository;
_currentContext = currentContext;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
}
public IEnumerable<PolicyType> RequiredPolicies => [];
public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
{
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
{
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
}
}
private async Task RemoveNonCompliantUsersAsync(Guid organizationId)
{
// Remove non-compliant users
var savingUserId = _currentContext.UserId;
// Note: must get OrganizationUserUserDetails so that Email is always populated from the User object
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
var org = await _organizationRepository.GetByIdAsync(organizationId);
if (org == null)
{
throw new NotFoundException("Organization not found.");
}
var removableOrgUsers = orgUsers.Where(ou =>
ou.Status != OrganizationUserStatusType.Invited &&
ou.Status != OrganizationUserStatusType.Revoked &&
ou.Type != OrganizationUserType.Owner &&
ou.Type != OrganizationUserType.Admin &&
ou.UserId != savingUserId
).ToList();
var userOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(
removableOrgUsers.Select(ou => ou.UserId!.Value));
foreach (var orgUser in removableOrgUsers)
{
if (userOrgs.Any(ou => ou.UserId == orgUser.UserId
&& ou.OrganizationId != org.Id
&& ou.Status != OrganizationUserStatusType.Invited))
{
await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id,
savingUserId);
await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(
org.DisplayName(), orgUser.Email);
}
}
}
public async Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
{
if (policyUpdate is not { Enabled: true })
{
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(policyUpdate.OrganizationId);
return ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]);
}
return "";
}
}

View File

@ -0,0 +1,87 @@
#nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Context;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
public class TwoFactorAuthenticationPolicyValidator : IPolicyValidator
{
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IMailService _mailService;
private readonly IOrganizationRepository _organizationRepository;
private readonly ICurrentContext _currentContext;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
public PolicyType Type => PolicyType.TwoFactorAuthentication;
public IEnumerable<PolicyType> RequiredPolicies => [];
public TwoFactorAuthenticationPolicyValidator(
IOrganizationUserRepository organizationUserRepository,
IMailService mailService,
IOrganizationRepository organizationRepository,
ICurrentContext currentContext,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
{
_organizationUserRepository = organizationUserRepository;
_mailService = mailService;
_organizationRepository = organizationRepository;
_currentContext = currentContext;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
}
public async Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
{
if (currentPolicy is not { Enabled: true } && policyUpdate is { Enabled: true })
{
await RemoveNonCompliantUsersAsync(policyUpdate.OrganizationId);
}
}
private async Task RemoveNonCompliantUsersAsync(Guid organizationId)
{
var org = await _organizationRepository.GetByIdAsync(organizationId);
var savingUserId = _currentContext.UserId;
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(organizationId);
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(orgUsers);
var removableOrgUsers = orgUsers.Where(ou =>
ou.Status != OrganizationUserStatusType.Invited && ou.Status != OrganizationUserStatusType.Revoked &&
ou.Type != OrganizationUserType.Owner && ou.Type != OrganizationUserType.Admin &&
ou.UserId != savingUserId);
// Reorder by HasMasterPassword to prioritize checking users without a master if they have 2FA enabled
foreach (var orgUser in removableOrgUsers.OrderBy(ou => ou.HasMasterPassword))
{
var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == orgUser.Id)
.twoFactorIsEnabled;
if (!userTwoFactorEnabled)
{
if (!orgUser.HasMasterPassword)
{
throw new BadRequestException(
"Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.");
}
await _removeOrganizationUserCommand.RemoveUserAsync(organizationId, orgUser.Id,
savingUserId);
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
org!.DisplayName(), orgUser.Email);
}
}
}
public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult("");
}

View File

@ -4,13 +4,12 @@ using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data.Organizations.OrganizationUsers; using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Services;
namespace Bit.Core.AdminConsole.Services; namespace Bit.Core.AdminConsole.Services;
public interface IPolicyService public interface IPolicyService
{ {
Task SaveAsync(Policy policy, IOrganizationService organizationService, Guid? savingUserId); Task SaveAsync(Policy policy, Guid? savingUserId);
/// <summary> /// <summary>
/// Get the combined master password policy options for the specified user. /// Get the combined master password policy options for the specified user.

View File

@ -1,4 +1,5 @@
using Bit.Core.Enums; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.Enums;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
@ -10,26 +11,29 @@ public class OrganizationDomainService : IOrganizationDomainService
{ {
private readonly IOrganizationDomainRepository _domainRepository; private readonly IOrganizationDomainRepository _domainRepository;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IDnsResolverService _dnsResolverService;
private readonly IEventService _eventService; private readonly IEventService _eventService;
private readonly IMailService _mailService; private readonly IMailService _mailService;
private readonly IVerifyOrganizationDomainCommand _verifyOrganizationDomainCommand;
private readonly TimeProvider _timeProvider;
private readonly ILogger<OrganizationDomainService> _logger; private readonly ILogger<OrganizationDomainService> _logger;
private readonly IGlobalSettings _globalSettings; private readonly IGlobalSettings _globalSettings;
public OrganizationDomainService( public OrganizationDomainService(
IOrganizationDomainRepository domainRepository, IOrganizationDomainRepository domainRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IDnsResolverService dnsResolverService,
IEventService eventService, IEventService eventService,
IMailService mailService, IMailService mailService,
IVerifyOrganizationDomainCommand verifyOrganizationDomainCommand,
TimeProvider timeProvider,
ILogger<OrganizationDomainService> logger, ILogger<OrganizationDomainService> logger,
IGlobalSettings globalSettings) IGlobalSettings globalSettings)
{ {
_domainRepository = domainRepository; _domainRepository = domainRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_dnsResolverService = dnsResolverService;
_eventService = eventService; _eventService = eventService;
_mailService = mailService; _mailService = mailService;
_verifyOrganizationDomainCommand = verifyOrganizationDomainCommand;
_timeProvider = timeProvider;
_logger = logger; _logger = logger;
_globalSettings = globalSettings; _globalSettings = globalSettings;
} }
@ -37,7 +41,7 @@ public class OrganizationDomainService : IOrganizationDomainService
public async Task ValidateOrganizationsDomainAsync() public async Task ValidateOrganizationsDomainAsync()
{ {
//Date should be set 1 hour behind to ensure it selects all domains that should be verified //Date should be set 1 hour behind to ensure it selects all domains that should be verified
var runDate = DateTime.UtcNow.AddHours(-1); var runDate = _timeProvider.GetUtcNow().UtcDateTime.AddHours(-1);
var verifiableDomains = await _domainRepository.GetManyByNextRunDateAsync(runDate); var verifiableDomains = await _domainRepository.GetManyByNextRunDateAsync(runDate);
@ -45,43 +49,17 @@ public class OrganizationDomainService : IOrganizationDomainService
foreach (var domain in verifiableDomains) foreach (var domain in verifiableDomains)
{ {
_logger.LogInformation(Constants.BypassFiltersEventId,
"Attempting verification for organization {OrgId} with domain {Domain}",
domain.OrganizationId,
domain.DomainName);
try try
{ {
_logger.LogInformation(Constants.BypassFiltersEventId, "Attempting verification for organization {OrgId} with domain {Domain}", domain.OrganizationId, domain.DomainName); _ = await _verifyOrganizationDomainCommand.SystemVerifyOrganizationDomainAsync(domain);
var status = await _dnsResolverService.ResolveAsync(domain.DomainName, domain.Txt);
if (status)
{
_logger.LogInformation(Constants.BypassFiltersEventId, "Successfully validated domain");
// Update entry on OrganizationDomain table
domain.SetLastCheckedDate();
domain.SetVerifiedDate();
domain.SetJobRunCount();
await _domainRepository.ReplaceAsync(domain);
await _eventService.LogOrganizationDomainEventAsync(domain, EventType.OrganizationDomain_Verified,
EventSystemUser.DomainVerification);
}
else
{
// Update entry on OrganizationDomain table
domain.SetLastCheckedDate();
domain.SetJobRunCount();
domain.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval);
await _domainRepository.ReplaceAsync(domain);
await _eventService.LogOrganizationDomainEventAsync(domain, EventType.OrganizationDomain_NotVerified,
EventSystemUser.DomainVerification);
_logger.LogInformation(Constants.BypassFiltersEventId, "Verification for organization {OrgId} with domain {Domain} failed",
domain.OrganizationId, domain.DomainName);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
// Update entry on OrganizationDomain table
domain.SetLastCheckedDate();
domain.SetJobRunCount();
domain.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval); domain.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval);
await _domainRepository.ReplaceAsync(domain); await _domainRepository.ReplaceAsync(domain);

View File

@ -708,10 +708,16 @@ public class OrganizationService : IOrganizationService
UseSecretsManager = license.UseSecretsManager, UseSecretsManager = license.UseSecretsManager,
SmSeats = license.SmSeats, SmSeats = license.SmSeats,
SmServiceAccounts = license.SmServiceAccounts, SmServiceAccounts = license.SmServiceAccounts,
LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion,
AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems,
}; };
// These fields are being removed from consideration when processing
// licenses.
if (!_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
{
organization.LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion;
organization.AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems;
}
var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false); var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);
var dir = $"{_globalSettings.LicenseDirectory}/organization"; var dir = $"{_globalSettings.LicenseDirectory}/organization";
@ -1832,12 +1838,12 @@ public class OrganizationService : IOrganizationService
} }
private async Task<IEnumerable<KeyValuePair<string, DeviceType>>> GetUserDeviceIdsAsync(Guid userId) private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)
{ {
var devices = await _deviceRepository.GetManyByUserIdAsync(userId); var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
return devices return devices
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken)) .Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
.Select(d => new KeyValuePair<string, DeviceType>(d.Id.ToString(), d.Type)); .Select(d => d.Id.ToString());
} }
public async Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null) public async Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null)

View File

@ -2,6 +2,8 @@
using Bit.Core.AdminConsole.Enums; using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies; using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces; using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using Bit.Core.AdminConsole.Repositories; using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Enums; using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories; using Bit.Core.Auth.Repositories;
@ -27,6 +29,8 @@ public class PolicyService : IPolicyService
private readonly IMailService _mailService; private readonly IMailService _mailService;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery; private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IFeatureService _featureService;
private readonly ISavePolicyCommand _savePolicyCommand;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand; private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
public PolicyService( public PolicyService(
@ -39,6 +43,8 @@ public class PolicyService : IPolicyService
IMailService mailService, IMailService mailService,
GlobalSettings globalSettings, GlobalSettings globalSettings,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery, ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IFeatureService featureService,
ISavePolicyCommand savePolicyCommand,
IRemoveOrganizationUserCommand removeOrganizationUserCommand) IRemoveOrganizationUserCommand removeOrganizationUserCommand)
{ {
_applicationCacheService = applicationCacheService; _applicationCacheService = applicationCacheService;
@ -50,11 +56,28 @@ public class PolicyService : IPolicyService
_mailService = mailService; _mailService = mailService;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery; _twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_featureService = featureService;
_savePolicyCommand = savePolicyCommand;
_removeOrganizationUserCommand = removeOrganizationUserCommand; _removeOrganizationUserCommand = removeOrganizationUserCommand;
} }
public async Task SaveAsync(Policy policy, IOrganizationService organizationService, Guid? savingUserId) public async Task SaveAsync(Policy policy, Guid? savingUserId)
{ {
if (_featureService.IsEnabled(FeatureFlagKeys.Pm13322AddPolicyDefinitions))
{
// Transitional mapping - this will be moved to callers once the feature flag is removed
var policyUpdate = new PolicyUpdate
{
OrganizationId = policy.OrganizationId,
Type = policy.Type,
Enabled = policy.Enabled,
Data = policy.Data
};
await _savePolicyCommand.SaveAsync(policyUpdate);
return;
}
var org = await _organizationRepository.GetByIdAsync(policy.OrganizationId); var org = await _organizationRepository.GetByIdAsync(policy.OrganizationId);
if (org == null) if (org == null)
{ {
@ -88,7 +111,7 @@ public class PolicyService : IPolicyService
return; return;
} }
await EnablePolicyAsync(policy, org, organizationService, savingUserId); await EnablePolicyAsync(policy, org, savingUserId);
} }
public async Task<MasterPasswordPolicyData> GetMasterPasswordPolicyForUserAsync(User user) public async Task<MasterPasswordPolicyData> GetMasterPasswordPolicyForUserAsync(User user)
@ -262,7 +285,7 @@ public class PolicyService : IPolicyService
await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated); await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated);
} }
private async Task EnablePolicyAsync(Policy policy, Organization org, IOrganizationService organizationService, Guid? savingUserId) private async Task EnablePolicyAsync(Policy policy, Organization org, Guid? savingUserId)
{ {
var currentPolicy = await _policyRepository.GetByIdAsync(policy.Id); var currentPolicy = await _policyRepository.GetByIdAsync(policy.Id);
if (!currentPolicy?.Enabled ?? true) if (!currentPolicy?.Enabled ?? true)

View File

@ -20,7 +20,6 @@ public class SsoConfigService : ISsoConfigService
private readonly IPolicyService _policyService; private readonly IPolicyService _policyService;
private readonly IOrganizationRepository _organizationRepository; private readonly IOrganizationRepository _organizationRepository;
private readonly IOrganizationUserRepository _organizationUserRepository; private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationService _organizationService;
private readonly IEventService _eventService; private readonly IEventService _eventService;
public SsoConfigService( public SsoConfigService(
@ -29,7 +28,6 @@ public class SsoConfigService : ISsoConfigService
IPolicyService policyService, IPolicyService policyService,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository, IOrganizationUserRepository organizationUserRepository,
IOrganizationService organizationService,
IEventService eventService) IEventService eventService)
{ {
_ssoConfigRepository = ssoConfigRepository; _ssoConfigRepository = ssoConfigRepository;
@ -37,7 +35,6 @@ public class SsoConfigService : ISsoConfigService
_policyService = policyService; _policyService = policyService;
_organizationRepository = organizationRepository; _organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository; _organizationUserRepository = organizationUserRepository;
_organizationService = organizationService;
_eventService = eventService; _eventService = eventService;
} }
@ -71,20 +68,20 @@ public class SsoConfigService : ISsoConfigService
singleOrgPolicy.Enabled = true; singleOrgPolicy.Enabled = true;
await _policyService.SaveAsync(singleOrgPolicy, _organizationService, null); await _policyService.SaveAsync(singleOrgPolicy, null);
var resetPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.ResetPassword) ?? var resetPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.ResetPassword) ??
new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.ResetPassword, }; new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.ResetPassword, };
resetPolicy.Enabled = true; resetPolicy.Enabled = true;
resetPolicy.SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = true }); resetPolicy.SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = true });
await _policyService.SaveAsync(resetPolicy, _organizationService, null); await _policyService.SaveAsync(resetPolicy, null);
var ssoRequiredPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.RequireSso) ?? var ssoRequiredPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.RequireSso) ??
new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.RequireSso, }; new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.RequireSso, };
ssoRequiredPolicy.Enabled = true; ssoRequiredPolicy.Enabled = true;
await _policyService.SaveAsync(ssoRequiredPolicy, _organizationService, null); await _policyService.SaveAsync(ssoRequiredPolicy, null);
} }
await LogEventsAsync(config, oldConfig); await LogEventsAsync(config, oldConfig);

View File

@ -3,17 +3,14 @@
public enum ProviderMigrationProgress public enum ProviderMigrationProgress
{ {
Started = 1, Started = 1,
ClientsMigrated = 2, NoClients = 2,
TeamsPlanConfigured = 3, ClientsMigrated = 3,
EnterprisePlanConfigured = 4, TeamsPlanConfigured = 4,
CustomerSetup = 5, EnterprisePlanConfigured = 5,
SubscriptionSetup = 6, CustomerSetup = 6,
CreditApplied = 7, SubscriptionSetup = 7,
Completed = 8, CreditApplied = 8,
Completed = 9,
Reversing = 9,
ReversedClientMigrations = 10,
RemovedProviderPlans = 11
} }
public class ProviderMigrationTracker public class ProviderMigrationTracker

View File

@ -41,7 +41,18 @@ public class ProviderMigrator(
await migrationTrackerCache.StartTracker(provider); await migrationTrackerCache.StartTracker(provider);
await MigrateClientsAsync(providerId); var organizations = await GetClientsAsync(provider.Id);
if (organizations.Count == 0)
{
logger.LogInformation("CB: Skipping migration for provider ({ProviderID}) with no clients", providerId);
await migrationTrackerCache.UpdateTrackingStatus(providerId, ProviderMigrationProgress.NoClients);
return;
}
await MigrateClientsAsync(providerId, organizations);
await ConfigureTeamsPlanAsync(providerId); await ConfigureTeamsPlanAsync(providerId);
@ -65,6 +76,16 @@ public class ProviderMigrator(
return null; return null;
} }
if (providerTracker.Progress == ProviderMigrationProgress.NoClients)
{
return new ProviderMigrationResult
{
ProviderId = providerTracker.ProviderId,
ProviderName = providerTracker.ProviderName,
Result = providerTracker.Progress.ToString()
};
}
var clientTrackers = await Task.WhenAll(providerTracker.OrganizationIds.Select(organizationId => var clientTrackers = await Task.WhenAll(providerTracker.OrganizationIds.Select(organizationId =>
migrationTrackerCache.GetTracker(providerId, organizationId))); migrationTrackerCache.GetTracker(providerId, organizationId)));
@ -99,12 +120,10 @@ public class ProviderMigrator(
#region Steps #region Steps
private async Task MigrateClientsAsync(Guid providerId) private async Task MigrateClientsAsync(Guid providerId, List<Organization> organizations)
{ {
logger.LogInformation("CB: Migrating clients for provider ({ProviderID})", providerId); logger.LogInformation("CB: Migrating clients for provider ({ProviderID})", providerId);
var organizations = await GetEnabledClientsAsync(providerId);
var organizationIds = organizations.Select(organization => organization.Id); var organizationIds = organizations.Select(organization => organization.Id);
await migrationTrackerCache.SetOrganizationIds(providerId, organizationIds); await migrationTrackerCache.SetOrganizationIds(providerId, organizationIds);
@ -129,7 +148,7 @@ public class ProviderMigrator(
{ {
logger.LogInformation("CB: Configuring Teams plan for provider ({ProviderID})", providerId); logger.LogInformation("CB: Configuring Teams plan for provider ({ProviderID})", providerId);
var organizations = await GetEnabledClientsAsync(providerId); var organizations = await GetClientsAsync(providerId);
var teamsSeats = organizations var teamsSeats = organizations
.Where(IsTeams) .Where(IsTeams)
@ -172,7 +191,7 @@ public class ProviderMigrator(
{ {
logger.LogInformation("CB: Configuring Enterprise plan for provider ({ProviderID})", providerId); logger.LogInformation("CB: Configuring Enterprise plan for provider ({ProviderID})", providerId);
var organizations = await GetEnabledClientsAsync(providerId); var organizations = await GetClientsAsync(providerId);
var enterpriseSeats = organizations var enterpriseSeats = organizations
.Where(IsEnterprise) .Where(IsEnterprise)
@ -215,7 +234,7 @@ public class ProviderMigrator(
{ {
if (string.IsNullOrEmpty(provider.GatewayCustomerId)) if (string.IsNullOrEmpty(provider.GatewayCustomerId))
{ {
var organizations = await GetEnabledClientsAsync(provider.Id); var organizations = await GetClientsAsync(provider.Id);
var sampleOrganization = organizations.FirstOrDefault(organization => !string.IsNullOrEmpty(organization.GatewayCustomerId)); var sampleOrganization = organizations.FirstOrDefault(organization => !string.IsNullOrEmpty(organization.GatewayCustomerId));
@ -299,28 +318,43 @@ public class ProviderMigrator(
private async Task ApplyCreditAsync(Provider provider) private async Task ApplyCreditAsync(Provider provider)
{ {
var organizations = await GetEnabledClientsAsync(provider.Id); var organizations = await GetClientsAsync(provider.Id);
var organizationCustomers = var organizationCustomers =
await Task.WhenAll(organizations.Select(organization => stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId))); await Task.WhenAll(organizations.Select(organization => stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId)));
var organizationCancellationCredit = organizationCustomers.Sum(customer => customer.Balance); var organizationCancellationCredit = organizationCustomers.Sum(customer => customer.Balance);
var legacyOrganizations = organizations.Where(organization => await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId,
organization.PlanType is new CustomerBalanceTransactionCreateOptions
{
Amount = organizationCancellationCredit,
Currency = "USD",
Description = "Unused, prorated time for client organization subscriptions."
});
var migrationRecords = await Task.WhenAll(organizations.Select(organization =>
clientOrganizationMigrationRecordRepository.GetByOrganizationId(organization.Id)));
var legacyOrganizationMigrationRecords = migrationRecords.Where(migrationRecord =>
migrationRecord.PlanType is
PlanType.EnterpriseAnnually2020 or PlanType.EnterpriseAnnually2020 or
PlanType.EnterpriseMonthly2020 or PlanType.TeamsAnnually2020);
PlanType.TeamsAnnually2020 or
PlanType.TeamsMonthly2020);
var legacyOrganizationCredit = legacyOrganizations.Sum(organization => organization.Seats ?? 0); var legacyOrganizationCredit = legacyOrganizationMigrationRecords.Sum(migrationRecord => migrationRecord.Seats) * 12 * -100;
await stripeAdapter.CustomerUpdateAsync(provider.GatewayCustomerId, new CustomerUpdateOptions if (legacyOrganizationCredit < 0)
{ {
Balance = organizationCancellationCredit + legacyOrganizationCredit await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId,
}); new CustomerBalanceTransactionCreateOptions
{
Amount = legacyOrganizationCredit,
Currency = "USD",
Description = "1 year rebate for legacy client organizations."
});
}
logger.LogInformation("CB: Applied {Credit} credit to provider ({ProviderID})", organizationCancellationCredit, provider.Id); logger.LogInformation("CB: Applied {Credit} credit to provider ({ProviderID})", organizationCancellationCredit + legacyOrganizationCredit, provider.Id);
await migrationTrackerCache.UpdateTrackingStatus(provider.Id, ProviderMigrationProgress.CreditApplied); await migrationTrackerCache.UpdateTrackingStatus(provider.Id, ProviderMigrationProgress.CreditApplied);
} }
@ -340,13 +374,12 @@ public class ProviderMigrator(
#region Utilities #region Utilities
private async Task<List<Organization>> GetEnabledClientsAsync(Guid providerId) private async Task<List<Organization>> GetClientsAsync(Guid providerId)
{ {
var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId); var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId);
return (await Task.WhenAll(providerOrganizations.Select(providerOrganization => return (await Task.WhenAll(providerOrganizations.Select(providerOrganization =>
organizationRepository.GetByIdAsync(providerOrganization.OrganizationId)))) organizationRepository.GetByIdAsync(providerOrganization.OrganizationId))))
.Where(organization => organization.Enabled)
.ToList(); .ToList();
} }

View File

@ -1,8 +1,10 @@
namespace Bit.Core.Billing.Models; namespace Bit.Core.Billing.Models;
public record OrganizationMetadata( public record OrganizationMetadata(
bool IsEligibleForSelfHost,
bool IsOnSecretsManagerStandalone) bool IsOnSecretsManagerStandalone)
{ {
public static OrganizationMetadata Default() => new( public static OrganizationMetadata Default() => new(
IsOnSecretsManagerStandalone: default); IsEligibleForSelfHost: false,
IsOnSecretsManagerStandalone: false);
} }

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities; using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Caches; using Bit.Core.Billing.Caches;
using Bit.Core.Billing.Constants; using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Models; using Bit.Core.Billing.Models;
@ -26,6 +27,7 @@ public class OrganizationBillingService(
IGlobalSettings globalSettings, IGlobalSettings globalSettings,
ILogger<OrganizationBillingService> logger, ILogger<OrganizationBillingService> logger,
IOrganizationRepository organizationRepository, IOrganizationRepository organizationRepository,
IProviderRepository providerRepository,
ISetupIntentCache setupIntentCache, ISetupIntentCache setupIntentCache,
IStripeAdapter stripeAdapter, IStripeAdapter stripeAdapter,
ISubscriberService subscriberService) : IOrganizationBillingService ISubscriberService subscriberService) : IOrganizationBillingService
@ -69,14 +71,11 @@ public class OrganizationBillingService(
var subscription = await subscriberService.GetSubscription(organization); var subscription = await subscriberService.GetSubscription(organization);
if (customer == null || subscription == null) var isEligibleForSelfHost = await IsEligibleForSelfHost(organization, subscription);
{
return OrganizationMetadata.Default();
}
var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription); var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription);
return new OrganizationMetadata(isOnSecretsManagerStandalone); return new OrganizationMetadata(isEligibleForSelfHost, isOnSecretsManagerStandalone);
} }
public async Task UpdatePaymentMethod( public async Task UpdatePaymentMethod(
@ -340,11 +339,38 @@ public class OrganizationBillingService(
return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions); return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
} }
private async Task<bool> IsEligibleForSelfHost(
Organization organization,
Subscription? organizationSubscription)
{
if (organization.Status != OrganizationStatusType.Managed)
{
return organization.Plan.Contains("Families") ||
organization.Plan.Contains("Enterprise") && IsActive(organizationSubscription);
}
var provider = await providerRepository.GetByOrganizationIdAsync(organization.Id);
var providerSubscription = await subscriberService.GetSubscriptionOrThrow(provider);
return organization.Plan.Contains("Enterprise") && IsActive(providerSubscription);
bool IsActive(Subscription? subscription) => subscription?.Status is
StripeConstants.SubscriptionStatus.Active or
StripeConstants.SubscriptionStatus.Trialing or
StripeConstants.SubscriptionStatus.PastDue;
}
private static bool IsOnSecretsManagerStandalone( private static bool IsOnSecretsManagerStandalone(
Organization organization, Organization organization,
Customer customer, Customer? customer,
Subscription subscription) Subscription? subscription)
{ {
if (customer == null || subscription == null)
{
return false;
}
var plan = StaticStore.GetPlan(organization.PlanType); var plan = StaticStore.GetPlan(organization.PlanType);
if (!plan.SupportsSecretsManager) if (!plan.SupportsSecretsManager)

View File

@ -146,6 +146,8 @@ public static class FeatureFlagKeys
public const string RemoveServerVersionHeader = "remove-server-version-header"; public const string RemoveServerVersionHeader = "remove-server-version-header";
public const string AccessIntelligence = "pm-13227-access-intelligence"; public const string AccessIntelligence = "pm-13227-access-intelligence";
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint"; public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
public const string Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions";
public const string LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split";
public static List<string> GetAllKeys() public static List<string> GetAllKeys()
{ {

View File

@ -1,5 +1,4 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
namespace Bit.Core.Models.Api; namespace Bit.Core.Models.Api;
@ -7,6 +6,4 @@ public class PushDeviceRequestModel
{ {
[Required] [Required]
public string Id { get; set; } public string Id { get; set; }
[Required]
public DeviceType Type { get; set; }
} }

View File

@ -1,5 +1,4 @@
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations;
using Bit.Core.Enums;
namespace Bit.Core.Models.Api; namespace Bit.Core.Models.Api;
@ -8,9 +7,9 @@ public class PushUpdateRequestModel
public PushUpdateRequestModel() public PushUpdateRequestModel()
{ } { }
public PushUpdateRequestModel(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId) public PushUpdateRequestModel(IEnumerable<string> deviceIds, string organizationId)
{ {
Devices = devices.Select(d => new PushDeviceRequestModel { Id = d.Key, Type = d.Value }); Devices = deviceIds.Select(d => new PushDeviceRequestModel { Id = d });
OrganizationId = organizationId; OrganizationId = organizationId;
} }

View File

@ -53,8 +53,11 @@ public class OrganizationLicense : ILicense
UseSecretsManager = org.UseSecretsManager; UseSecretsManager = org.UseSecretsManager;
SmSeats = org.SmSeats; SmSeats = org.SmSeats;
SmServiceAccounts = org.SmServiceAccounts; SmServiceAccounts = org.SmServiceAccounts;
// Deprecated. Left for backwards compatibility with old license versions.
LimitCollectionCreationDeletion = org.LimitCollectionCreationDeletion; LimitCollectionCreationDeletion = org.LimitCollectionCreationDeletion;
AllowAdminAccessToAllCollectionItems = org.AllowAdminAccessToAllCollectionItems; AllowAdminAccessToAllCollectionItems = org.AllowAdminAccessToAllCollectionItems;
//
if (subscriptionInfo?.Subscription == null) if (subscriptionInfo?.Subscription == null)
{ {
@ -138,8 +141,12 @@ public class OrganizationLicense : ILicense
public bool UseSecretsManager { get; set; } public bool UseSecretsManager { get; set; }
public int? SmSeats { get; set; } public int? SmSeats { get; set; }
public int? SmServiceAccounts { get; set; } public int? SmServiceAccounts { get; set; }
// Deprecated. Left for backwards compatibility with old license versions.
public bool LimitCollectionCreationDeletion { get; set; } = true; public bool LimitCollectionCreationDeletion { get; set; } = true;
public bool AllowAdminAccessToAllCollectionItems { 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; }
@ -150,7 +157,8 @@ public class OrganizationLicense : ILicense
/// Represents the current version of the license format. Should be updated whenever new fields are added. /// Represents the current version of the license format. Should be updated whenever new fields are added.
/// </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 = 14; public const int CurrentLicenseFileVersion = 14;
private bool ValidLicenseVersion private bool ValidLicenseVersion
{ {
@ -368,10 +376,11 @@ public class OrganizationLicense : ILicense
} }
/* /*
* Version 14 added LimitCollectionCreationDeletion and Version 15 added AllowAdminAccessToAllCollectionItems, * Version 14 added LimitCollectionCreationDeletion and Version
* however these are just user settings and it is not worth failing validation if they mismatch. * 15 added AllowAdminAccessToAllCollectionItems, however they
* They are intentionally excluded. * are no longer used and are intentionally excluded from
*/ * validation.
*/
return valid; return valid;
} }

View File

@ -37,4 +37,25 @@ public class InstallationDeviceEntity : ITableEntity
{ {
return deviceId != null && deviceId.Length == 73 && deviceId[36] == '_'; return deviceId != null && deviceId.Length == 73 && deviceId[36] == '_';
} }
public static bool TryParse(string deviceId, out InstallationDeviceEntity installationDeviceEntity)
{
installationDeviceEntity = null;
var installationId = Guid.Empty;
var deviceIdGuid = Guid.Empty;
if (!IsInstallationDeviceId(deviceId))
{
return false;
}
var parts = deviceId.Split("_");
if (parts.Length < 2)
{
return false;
}
if (!Guid.TryParse(parts[0], out installationId) || !Guid.TryParse(parts[1], out deviceIdGuid))
{
return false;
}
installationDeviceEntity = new InstallationDeviceEntity(installationId, deviceIdGuid);
return true;
}
} }

View File

@ -49,11 +49,11 @@ public class MarkNotificationDeletedCommand : IMarkNotificationDeletedCommand
if (notificationStatus == null) if (notificationStatus == null)
{ {
notificationStatus = new NotificationStatus() notificationStatus = new NotificationStatus
{ {
NotificationId = notificationId, NotificationId = notificationId,
UserId = _currentContext.UserId.Value, UserId = _currentContext.UserId.Value,
DeletedDate = DateTime.Now DeletedDate = DateTime.UtcNow
}; };
await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus, await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus,

View File

@ -49,11 +49,11 @@ public class MarkNotificationReadCommand : IMarkNotificationReadCommand
if (notificationStatus == null) if (notificationStatus == null)
{ {
notificationStatus = new NotificationStatus() notificationStatus = new NotificationStatus
{ {
NotificationId = notificationId, NotificationId = notificationId,
UserId = _currentContext.UserId.Value, UserId = _currentContext.UserId.Value,
ReadDate = DateTime.Now ReadDate = DateTime.UtcNow
}; };
await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus, await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus,

View File

@ -0,0 +1,8 @@
using Microsoft.Azure.NotificationHubs;
namespace Bit.Core.NotificationHub;
public interface INotificationHubProxy
{
Task<(INotificationHubClient Client, NotificationOutcome Outcome)[]> SendTemplateNotificationAsync(IDictionary<string, string> properties, string tagExpression);
}

View File

@ -0,0 +1,9 @@
using Microsoft.Azure.NotificationHubs;
namespace Bit.Core.NotificationHub;
public interface INotificationHubPool
{
NotificationHubClient ClientFor(Guid comb);
INotificationHubProxy AllClients { get; }
}

View File

@ -0,0 +1,26 @@
using Microsoft.Azure.NotificationHubs;
namespace Bit.Core.NotificationHub;
public class NotificationHubClientProxy : INotificationHubProxy
{
private readonly IEnumerable<INotificationHubClient> _clients;
public NotificationHubClientProxy(IEnumerable<INotificationHubClient> clients)
{
_clients = clients;
}
private async Task<(INotificationHubClient, T)[]> ApplyToAllClientsAsync<T>(Func<INotificationHubClient, Task<T>> action)
{
var tasks = _clients.Select(async c => (c, await action(c)));
return await Task.WhenAll(tasks);
}
// partial proxy of INotificationHubClient implementation
// Note: Any other methods that are needed can simply be delegated as done here.
public async Task<(INotificationHubClient Client, NotificationOutcome Outcome)[]> SendTemplateNotificationAsync(IDictionary<string, string> properties, string tagExpression)
{
return await ApplyToAllClientsAsync(async c => await c.SendTemplateNotificationAsync(properties, tagExpression));
}
}

View File

@ -0,0 +1,128 @@
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.Azure.NotificationHubs;
class NotificationHubConnection
{
public string HubName { get; init; }
public string ConnectionString { get; init; }
public bool EnableSendTracing { get; init; }
private NotificationHubClient _hubClient;
/// <summary>
/// Gets the NotificationHubClient for this connection.
///
/// If the client is null, it will be initialized.
///
/// <throws>Exception</throws> if the connection is invalid.
/// </summary>
public NotificationHubClient HubClient
{
get
{
if (_hubClient == null)
{
if (!IsValid)
{
throw new Exception("Invalid notification hub settings");
}
Init();
}
return _hubClient;
}
private set
{
_hubClient = value;
}
}
/// <summary>
/// Gets the start date for registration.
///
/// If null, registration is always disabled.
/// </summary>
public DateTime? RegistrationStartDate { get; init; }
/// <summary>
/// Gets the end date for registration.
///
/// If null, registration has no end date.
/// </summary>
public DateTime? RegistrationEndDate { get; init; }
/// <summary>
/// Gets whether all data needed to generate a connection to Notification Hub is present.
/// </summary>
public bool IsValid
{
get
{
{
var invalid = string.IsNullOrWhiteSpace(HubName) || string.IsNullOrWhiteSpace(ConnectionString);
return !invalid;
}
}
}
public string LogString
{
get
{
return $"HubName: {HubName}, EnableSendTracing: {EnableSendTracing}, RegistrationStartDate: {RegistrationStartDate}, RegistrationEndDate: {RegistrationEndDate}";
}
}
/// <summary>
/// Gets whether registration is enabled for the given comb ID.
/// This is based off of the generation time encoded in the comb ID.
/// </summary>
/// <param name="comb"></param>
/// <returns></returns>
public bool RegistrationEnabled(Guid comb)
{
var combTime = CoreHelpers.DateFromComb(comb);
return RegistrationEnabled(combTime);
}
/// <summary>
/// Gets whether registration is enabled for the given time.
/// </summary>
/// <param name="queryTime">The time to check</param>
/// <returns></returns>
public bool RegistrationEnabled(DateTime queryTime)
{
if (queryTime >= RegistrationEndDate || RegistrationStartDate == null)
{
return false;
}
return RegistrationStartDate < queryTime;
}
private NotificationHubConnection() { }
/// <summary>
/// Creates a new NotificationHubConnection from the given settings.
/// </summary>
/// <param name="settings"></param>
/// <returns></returns>
public static NotificationHubConnection From(GlobalSettings.NotificationHubSettings settings)
{
return new()
{
HubName = settings.HubName,
ConnectionString = settings.ConnectionString,
EnableSendTracing = settings.EnableSendTracing,
// Comb time is not precise enough for millisecond accuracy
RegistrationStartDate = settings.RegistrationStartDate.HasValue ? Truncate(settings.RegistrationStartDate.Value, TimeSpan.FromMilliseconds(10)) : null,
RegistrationEndDate = settings.RegistrationEndDate
};
}
private NotificationHubConnection Init()
{
HubClient = NotificationHubClient.CreateClientFromConnectionString(ConnectionString, HubName, EnableSendTracing);
return this;
}
private static DateTime Truncate(DateTime dateTime, TimeSpan resolution)
{
return dateTime.AddTicks(-(dateTime.Ticks % resolution.Ticks));
}
}

View File

@ -0,0 +1,62 @@
using Bit.Core.Settings;
using Bit.Core.Utilities;
using Microsoft.Azure.NotificationHubs;
using Microsoft.Extensions.Logging;
namespace Bit.Core.NotificationHub;
public class NotificationHubPool : INotificationHubPool
{
private List<NotificationHubConnection> _connections { get; }
private readonly IEnumerable<INotificationHubClient> _clients;
private readonly ILogger<NotificationHubPool> _logger;
public NotificationHubPool(ILogger<NotificationHubPool> logger, GlobalSettings globalSettings)
{
_logger = logger;
_connections = FilterInvalidHubs(globalSettings.NotificationHubPool.NotificationHubs);
_clients = _connections.GroupBy(c => c.ConnectionString).Select(g => g.First().HubClient);
}
private List<NotificationHubConnection> FilterInvalidHubs(IEnumerable<GlobalSettings.NotificationHubSettings> hubs)
{
List<NotificationHubConnection> result = new();
_logger.LogDebug("Filtering {HubCount} notification hubs", hubs.Count());
foreach (var hub in hubs)
{
var connection = NotificationHubConnection.From(hub);
if (!connection.IsValid)
{
_logger.LogWarning("Invalid notification hub settings: {HubName}", hub.HubName ?? "hub name missing");
continue;
}
_logger.LogDebug("Adding notification hub: {ConnectionLogString}", connection.LogString);
result.Add(connection);
}
return result;
}
/// <summary>
/// Gets the NotificationHubClient for the given comb ID.
/// </summary>
/// <param name="comb"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException">Thrown when no notification hub is found for a given comb.</exception>
public NotificationHubClient ClientFor(Guid comb)
{
var possibleConnections = _connections.Where(c => c.RegistrationEnabled(comb)).ToArray();
if (possibleConnections.Length == 0)
{
throw new InvalidOperationException($"No valid notification hubs are available for the given comb ({comb}).\n" +
$"The comb's datetime is {CoreHelpers.DateFromComb(comb)}." +
$"Hub start and end times are configured as follows:\n" +
string.Join("\n", _connections.Select(c => $"Hub {c.HubName} - Start: {c.RegistrationStartDate}, End: {c.RegistrationEndDate}")));
}
var resolvedConnection = possibleConnections[CoreHelpers.BinForComb(comb, possibleConnections.Length)];
_logger.LogTrace("Resolved notification hub for comb {Comb} out of {HubCount} hubs.\n{ConnectionInfo}", comb, possibleConnections.Length, resolvedConnection.LogString);
return resolvedConnection.HubClient;
}
public INotificationHubProxy AllClients { get { return new NotificationHubClientProxy(_clients); } }
}

View File

@ -6,45 +6,31 @@ using Bit.Core.Enums;
using Bit.Core.Models; using Bit.Core.Models;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Settings; using Bit.Core.Services;
using Bit.Core.Tools.Entities; using Bit.Core.Tools.Entities;
using Bit.Core.Vault.Entities; using Bit.Core.Vault.Entities;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.Azure.NotificationHubs;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Bit.Core.Services; namespace Bit.Core.NotificationHub;
public class NotificationHubPushNotificationService : IPushNotificationService public class NotificationHubPushNotificationService : IPushNotificationService
{ {
private readonly IInstallationDeviceRepository _installationDeviceRepository; private readonly IInstallationDeviceRepository _installationDeviceRepository;
private readonly GlobalSettings _globalSettings;
private readonly IHttpContextAccessor _httpContextAccessor; private readonly IHttpContextAccessor _httpContextAccessor;
private readonly List<NotificationHubClient> _clients = [];
private readonly bool _enableTracing = false; private readonly bool _enableTracing = false;
private readonly INotificationHubPool _notificationHubPool;
private readonly ILogger _logger; private readonly ILogger _logger;
public NotificationHubPushNotificationService( public NotificationHubPushNotificationService(
IInstallationDeviceRepository installationDeviceRepository, IInstallationDeviceRepository installationDeviceRepository,
GlobalSettings globalSettings, INotificationHubPool notificationHubPool,
IHttpContextAccessor httpContextAccessor, IHttpContextAccessor httpContextAccessor,
ILogger<NotificationsApiPushNotificationService> logger) ILogger<NotificationsApiPushNotificationService> logger)
{ {
_installationDeviceRepository = installationDeviceRepository; _installationDeviceRepository = installationDeviceRepository;
_globalSettings = globalSettings;
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
_notificationHubPool = notificationHubPool;
foreach (var hub in globalSettings.NotificationHubs)
{
var client = NotificationHubClient.CreateClientFromConnectionString(
hub.ConnectionString,
hub.HubName,
hub.EnableSendTracing);
_clients.Add(client);
_enableTracing = _enableTracing || hub.EnableSendTracing;
}
_logger = logger; _logger = logger;
} }
@ -264,30 +250,23 @@ public class NotificationHubPushNotificationService : IPushNotificationService
private async Task SendPayloadAsync(string tag, PushType type, object payload) private async Task SendPayloadAsync(string tag, PushType type, object payload)
{ {
var tasks = new List<Task<NotificationOutcome>>(); var results = await _notificationHubPool.AllClients.SendTemplateNotificationAsync(
foreach (var client in _clients) new Dictionary<string, string>
{ {
var task = client.SendTemplateNotificationAsync( { "type", ((byte)type).ToString() },
new Dictionary<string, string> { "payload", JsonSerializer.Serialize(payload) }
{ }, tag);
{ "type", ((byte)type).ToString() },
{ "payload", JsonSerializer.Serialize(payload) }
}, tag);
tasks.Add(task);
}
await Task.WhenAll(tasks);
if (_enableTracing) if (_enableTracing)
{ {
for (var i = 0; i < tasks.Count; i++) foreach (var (client, outcome) in results)
{ {
if (_clients[i].EnableTestSend) if (!client.EnableTestSend)
{ {
var outcome = await tasks[i]; continue;
_logger.LogInformation("Azure Notification Hub Tracking ID: {id} | {type} push notification with {success} successes and {failure} failures with a payload of {@payload} and result of {@results}",
outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results);
} }
_logger.LogInformation("Azure Notification Hub Tracking ID: {Id} | {Type} push notification with {Success} successes and {Failure} failures with a payload of {@Payload} and result of {@Results}",
outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results);
} }
} }
} }

View File

@ -1,50 +1,34 @@
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Models.Data; using Bit.Core.Models.Data;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Microsoft.Azure.NotificationHubs; using Microsoft.Azure.NotificationHubs;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Bit.Core.Services; namespace Bit.Core.NotificationHub;
public class NotificationHubPushRegistrationService : IPushRegistrationService public class NotificationHubPushRegistrationService : IPushRegistrationService
{ {
private readonly IInstallationDeviceRepository _installationDeviceRepository; private readonly IInstallationDeviceRepository _installationDeviceRepository;
private readonly GlobalSettings _globalSettings; private readonly GlobalSettings _globalSettings;
private readonly INotificationHubPool _notificationHubPool;
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly ILogger<NotificationHubPushRegistrationService> _logger; private readonly ILogger<NotificationHubPushRegistrationService> _logger;
private Dictionary<NotificationHubType, NotificationHubClient> _clients = [];
public NotificationHubPushRegistrationService( public NotificationHubPushRegistrationService(
IInstallationDeviceRepository installationDeviceRepository, IInstallationDeviceRepository installationDeviceRepository,
GlobalSettings globalSettings, GlobalSettings globalSettings,
INotificationHubPool notificationHubPool,
IServiceProvider serviceProvider, IServiceProvider serviceProvider,
ILogger<NotificationHubPushRegistrationService> logger) ILogger<NotificationHubPushRegistrationService> logger)
{ {
_installationDeviceRepository = installationDeviceRepository; _installationDeviceRepository = installationDeviceRepository;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_notificationHubPool = notificationHubPool;
_serviceProvider = serviceProvider; _serviceProvider = serviceProvider;
_logger = logger; _logger = logger;
// Is this dirty to do in the ctor?
void addHub(NotificationHubType type)
{
var hubRegistration = globalSettings.NotificationHubs.FirstOrDefault(
h => h.HubType == type && h.EnableRegistration);
if (hubRegistration != null)
{
var client = NotificationHubClient.CreateClientFromConnectionString(
hubRegistration.ConnectionString,
hubRegistration.HubName,
hubRegistration.EnableSendTracing);
_clients.Add(type, client);
}
}
addHub(NotificationHubType.General);
addHub(NotificationHubType.iOS);
addHub(NotificationHubType.Android);
} }
public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
@ -117,7 +101,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate, BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate,
userId, identifier); userId, identifier);
await GetClient(type).CreateOrUpdateInstallationAsync(installation); await ClientFor(GetComb(deviceId)).CreateOrUpdateInstallationAsync(installation);
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId)) if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
{ {
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId)); await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
@ -152,11 +136,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
installation.Templates.Add(fullTemplateId, template); installation.Templates.Add(fullTemplateId, template);
} }
public async Task DeleteRegistrationAsync(string deviceId, DeviceType deviceType) public async Task DeleteRegistrationAsync(string deviceId)
{ {
try try
{ {
await GetClient(deviceType).DeleteInstallationAsync(deviceId); await ClientFor(GetComb(deviceId)).DeleteInstallationAsync(deviceId);
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId)) if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
{ {
await _installationDeviceRepository.DeleteAsync(new InstallationDeviceEntity(deviceId)); await _installationDeviceRepository.DeleteAsync(new InstallationDeviceEntity(deviceId));
@ -168,31 +152,31 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
} }
} }
public async Task AddUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId) public async Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
{ {
await PatchTagsForUserDevicesAsync(devices, UpdateOperationType.Add, $"organizationId:{organizationId}"); await PatchTagsForUserDevicesAsync(deviceIds, UpdateOperationType.Add, $"organizationId:{organizationId}");
if (devices.Any() && InstallationDeviceEntity.IsInstallationDeviceId(devices.First().Key)) if (deviceIds.Any() && InstallationDeviceEntity.IsInstallationDeviceId(deviceIds.First()))
{ {
var entities = devices.Select(e => new InstallationDeviceEntity(e.Key)); var entities = deviceIds.Select(e => new InstallationDeviceEntity(e));
await _installationDeviceRepository.UpsertManyAsync(entities.ToList()); await _installationDeviceRepository.UpsertManyAsync(entities.ToList());
} }
} }
public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId) public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
{ {
await PatchTagsForUserDevicesAsync(devices, UpdateOperationType.Remove, await PatchTagsForUserDevicesAsync(deviceIds, UpdateOperationType.Remove,
$"organizationId:{organizationId}"); $"organizationId:{organizationId}");
if (devices.Any() && InstallationDeviceEntity.IsInstallationDeviceId(devices.First().Key)) if (deviceIds.Any() && InstallationDeviceEntity.IsInstallationDeviceId(deviceIds.First()))
{ {
var entities = devices.Select(e => new InstallationDeviceEntity(e.Key)); var entities = deviceIds.Select(e => new InstallationDeviceEntity(e));
await _installationDeviceRepository.UpsertManyAsync(entities.ToList()); await _installationDeviceRepository.UpsertManyAsync(entities.ToList());
} }
} }
private async Task PatchTagsForUserDevicesAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, UpdateOperationType op, private async Task PatchTagsForUserDevicesAsync(IEnumerable<string> deviceIds, UpdateOperationType op,
string tag) string tag)
{ {
if (!devices.Any()) if (!deviceIds.Any())
{ {
return; return;
} }
@ -212,11 +196,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
operation.Path += $"/{tag}"; operation.Path += $"/{tag}";
} }
foreach (var device in devices) foreach (var deviceId in deviceIds)
{ {
try try
{ {
await GetClient(device.Value).PatchInstallationAsync(device.Key, new List<PartialUpdateOperation> { operation }); await ClientFor(GetComb(deviceId)).PatchInstallationAsync(deviceId, new List<PartialUpdateOperation> { operation });
} }
catch (Exception e) when (e.InnerException == null || !e.InnerException.Message.Contains("(404) Not Found")) catch (Exception e) when (e.InnerException == null || !e.InnerException.Message.Contains("(404) Not Found"))
{ {
@ -225,53 +209,29 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
} }
} }
private NotificationHubClient GetClient(DeviceType deviceType) private NotificationHubClient ClientFor(Guid deviceId)
{ {
var hubType = NotificationHubType.General; return _notificationHubPool.ClientFor(deviceId);
switch (deviceType) }
private Guid GetComb(string deviceId)
{
var deviceIdString = deviceId;
InstallationDeviceEntity installationDeviceEntity;
Guid deviceIdGuid;
if (InstallationDeviceEntity.TryParse(deviceIdString, out installationDeviceEntity))
{ {
case DeviceType.Android: // Strip off the installation id (PartitionId). RowKey is the ID in the Installation's table.
hubType = NotificationHubType.Android; deviceIdString = installationDeviceEntity.RowKey;
break;
case DeviceType.iOS:
hubType = NotificationHubType.iOS;
break;
case DeviceType.ChromeExtension:
case DeviceType.FirefoxExtension:
case DeviceType.OperaExtension:
case DeviceType.EdgeExtension:
case DeviceType.VivaldiExtension:
case DeviceType.SafariExtension:
hubType = NotificationHubType.GeneralBrowserExtension;
break;
case DeviceType.WindowsDesktop:
case DeviceType.MacOsDesktop:
case DeviceType.LinuxDesktop:
hubType = NotificationHubType.GeneralDesktop;
break;
case DeviceType.ChromeBrowser:
case DeviceType.FirefoxBrowser:
case DeviceType.OperaBrowser:
case DeviceType.EdgeBrowser:
case DeviceType.IEBrowser:
case DeviceType.UnknownBrowser:
case DeviceType.SafariBrowser:
case DeviceType.VivaldiBrowser:
hubType = NotificationHubType.GeneralWeb;
break;
default:
break;
} }
if (!_clients.ContainsKey(hubType)) if (Guid.TryParse(deviceIdString, out deviceIdGuid))
{ {
_logger.LogWarning("No hub client for '{0}'. Using general hub instead.", hubType);
hubType = NotificationHubType.General;
if (!_clients.ContainsKey(hubType))
{
throw new Exception("No general hub client found.");
}
} }
return _clients[hubType]; else
{
throw new Exception($"Invalid device id {deviceId}.");
}
return deviceIdGuid;
} }
} }

View File

@ -17,15 +17,18 @@ public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseComman
private readonly ILicensingService _licensingService; private readonly ILicensingService _licensingService;
private readonly IGlobalSettings _globalSettings; private readonly IGlobalSettings _globalSettings;
private readonly IOrganizationService _organizationService; private readonly IOrganizationService _organizationService;
private readonly IFeatureService _featureService;
public UpdateOrganizationLicenseCommand( public UpdateOrganizationLicenseCommand(
ILicensingService licensingService, ILicensingService licensingService,
IGlobalSettings globalSettings, IGlobalSettings globalSettings,
IOrganizationService organizationService) IOrganizationService organizationService,
IFeatureService featureService)
{ {
_licensingService = licensingService; _licensingService = licensingService;
_globalSettings = globalSettings; _globalSettings = globalSettings;
_organizationService = organizationService; _organizationService = organizationService;
_featureService = featureService;
} }
public async Task UpdateLicenseAsync(SelfHostedOrganizationDetails selfHostedOrganization, public async Task UpdateLicenseAsync(SelfHostedOrganizationDetails selfHostedOrganization,
@ -59,7 +62,8 @@ public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseComman
private async Task UpdateOrganizationAsync(SelfHostedOrganizationDetails selfHostedOrganizationDetails, OrganizationLicense license) private async Task UpdateOrganizationAsync(SelfHostedOrganizationDetails selfHostedOrganizationDetails, OrganizationLicense license)
{ {
var organization = selfHostedOrganizationDetails.ToOrganization(); var organization = selfHostedOrganizationDetails.ToOrganization();
organization.UpdateFromLicense(license);
organization.UpdateFromLicense(license, _featureService);
await _organizationService.ReplaceAndUpdateCacheAsync(organization); await _organizationService.ReplaceAndUpdateCacheAsync(organization);
} }

View File

@ -6,7 +6,7 @@ public interface IPushRegistrationService
{ {
Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId, Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
string identifier, DeviceType type); string identifier, DeviceType type);
Task DeleteRegistrationAsync(string deviceId, DeviceType type); Task DeleteRegistrationAsync(string deviceId);
Task AddUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId); Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId);
Task DeleteUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId); Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId);
} }

View File

@ -10,6 +10,8 @@ public interface IStripeAdapter
Task<Stripe.Customer> CustomerUpdateAsync(string id, Stripe.CustomerUpdateOptions options = null); Task<Stripe.Customer> CustomerUpdateAsync(string id, Stripe.CustomerUpdateOptions options = null);
Task<Stripe.Customer> CustomerDeleteAsync(string id); Task<Stripe.Customer> CustomerDeleteAsync(string id);
Task<List<PaymentMethod>> CustomerListPaymentMethods(string id, CustomerListPaymentMethodsOptions options = null); Task<List<PaymentMethod>> CustomerListPaymentMethods(string id, CustomerListPaymentMethodsOptions options = null);
Task<CustomerBalanceTransaction> CustomerBalanceTransactionCreate(string customerId,
CustomerBalanceTransactionCreateOptions options);
Task<Stripe.Subscription> SubscriptionCreateAsync(Stripe.SubscriptionCreateOptions subscriptionCreateOptions); Task<Stripe.Subscription> SubscriptionCreateAsync(Stripe.SubscriptionCreateOptions subscriptionCreateOptions);
Task<Stripe.Subscription> SubscriptionGetAsync(string id, Stripe.SubscriptionGetOptions options = null); Task<Stripe.Subscription> SubscriptionGetAsync(string id, Stripe.SubscriptionGetOptions options = null);
Task<List<Stripe.Subscription>> SubscriptionListAsync(StripeSubscriptionListOptions subscriptionSearchOptions); Task<List<Stripe.Subscription>> SubscriptionListAsync(StripeSubscriptionListOptions subscriptionSearchOptions);

View File

@ -38,13 +38,13 @@ public class DeviceService : IDeviceService
public async Task ClearTokenAsync(Device device) public async Task ClearTokenAsync(Device device)
{ {
await _deviceRepository.ClearPushTokenAsync(device.Id); await _deviceRepository.ClearPushTokenAsync(device.Id);
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString(), device.Type); await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
} }
public async Task DeleteAsync(Device device) public async Task DeleteAsync(Device device)
{ {
await _deviceRepository.DeleteAsync(device); await _deviceRepository.DeleteAsync(device);
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString(), device.Type); await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
} }
public async Task UpdateDevicesTrustAsync(string currentDeviceIdentifier, public async Task UpdateDevicesTrustAsync(string currentDeviceIdentifier,

View File

@ -1,61 +1,31 @@
using Bit.Core.Auth.Entities; using Bit.Core.Auth.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Settings; using Bit.Core.Settings;
using Bit.Core.Tools.Entities; using Bit.Core.Tools.Entities;
using Bit.Core.Utilities;
using Bit.Core.Vault.Entities; using Bit.Core.Vault.Entities;
using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
namespace Bit.Core.Services; namespace Bit.Core.Services;
public class MultiServicePushNotificationService : IPushNotificationService public class MultiServicePushNotificationService : IPushNotificationService
{ {
private readonly List<IPushNotificationService> _services = new List<IPushNotificationService>(); private readonly IEnumerable<IPushNotificationService> _services;
private readonly ILogger<MultiServicePushNotificationService> _logger; private readonly ILogger<MultiServicePushNotificationService> _logger;
public MultiServicePushNotificationService( public MultiServicePushNotificationService(
IHttpClientFactory httpFactory, [FromKeyedServices("implementation")] IEnumerable<IPushNotificationService> services,
IDeviceRepository deviceRepository,
IInstallationDeviceRepository installationDeviceRepository,
GlobalSettings globalSettings,
IHttpContextAccessor httpContextAccessor,
ILogger<MultiServicePushNotificationService> logger, ILogger<MultiServicePushNotificationService> logger,
ILogger<RelayPushNotificationService> relayLogger, GlobalSettings globalSettings)
ILogger<NotificationsApiPushNotificationService> hubLogger)
{ {
if (globalSettings.SelfHosted) _services = services;
{
if (CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) &&
globalSettings.Installation?.Id != null &&
CoreHelpers.SettingHasValue(globalSettings.Installation?.Key))
{
_services.Add(new RelayPushNotificationService(httpFactory, deviceRepository, globalSettings,
httpContextAccessor, relayLogger));
}
if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) &&
CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications))
{
_services.Add(new NotificationsApiPushNotificationService(
httpFactory, globalSettings, httpContextAccessor, hubLogger));
}
}
else
{
var generalHub = globalSettings.NotificationHubs?.FirstOrDefault(h => h.HubType == NotificationHubType.General);
if (CoreHelpers.SettingHasValue(generalHub?.ConnectionString))
{
_services.Add(new NotificationHubPushNotificationService(installationDeviceRepository,
globalSettings, httpContextAccessor, hubLogger));
}
if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString))
{
_services.Add(new AzureQueuePushNotificationService(globalSettings, httpContextAccessor));
}
}
_logger = logger; _logger = logger;
_logger.LogInformation("Hub services: {Services}", _services.Count());
globalSettings?.NotificationHubPool?.NotificationHubs?.ForEach(hub =>
{
_logger.LogInformation("HubName: {HubName}, EnableSendTracing: {EnableSendTracing}, RegistrationStartDate: {RegistrationStartDate}, RegistrationEndDate: {RegistrationEndDate}", hub.HubName, hub.EnableSendTracing, hub.RegistrationStartDate, hub.RegistrationEndDate);
});
} }
public Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds) public Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)

View File

@ -38,37 +38,36 @@ public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegi
await SendAsync(HttpMethod.Post, "push/register", requestModel); await SendAsync(HttpMethod.Post, "push/register", requestModel);
} }
public async Task DeleteRegistrationAsync(string deviceId, DeviceType type) public async Task DeleteRegistrationAsync(string deviceId)
{ {
var requestModel = new PushDeviceRequestModel var requestModel = new PushDeviceRequestModel
{ {
Id = deviceId, Id = deviceId,
Type = type,
}; };
await SendAsync(HttpMethod.Post, "push/delete", requestModel); await SendAsync(HttpMethod.Post, "push/delete", requestModel);
} }
public async Task AddUserRegistrationOrganizationAsync( public async Task AddUserRegistrationOrganizationAsync(
IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId) IEnumerable<string> deviceIds, string organizationId)
{ {
if (!devices.Any()) if (!deviceIds.Any())
{ {
return; return;
} }
var requestModel = new PushUpdateRequestModel(devices, organizationId); var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);
await SendAsync(HttpMethod.Put, "push/add-organization", requestModel); await SendAsync(HttpMethod.Put, "push/add-organization", requestModel);
} }
public async Task DeleteUserRegistrationOrganizationAsync( public async Task DeleteUserRegistrationOrganizationAsync(
IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId) IEnumerable<string> deviceIds, string organizationId)
{ {
if (!devices.Any()) if (!deviceIds.Any())
{ {
return; return;
} }
var requestModel = new PushUpdateRequestModel(devices, organizationId); var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);
await SendAsync(HttpMethod.Put, "push/delete-organization", requestModel); await SendAsync(HttpMethod.Put, "push/delete-organization", requestModel);
} }
} }

View File

@ -18,6 +18,7 @@ public class StripeAdapter : IStripeAdapter
private readonly Stripe.PriceService _priceService; private readonly Stripe.PriceService _priceService;
private readonly Stripe.SetupIntentService _setupIntentService; private readonly Stripe.SetupIntentService _setupIntentService;
private readonly Stripe.TestHelpers.TestClockService _testClockService; private readonly Stripe.TestHelpers.TestClockService _testClockService;
private readonly CustomerBalanceTransactionService _customerBalanceTransactionService;
public StripeAdapter() public StripeAdapter()
{ {
@ -34,6 +35,7 @@ public class StripeAdapter : IStripeAdapter
_priceService = new Stripe.PriceService(); _priceService = new Stripe.PriceService();
_setupIntentService = new SetupIntentService(); _setupIntentService = new SetupIntentService();
_testClockService = new Stripe.TestHelpers.TestClockService(); _testClockService = new Stripe.TestHelpers.TestClockService();
_customerBalanceTransactionService = new CustomerBalanceTransactionService();
} }
public Task<Stripe.Customer> CustomerCreateAsync(Stripe.CustomerCreateOptions options) public Task<Stripe.Customer> CustomerCreateAsync(Stripe.CustomerCreateOptions options)
@ -63,6 +65,10 @@ public class StripeAdapter : IStripeAdapter
return paymentMethods.Data; return paymentMethods.Data;
} }
public async Task<CustomerBalanceTransaction> CustomerBalanceTransactionCreate(string customerId,
CustomerBalanceTransactionCreateOptions options)
=> await _customerBalanceTransactionService.CreateAsync(customerId, options);
public Task<Stripe.Subscription> SubscriptionCreateAsync(Stripe.SubscriptionCreateOptions options) public Task<Stripe.Subscription> SubscriptionCreateAsync(Stripe.SubscriptionCreateOptions options)
{ {
return _subscriptionService.CreateAsync(options); return _subscriptionService.CreateAsync(options);

View File

@ -4,7 +4,7 @@ namespace Bit.Core.Services;
public class NoopPushRegistrationService : IPushRegistrationService public class NoopPushRegistrationService : IPushRegistrationService
{ {
public Task AddUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId) public Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
{ {
return Task.FromResult(0); return Task.FromResult(0);
} }
@ -15,12 +15,12 @@ public class NoopPushRegistrationService : IPushRegistrationService
return Task.FromResult(0); return Task.FromResult(0);
} }
public Task DeleteRegistrationAsync(string deviceId, DeviceType deviceType) public Task DeleteRegistrationAsync(string deviceId)
{ {
return Task.FromResult(0); return Task.FromResult(0);
} }
public Task DeleteUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId) public Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
{ {
return Task.FromResult(0); return Task.FromResult(0);
} }

View File

@ -1,5 +1,4 @@
using Bit.Core.Auth.Settings; using Bit.Core.Auth.Settings;
using Bit.Core.Enums;
using Bit.Core.Settings.LoggingSettings; using Bit.Core.Settings.LoggingSettings;
namespace Bit.Core.Settings; namespace Bit.Core.Settings;
@ -65,7 +64,7 @@ public class GlobalSettings : IGlobalSettings
public virtual SentrySettings Sentry { get; set; } = new SentrySettings(); public virtual SentrySettings Sentry { get; set; } = new SentrySettings();
public virtual SyslogSettings Syslog { get; set; } = new SyslogSettings(); public virtual SyslogSettings Syslog { get; set; } = new SyslogSettings();
public virtual ILogLevelSettings MinLogLevel { get; set; } = new LogLevelSettings(); public virtual ILogLevelSettings MinLogLevel { get; set; } = new LogLevelSettings();
public virtual List<NotificationHubSettings> NotificationHubs { get; set; } = new(); public virtual NotificationHubPoolSettings NotificationHubPool { get; set; } = new();
public virtual YubicoSettings Yubico { get; set; } = new YubicoSettings(); public virtual YubicoSettings Yubico { get; set; } = new YubicoSettings();
public virtual DuoSettings Duo { get; set; } = new DuoSettings(); public virtual DuoSettings Duo { get; set; } = new DuoSettings();
public virtual BraintreeSettings Braintree { get; set; } = new BraintreeSettings(); public virtual BraintreeSettings Braintree { get; set; } = new BraintreeSettings();
@ -424,7 +423,7 @@ public class GlobalSettings : IGlobalSettings
public string ConnectionString public string ConnectionString
{ {
get => _connectionString; get => _connectionString;
set => _connectionString = value.Trim('"'); set => _connectionString = value?.Trim('"');
} }
public string HubName { get; set; } public string HubName { get; set; }
/// <summary> /// <summary>
@ -433,10 +432,32 @@ public class GlobalSettings : IGlobalSettings
/// </summary> /// </summary>
public bool EnableSendTracing { get; set; } = false; public bool EnableSendTracing { get; set; } = false;
/// <summary> /// <summary>
/// At least one hub configuration should have registration enabled, preferably the General hub as a safety net. /// The date and time at which registration will be enabled.
///
/// **This value should not be updated once set, as it is used to determine installation location of devices.**
///
/// If null, registration is disabled.
///
/// </summary> /// </summary>
public bool EnableRegistration { get; set; } public DateTime? RegistrationStartDate { get; set; }
public NotificationHubType HubType { get; set; } /// <summary>
/// The date and time at which registration will be disabled.
///
/// **This value should not be updated once set, as it is used to determine installation location of devices.**
///
/// If null, hub registration has no yet known expiry.
/// </summary>
public DateTime? RegistrationEndDate { get; set; }
}
public class NotificationHubPoolSettings
{
/// <summary>
/// List of Notification Hub settings to use for sending push notifications.
///
/// Note that hubs on the same namespace share active device limits, so multiple namespaces should be used to increase capacity.
/// </summary>
public List<NotificationHubSettings> NotificationHubs { get; set; } = new();
} }
public class YubicoSettings public class YubicoSettings

View File

@ -76,6 +76,39 @@ public static class CoreHelpers
return new Guid(guidArray); return new Guid(guidArray);
} }
internal static DateTime DateFromComb(Guid combGuid)
{
var guidArray = combGuid.ToByteArray();
var daysArray = new byte[4];
var msecsArray = new byte[4];
Array.Copy(guidArray, guidArray.Length - 6, daysArray, 2, 2);
Array.Copy(guidArray, guidArray.Length - 4, msecsArray, 0, 4);
Array.Reverse(daysArray);
Array.Reverse(msecsArray);
var days = BitConverter.ToInt32(daysArray, 0);
var msecs = BitConverter.ToInt32(msecsArray, 0);
var time = TimeSpan.FromDays(days) + TimeSpan.FromMilliseconds(msecs * 3.333333);
return new DateTime(_baseDateTicks + time.Ticks, DateTimeKind.Utc);
}
internal static long BinForComb(Guid combGuid, int binCount)
{
// From System.Web.Util.HashCodeCombiner
uint CombineHashCodes(uint h1, byte h2)
{
return (uint)(((h1 << 5) + h1) ^ h2);
}
var guidArray = combGuid.ToByteArray();
var randomArray = new byte[10];
Array.Copy(guidArray, 0, randomArray, 0, 10);
var hash = randomArray.Aggregate((uint)randomArray.Length, CombineHashCodes);
return hash % binCount;
}
public static string CleanCertificateThumbprint(string thumbprint) public static string CleanCertificateThumbprint(string thumbprint)
{ {
// Clean possible garbage characters from thumbprint copy/paste // Clean possible garbage characters from thumbprint copy/paste

View File

@ -9,10 +9,6 @@ namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models;
public class Organization : Core.AdminConsole.Entities.Organization public class Organization : Core.AdminConsole.Entities.Organization
{ {
// Shadow properties - to be introduced by https://bitwarden.atlassian.net/browse/PM-10863
public bool LimitCollectionCreation { get => LimitCollectionCreationDeletion; set => LimitCollectionCreationDeletion = value; }
public bool LimitCollectionDeletion { get => LimitCollectionCreationDeletion; set => LimitCollectionCreationDeletion = value; }
public virtual ICollection<Cipher> Ciphers { get; set; } public virtual ICollection<Cipher> Ciphers { get; set; }
public virtual ICollection<OrganizationUser> OrganizationUsers { get; set; } public virtual ICollection<OrganizationUser> OrganizationUsers { get; set; }
public virtual ICollection<Group> Groups { get; set; } public virtual ICollection<Group> Groups { get; set; }
@ -42,9 +38,6 @@ public class OrganizationMapperProfile : Profile
.ForMember(org => org.ApiKeys, opt => opt.Ignore()) .ForMember(org => org.ApiKeys, opt => opt.Ignore())
.ForMember(org => org.Connections, opt => opt.Ignore()) .ForMember(org => org.Connections, opt => opt.Ignore())
.ForMember(org => org.Domains, opt => opt.Ignore()) .ForMember(org => org.Domains, opt => opt.Ignore())
// Shadow properties - to be introduced by https://bitwarden.atlassian.net/browse/PM-10863
.ForMember(org => org.LimitCollectionCreation, opt => opt.Ignore())
.ForMember(org => org.LimitCollectionDeletion, opt => opt.Ignore())
.ReverseMap(); .ReverseMap();
CreateProjection<Organization, SelfHostedOrganizationDetails>() CreateProjection<Organization, SelfHostedOrganizationDetails>()

View File

@ -99,6 +99,9 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
UseScim = e.UseScim, UseScim = e.UseScim,
UseCustomPermissions = e.UseCustomPermissions, UseCustomPermissions = e.UseCustomPermissions,
UsePolicies = e.UsePolicies, UsePolicies = e.UsePolicies,
LimitCollectionCreation = e.LimitCollectionCreation,
LimitCollectionDeletion = e.LimitCollectionDeletion,
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
LimitCollectionCreationDeletion = e.LimitCollectionCreationDeletion, LimitCollectionCreationDeletion = e.LimitCollectionCreationDeletion,
AllowAdminAccessToAllCollectionItems = e.AllowAdminAccessToAllCollectionItems AllowAdminAccessToAllCollectionItems = e.AllowAdminAccessToAllCollectionItems
}).ToListAsync(); }).ToListAsync();

View File

@ -66,6 +66,9 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery<OrganizationU
UsePasswordManager = o.UsePasswordManager, UsePasswordManager = o.UsePasswordManager,
SmSeats = o.SmSeats, SmSeats = o.SmSeats,
SmServiceAccounts = o.SmServiceAccounts, SmServiceAccounts = o.SmServiceAccounts,
LimitCollectionCreation = o.LimitCollectionCreation,
LimitCollectionDeletion = o.LimitCollectionDeletion,
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
LimitCollectionCreationDeletion = o.LimitCollectionCreationDeletion, LimitCollectionCreationDeletion = o.LimitCollectionCreationDeletion,
AllowAdminAccessToAllCollectionItems = o.AllowAdminAccessToAllCollectionItems, AllowAdminAccessToAllCollectionItems = o.AllowAdminAccessToAllCollectionItems,
}; };

View File

@ -44,6 +44,9 @@ public class ProviderUserOrganizationDetailsViewQuery : IQuery<ProviderUserOrgan
ProviderId = x.p.Id, ProviderId = x.p.Id,
ProviderName = x.p.Name, ProviderName = x.p.Name,
PlanType = x.o.PlanType, PlanType = x.o.PlanType,
LimitCollectionCreation = x.o.LimitCollectionCreation,
LimitCollectionDeletion = x.o.LimitCollectionDeletion,
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
LimitCollectionCreationDeletion = x.o.LimitCollectionCreationDeletion, LimitCollectionCreationDeletion = x.o.LimitCollectionCreationDeletion,
AllowAdminAccessToAllCollectionItems = x.o.AllowAdminAccessToAllCollectionItems, AllowAdminAccessToAllCollectionItems = x.o.AllowAdminAccessToAllCollectionItems,
}); });

View File

@ -4,6 +4,7 @@ using System.Security.Claims;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using AspNetCoreRateLimit; using AspNetCoreRateLimit;
using Bit.Core.AdminConsole.Models.Business.Tokenables; using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.Services; using Bit.Core.AdminConsole.Services;
using Bit.Core.AdminConsole.Services.Implementations; using Bit.Core.AdminConsole.Services.Implementations;
using Bit.Core.AdminConsole.Services.NoopImplementations; using Bit.Core.AdminConsole.Services.NoopImplementations;
@ -24,6 +25,7 @@ using Bit.Core.Enums;
using Bit.Core.HostedServices; using Bit.Core.HostedServices;
using Bit.Core.Identity; using Bit.Core.Identity;
using Bit.Core.IdentityServer; using Bit.Core.IdentityServer;
using Bit.Core.NotificationHub;
using Bit.Core.OrganizationFeatures; using Bit.Core.OrganizationFeatures;
using Bit.Core.Repositories; using Bit.Core.Repositories;
using Bit.Core.Resources; using Bit.Core.Resources;
@ -102,9 +104,9 @@ public static class ServiceCollectionExtensions
services.AddUserServices(globalSettings); services.AddUserServices(globalSettings);
services.AddTrialInitiationServices(); services.AddTrialInitiationServices();
services.AddOrganizationServices(globalSettings); services.AddOrganizationServices(globalSettings);
services.AddPolicyServices();
services.AddScoped<ICollectionService, CollectionService>(); services.AddScoped<ICollectionService, CollectionService>();
services.AddScoped<IGroupService, GroupService>(); services.AddScoped<IGroupService, GroupService>();
services.AddScoped<IPolicyService, PolicyService>();
services.AddScoped<IEventService, EventService>(); services.AddScoped<IEventService, EventService>();
services.AddScoped<IEmergencyAccessService, EmergencyAccessService>(); services.AddScoped<IEmergencyAccessService, EmergencyAccessService>();
services.AddSingleton<IDeviceService, DeviceService>(); services.AddSingleton<IDeviceService, DeviceService>();
@ -263,16 +265,30 @@ public static class ServiceCollectionExtensions
} }
services.AddSingleton<IPushNotificationService, MultiServicePushNotificationService>(); services.AddSingleton<IPushNotificationService, MultiServicePushNotificationService>();
if (globalSettings.SelfHosted && if (globalSettings.SelfHosted)
CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) &&
globalSettings.Installation?.Id != null &&
CoreHelpers.SettingHasValue(globalSettings.Installation?.Key))
{ {
services.AddSingleton<IPushRegistrationService, RelayPushRegistrationService>(); if (CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) &&
globalSettings.Installation?.Id != null &&
CoreHelpers.SettingHasValue(globalSettings.Installation?.Key))
{
services.AddKeyedSingleton<IPushNotificationService, RelayPushNotificationService>("implementation");
services.AddSingleton<IPushRegistrationService, RelayPushRegistrationService>();
}
if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) &&
CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications))
{
services.AddKeyedSingleton<IPushNotificationService, NotificationsApiPushNotificationService>("implementation");
}
} }
else if (!globalSettings.SelfHosted) else if (!globalSettings.SelfHosted)
{ {
services.AddSingleton<INotificationHubPool, NotificationHubPool>();
services.AddSingleton<IPushRegistrationService, NotificationHubPushRegistrationService>(); services.AddSingleton<IPushRegistrationService, NotificationHubPushRegistrationService>();
services.AddKeyedSingleton<IPushNotificationService, NotificationHubPushNotificationService>("implementation");
if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString))
{
services.AddKeyedSingleton<IPushNotificationService, AzureQueuePushNotificationService>("implementation");
}
} }
else else
{ {

View File

@ -12,18 +12,28 @@ BEGIN
[dbo].[Collection] S ON S.[Id] = CC.[CollectionId] [dbo].[Collection] S ON S.[Id] = CC.[CollectionId]
INNER JOIN INNER JOIN
[dbo].[OrganizationUser] OU ON OU.[OrganizationId] = S.[OrganizationId] AND OU.[UserId] = @UserId [dbo].[OrganizationUser] OU ON OU.[OrganizationId] = S.[OrganizationId] AND OU.[UserId] = @UserId
INNER JOIN
[dbo].[CollectionUser] CU ON CU.[CollectionId] = S.[Id] AND CU.[OrganizationUserId] = OU.[Id]
WHERE
OU.[Status] = 2
UNION ALL
SELECT
CC.*
FROM
[dbo].[CollectionCipher] CC
INNER JOIN
[dbo].[Collection] S ON S.[Id] = CC.[CollectionId]
INNER JOIN
[dbo].[OrganizationUser] OU ON OU.[OrganizationId] = S.[OrganizationId] AND OU.[UserId] = @UserId
INNER JOIN
[dbo].[GroupUser] GU ON GU.[OrganizationUserId] = OU.[Id]
INNER JOIN
[dbo].[CollectionGroup] CG ON CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId]
LEFT JOIN LEFT JOIN
[dbo].[CollectionUser] CU ON CU.[CollectionId] = S.[Id] AND CU.[OrganizationUserId] = OU.[Id] [dbo].[CollectionUser] CU ON CU.[CollectionId] = S.[Id] 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 WHERE
OU.[Status] = 2 -- Confirmed OU.[Status] = 2
AND ( AND CU.[CollectionId] IS NULL
CU.[CollectionId] IS NOT NULL
OR CG.[CollectionId] IS NOT NULL
)
END END

View File

@ -229,13 +229,13 @@ public class OrganizationDomainControllerTests
sutProvider.GetDependency<IOrganizationDomainRepository>() sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId) .GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId)
.Returns(organizationDomain); .Returns(organizationDomain);
sutProvider.GetDependency<IVerifyOrganizationDomainCommand>().VerifyOrganizationDomainAsync(organizationDomain) sutProvider.GetDependency<IVerifyOrganizationDomainCommand>().UserVerifyOrganizationDomainAsync(organizationDomain)
.Returns(new OrganizationDomain()); .Returns(new OrganizationDomain());
var result = await sutProvider.Sut.Verify(organizationDomain.OrganizationId, organizationDomain.Id); var result = await sutProvider.Sut.Verify(organizationDomain.OrganizationId, organizationDomain.Id);
await sutProvider.GetDependency<IVerifyOrganizationDomainCommand>().Received(1) await sutProvider.GetDependency<IVerifyOrganizationDomainCommand>().Received(1)
.VerifyOrganizationDomainAsync(organizationDomain); .UserVerifyOrganizationDomainAsync(organizationDomain);
Assert.IsType<OrganizationDomainResponseModel>(result); Assert.IsType<OrganizationDomainResponseModel>(result);
} }

View File

@ -52,7 +52,7 @@ public class OrganizationBillingControllerTests
{ {
sutProvider.GetDependency<ICurrentContext>().AccessMembersTab(organizationId).Returns(true); sutProvider.GetDependency<ICurrentContext>().AccessMembersTab(organizationId).Returns(true);
sutProvider.GetDependency<IOrganizationBillingService>().GetMetadata(organizationId) sutProvider.GetDependency<IOrganizationBillingService>().GetMetadata(organizationId)
.Returns(new OrganizationMetadata(true)); .Returns(new OrganizationMetadata(true, true));
var result = await sutProvider.Sut.GetMetadataAsync(organizationId); var result = await sutProvider.Sut.GetMetadataAsync(organizationId);
@ -60,6 +60,7 @@ public class OrganizationBillingControllerTests
var organizationMetadataResponse = ((Ok<OrganizationMetadataResponse>)result).Value; var organizationMetadataResponse = ((Ok<OrganizationMetadataResponse>)result).Value;
Assert.True(organizationMetadataResponse.IsEligibleForSelfHost);
Assert.True(organizationMetadataResponse.IsOnSecretsManagerStandalone); Assert.True(organizationMetadataResponse.IsOnSecretsManagerStandalone);
} }

View File

@ -1,5 +1,6 @@
using System.Security.Claims; using System.Security.Claims;
using Bit.Api.Vault.AuthorizationHandlers.Collections; using Bit.Api.Vault.AuthorizationHandlers.Collections;
using Bit.Core;
using Bit.Core.Context; using Bit.Core.Context;
using Bit.Core.Entities; using Bit.Core.Entities;
using Bit.Core.Enums; using Bit.Core.Enums;
@ -32,7 +33,10 @@ public class BulkCollectionAuthorizationHandlerTests
organization.Type = userType; organization.Type = userType;
organization.Permissions = new Permissions(); organization.Permissions = new Permissions();
ArrangeOrganizationAbility(sutProvider, organization, true); // `LimitCollectonCreationDeletionSplit` feature flag state isn't
// relevant for this test. The flag is never checked for in this
// test. This is asserted below.
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Create }, new[] { BulkCollectionOperations.Create },
@ -44,11 +48,12 @@ public class BulkCollectionAuthorizationHandlerTests
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.True(context.HasSucceeded); Assert.True(context.HasSucceeded);
} }
[Theory, BitAutoData, CollectionCustomization] [Theory, BitAutoData, CollectionCustomization]
public async Task CanCreateAsync_WhenUser_WithLimitCollectionCreationDeletionFalse_Success( public async Task CanCreateAsync_WhenUser_WithLimitCollectionCreationFalse_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<Collection> collections, ICollection<Collection> collections,
CurrentContextOrganization organization) CurrentContextOrganization organization)
@ -57,7 +62,7 @@ public class BulkCollectionAuthorizationHandlerTests
organization.Type = OrganizationUserType.User; organization.Type = OrganizationUserType.User;
ArrangeOrganizationAbility(sutProvider, organization, false); ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, false, false);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Create }, new[] { BulkCollectionOperations.Create },
@ -66,16 +71,49 @@ 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<IFeatureService>()
.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)
.Returns(false);
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.True(context.HasSucceeded);
}
[Theory, BitAutoData, CollectionCustomization]
public async Task CanCreateAsync_WhenUser_WithLimitCollectionCreationFalse_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<Collection> collections,
CurrentContextOrganization organization)
{
var actingUserId = Guid.NewGuid();
organization.Type = OrganizationUserType.User;
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, false, false);
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Create },
new ClaimsPrincipal(),
collections);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)
.Returns(true);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.True(context.HasSucceeded); Assert.True(context.HasSucceeded);
} }
[Theory, CollectionCustomization] [Theory, CollectionCustomization]
[BitAutoData(OrganizationUserType.User)] [BitAutoData(OrganizationUserType.User)]
[BitAutoData(OrganizationUserType.Custom)] [BitAutoData(OrganizationUserType.Custom)]
public async Task CanCreateAsync_WhenMissingPermissions_NoSuccess( public async Task CanCreateAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureDisabled_NoSuccess(
OrganizationUserType userType, OrganizationUserType userType,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<Collection> collections, ICollection<Collection> collections,
@ -92,7 +130,7 @@ public class BulkCollectionAuthorizationHandlerTests
ManageUsers = false ManageUsers = false
}; };
ArrangeOrganizationAbility(sutProvider, organization, true); ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Create }, new[] { BulkCollectionOperations.Create },
@ -102,21 +140,61 @@ 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<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false); sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded);
}
[Theory, CollectionCustomization]
[BitAutoData(OrganizationUserType.User)]
[BitAutoData(OrganizationUserType.Custom)]
public async Task CanCreateAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureEnabled_NoSuccess(
OrganizationUserType userType,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<Collection> collections,
CurrentContextOrganization organization)
{
var actingUserId = Guid.NewGuid();
organization.Type = userType;
organization.Permissions = new Permissions
{
EditAnyCollection = false,
DeleteAnyCollection = false,
ManageGroups = false,
ManageUsers = false
};
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Create },
new ClaimsPrincipal(),
collections);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded); Assert.False(context.HasSucceeded);
} }
[Theory, BitAutoData, CollectionCustomization] [Theory, BitAutoData, CollectionCustomization]
public async Task CanCreateAsync_WhenMissingOrgAccess_NoSuccess( public async Task CanCreateAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitDisabled_NoSuccess(
Guid userId, Guid userId,
CurrentContextOrganization organization, CurrentContextOrganization organization,
List<Collection> collections, List<Collection> collections,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider) SutProvider<BulkCollectionAuthorizationHandler> sutProvider)
{ {
collections.ForEach(c => c.OrganizationId = organization.Id); collections.ForEach(c => c.OrganizationId = organization.Id);
ArrangeOrganizationAbility(sutProvider, organization, true); ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Create }, new[] { BulkCollectionOperations.Create },
@ -127,8 +205,38 @@ 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); sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded);
}
[Theory, BitAutoData, CollectionCustomization]
public async Task CanCreateAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitEnabled_NoSuccess(
Guid userId,
CurrentContextOrganization organization,
List<Collection> collections,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider)
{
collections.ForEach(c => c.OrganizationId = organization.Id);
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Create },
new ClaimsPrincipal(),
collections
);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded); Assert.False(context.HasSucceeded);
} }
@ -904,7 +1012,10 @@ public class BulkCollectionAuthorizationHandlerTests
DeleteAnyCollection = true DeleteAnyCollection = true
}; };
ArrangeOrganizationAbility(sutProvider, organization, true); // `LimitCollectonCreationDeletionSplit` feature flag state isn't
// relevant for this test. The flag is never checked for in this
// test. This is asserted below.
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete }, new[] { BulkCollectionOperations.Delete },
@ -916,6 +1027,7 @@ public class BulkCollectionAuthorizationHandlerTests
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.True(context.HasSucceeded); Assert.True(context.HasSucceeded);
} }
@ -931,7 +1043,10 @@ public class BulkCollectionAuthorizationHandlerTests
organization.Type = userType; organization.Type = userType;
organization.Permissions = new Permissions(); organization.Permissions = new Permissions();
ArrangeOrganizationAbility(sutProvider, organization, true); // `LimitCollectonCreationDeletionSplit` feature flag state isn't
// relevant for this test. The flag is never checked for in this
// test. This is asserted below.
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete }, new[] { BulkCollectionOperations.Delete },
@ -943,11 +1058,12 @@ public class BulkCollectionAuthorizationHandlerTests
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.True(context.HasSucceeded); Assert.True(context.HasSucceeded);
} }
[Theory, BitAutoData, CollectionCustomization] [Theory, BitAutoData, CollectionCustomization]
public async Task CanDeleteAsync_WhenUser_LimitCollectionCreationDeletionFalse_WithCanManagePermission_Success( public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections, ICollection<CollectionDetails> collections,
CurrentContextOrganization organization) CurrentContextOrganization organization)
@ -957,11 +1073,12 @@ public class BulkCollectionAuthorizationHandlerTests
organization.Type = OrganizationUserType.User; organization.Type = OrganizationUserType.User;
organization.Permissions = new Permissions(); organization.Permissions = new Permissions();
ArrangeOrganizationAbility(sutProvider, organization, false); ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, false, 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).Returns(collections); sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
foreach (var c in collections) foreach (var c in collections)
{ {
@ -975,6 +1092,41 @@ public class BulkCollectionAuthorizationHandlerTests
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.True(context.HasSucceeded);
}
[Theory, BitAutoData, CollectionCustomization]
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections,
CurrentContextOrganization organization)
{
var actingUserId = Guid.NewGuid();
organization.Type = OrganizationUserType.User;
organization.Permissions = new Permissions();
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, false, false);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
foreach (var c in collections)
{
c.Manage = true;
}
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete },
new ClaimsPrincipal(),
collections);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.True(context.HasSucceeded); Assert.True(context.HasSucceeded);
} }
@ -982,7 +1134,7 @@ public class BulkCollectionAuthorizationHandlerTests
[BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Owner)]
[BitAutoData(OrganizationUserType.User)] [BitAutoData(OrganizationUserType.User)]
public async Task CanDeleteAsync_LimitCollectionCreationDeletionFalse_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_Success( public async Task CanDeleteAsync_LimitCollectionDeletionFalse_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
OrganizationUserType userType, OrganizationUserType userType,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections, ICollection<CollectionDetails> collections,
@ -993,11 +1145,12 @@ public class BulkCollectionAuthorizationHandlerTests
organization.Type = userType; organization.Type = userType;
organization.Permissions = new Permissions(); organization.Permissions = new Permissions();
ArrangeOrganizationAbility(sutProvider, organization, false, false); ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, false, false, 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).Returns(collections); sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
foreach (var c in collections) foreach (var c in collections)
{ {
@ -1011,13 +1164,15 @@ public class BulkCollectionAuthorizationHandlerTests
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.True(context.HasSucceeded); Assert.True(context.HasSucceeded);
} }
[Theory, CollectionCustomization] [Theory, CollectionCustomization]
[BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Owner)]
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_Success( [BitAutoData(OrganizationUserType.User)]
public async Task CanDeleteAsync_LimitCollectionDeletionFalse_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success(
OrganizationUserType userType, OrganizationUserType userType,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections, ICollection<CollectionDetails> collections,
@ -1028,11 +1183,12 @@ public class BulkCollectionAuthorizationHandlerTests
organization.Type = userType; organization.Type = userType;
organization.Permissions = new Permissions(); organization.Permissions = new Permissions();
ArrangeOrganizationAbility(sutProvider, organization, true, false); ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, false, false, 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).Returns(collections); sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
foreach (var c in collections) foreach (var c in collections)
{ {
@ -1046,13 +1202,14 @@ public class BulkCollectionAuthorizationHandlerTests
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.True(context.HasSucceeded); Assert.True(context.HasSucceeded);
} }
[Theory, CollectionCustomization] [Theory, CollectionCustomization]
[BitAutoData(OrganizationUserType.Admin)] [BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)] [BitAutoData(OrganizationUserType.Owner)]
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithoutCanManagePermission_Failure( public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
OrganizationUserType userType, OrganizationUserType userType,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections, ICollection<CollectionDetails> collections,
@ -1063,12 +1220,87 @@ public class BulkCollectionAuthorizationHandlerTests
organization.Type = userType; organization.Type = userType;
organization.Permissions = new Permissions(); organization.Permissions = new Permissions();
ArrangeOrganizationAbility(sutProvider, organization, true, false); ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true, false);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
foreach (var c in collections)
{
c.Manage = true;
}
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete },
new ClaimsPrincipal(),
collections);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.True(context.HasSucceeded);
}
[Theory, CollectionCustomization]
[BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)]
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success(
OrganizationUserType userType,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections,
CurrentContextOrganization organization)
{
var actingUserId = Guid.NewGuid();
organization.Type = userType;
organization.Permissions = new Permissions();
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true, false);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
foreach (var c in collections)
{
c.Manage = true;
}
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete },
new ClaimsPrincipal(),
collections);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.True(context.HasSucceeded);
}
[Theory, CollectionCustomization]
[BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)]
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithoutCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Failure(
OrganizationUserType userType,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections,
CurrentContextOrganization organization)
{
var actingUserId = Guid.NewGuid();
organization.Type = userType;
organization.Permissions = new Permissions();
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true, 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).Returns(collections); sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false); sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
foreach (var c in collections) foreach (var c in collections)
{ {
@ -1082,11 +1314,50 @@ public class BulkCollectionAuthorizationHandlerTests
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded);
}
[Theory, CollectionCustomization]
[BitAutoData(OrganizationUserType.Admin)]
[BitAutoData(OrganizationUserType.Owner)]
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithoutCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Failure(
OrganizationUserType userType,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections,
CurrentContextOrganization organization)
{
var actingUserId = Guid.NewGuid();
organization.Type = userType;
organization.Permissions = new Permissions();
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true, false);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
foreach (var c in collections)
{
c.Manage = false;
}
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete },
new ClaimsPrincipal(),
collections);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded); Assert.False(context.HasSucceeded);
} }
[Theory, BitAutoData, CollectionCustomization] [Theory, BitAutoData, CollectionCustomization]
public async Task CanDeleteAsync_WhenUser_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsTrue_Failure( public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsTrue_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Failure(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections, ICollection<CollectionDetails> collections,
CurrentContextOrganization organization) CurrentContextOrganization organization)
@ -1096,12 +1367,13 @@ public class BulkCollectionAuthorizationHandlerTests
organization.Type = OrganizationUserType.User; organization.Type = OrganizationUserType.User;
organization.Permissions = new Permissions(); organization.Permissions = new Permissions();
ArrangeOrganizationAbility(sutProvider, organization, true); ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true);
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).Returns(collections); sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false); sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
foreach (var c in collections) foreach (var c in collections)
{ {
@ -1115,11 +1387,12 @@ public class BulkCollectionAuthorizationHandlerTests
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded); Assert.False(context.HasSucceeded);
} }
[Theory, BitAutoData, CollectionCustomization] [Theory, BitAutoData, CollectionCustomization]
public async Task CanDeleteAsync_WhenUser_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_Failure( public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsTrue_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Failure(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections, ICollection<CollectionDetails> collections,
CurrentContextOrganization organization) CurrentContextOrganization organization)
@ -1129,12 +1402,13 @@ public class BulkCollectionAuthorizationHandlerTests
organization.Type = OrganizationUserType.User; organization.Type = OrganizationUserType.User;
organization.Permissions = new Permissions(); organization.Permissions = new Permissions();
ArrangeOrganizationAbility(sutProvider, organization, true, false); ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
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).Returns(collections); sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false); sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
foreach (var c in collections) foreach (var c in collections)
{ {
@ -1148,13 +1422,88 @@ public class BulkCollectionAuthorizationHandlerTests
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded);
}
[Theory, BitAutoData, CollectionCustomization]
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Failure(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections,
CurrentContextOrganization organization)
{
var actingUserId = Guid.NewGuid();
organization.Type = OrganizationUserType.User;
organization.Permissions = new Permissions();
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true, false);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)
.Returns(false);
foreach (var c in collections)
{
c.Manage = true;
}
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete },
new ClaimsPrincipal(),
collections);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded);
}
[Theory, BitAutoData, CollectionCustomization]
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Failure(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<CollectionDetails> collections,
CurrentContextOrganization organization)
{
var actingUserId = Guid.NewGuid();
organization.Type = OrganizationUserType.User;
organization.Permissions = new Permissions();
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true, false);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)
.Returns(true);
foreach (var c in collections)
{
c.Manage = true;
}
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete },
new ClaimsPrincipal(),
collections);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded); Assert.False(context.HasSucceeded);
} }
[Theory, CollectionCustomization] [Theory, CollectionCustomization]
[BitAutoData(OrganizationUserType.User)] [BitAutoData(OrganizationUserType.User)]
[BitAutoData(OrganizationUserType.Custom)] [BitAutoData(OrganizationUserType.Custom)]
public async Task CanDeleteAsync_WhenMissingPermissions_NoSuccess( public async Task CanDeleteAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureDisabled_NoSuccess(
OrganizationUserType userType, OrganizationUserType userType,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<Collection> collections, ICollection<Collection> collections,
@ -1171,7 +1520,7 @@ public class BulkCollectionAuthorizationHandlerTests
ManageUsers = false ManageUsers = false
}; };
ArrangeOrganizationAbility(sutProvider, organization, true); ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete }, new[] { BulkCollectionOperations.Delete },
@ -1181,14 +1530,54 @@ 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<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false); sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded);
}
[Theory, CollectionCustomization]
[BitAutoData(OrganizationUserType.User)]
[BitAutoData(OrganizationUserType.Custom)]
public async Task CanDeleteAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureEnabled_NoSuccess(
OrganizationUserType userType,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<Collection> collections,
CurrentContextOrganization organization)
{
var actingUserId = Guid.NewGuid();
organization.Type = userType;
organization.Permissions = new Permissions
{
EditAnyCollection = false,
DeleteAnyCollection = false,
ManageGroups = false,
ManageUsers = false
};
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete },
new ClaimsPrincipal(),
collections);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded); Assert.False(context.HasSucceeded);
} }
[Theory, BitAutoData, CollectionCustomization] [Theory, BitAutoData, CollectionCustomization]
public async Task CanDeleteAsync_WhenMissingOrgAccess_NoSuccess( public async Task CanDeleteAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitFeatureDisabled_NoSuccess(
Guid userId, Guid userId,
ICollection<Collection> collections, ICollection<Collection> collections,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider) SutProvider<BulkCollectionAuthorizationHandler> sutProvider)
@ -1202,8 +1591,34 @@ 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); sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded);
}
[Theory, BitAutoData, CollectionCustomization]
public async Task CanDeleteAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitFeatureEnabled_NoSuccess(
Guid userId,
ICollection<Collection> collections,
SutProvider<BulkCollectionAuthorizationHandler> sutProvider)
{
var context = new AuthorizationHandlerContext(
new[] { BulkCollectionOperations.Delete },
new ClaimsPrincipal(),
collections
);
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
await sutProvider.Sut.HandleAsync(context);
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
Assert.False(context.HasSucceeded); Assert.False(context.HasSucceeded);
} }
@ -1224,6 +1639,7 @@ public class BulkCollectionAuthorizationHandlerTests
await sutProvider.Sut.HandleAsync(context); await sutProvider.Sut.HandleAsync(context);
Assert.True(context.HasFailed); Assert.True(context.HasFailed);
sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs(); sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs();
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
} }
[Theory, BitAutoData, CollectionCustomization] [Theory, BitAutoData, CollectionCustomization]
@ -1247,10 +1663,11 @@ public class BulkCollectionAuthorizationHandlerTests
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.HandleAsync(context)); var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.HandleAsync(context));
Assert.Equal("Requested collections must belong to the same organization.", exception.Message); Assert.Equal("Requested collections must belong to the same organization.", exception.Message);
sutProvider.GetDependency<ICurrentContext>().DidNotReceiveWithAnyArgs().GetOrganization(default); sutProvider.GetDependency<ICurrentContext>().DidNotReceiveWithAnyArgs().GetOrganization(default);
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
} }
[Theory, BitAutoData, CollectionCustomization] [Theory, BitAutoData, CollectionCustomization]
public async Task HandleRequirementAsync_Provider_Success( public async Task HandleRequirementAsync_Provider_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<Collection> collections) ICollection<Collection> collections)
{ {
@ -1286,6 +1703,63 @@ public class BulkCollectionAuthorizationHandlerTests
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync() sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync()
.Returns(organizationAbilities); .Returns(organizationAbilities);
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(true); sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(true);
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
var context = new AuthorizationHandlerContext(
new[] { op },
new ClaimsPrincipal(),
collections
);
await sutProvider.Sut.HandleAsync(context);
Assert.True(context.HasSucceeded);
await sutProvider.GetDependency<ICurrentContext>().Received().ProviderUserForOrgAsync(orgId);
// Recreate the SUT to reset the mocks/dependencies between tests
sutProvider.Recreate();
}
}
[Theory, BitAutoData, CollectionCustomization]
public async Task HandleRequirementAsync_Provider_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
ICollection<Collection> collections)
{
var actingUserId = Guid.NewGuid();
var orgId = collections.First().OrganizationId;
var organizationAbilities = new Dictionary<Guid, OrganizationAbility>
{
{ collections.First().OrganizationId,
new OrganizationAbility
{
LimitCollectionCreation = true,
LimitCollectionDeletion = true,
AllowAdminAccessToAllCollectionItems = true
}
}
};
var operationsToTest = new[]
{
BulkCollectionOperations.Create,
BulkCollectionOperations.Read,
BulkCollectionOperations.ReadAccess,
BulkCollectionOperations.Update,
BulkCollectionOperations.ModifyUserAccess,
BulkCollectionOperations.ModifyGroupAccess,
BulkCollectionOperations.Delete,
};
foreach (var op in operationsToTest)
{
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
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<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
var context = new AuthorizationHandlerContext( var context = new AuthorizationHandlerContext(
new[] { op }, new[] { op },
@ -1336,14 +1810,37 @@ public class BulkCollectionAuthorizationHandlerTests
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdAsync(Arg.Any<Guid>()); await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdAsync(Arg.Any<Guid>());
} }
private static void ArrangeOrganizationAbility( private static void ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider, SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
CurrentContextOrganization organization, bool limitCollectionCreationDeletion, CurrentContextOrganization organization,
bool limitCollectionCreation,
bool limitCollectionDeletion,
bool allowAdminAccessToAllCollectionItems = true) bool allowAdminAccessToAllCollectionItems = true)
{ {
var organizationAbility = new OrganizationAbility(); var organizationAbility = new OrganizationAbility();
organizationAbility.Id = organization.Id; organizationAbility.Id = organization.Id;
organizationAbility.LimitCollectionCreationDeletion = limitCollectionCreationDeletion;
organizationAbility.LimitCollectionCreationDeletion = limitCollectionCreation || limitCollectionDeletion;
organizationAbility.AllowAdminAccessToAllCollectionItems = allowAdminAccessToAllCollectionItems;
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)
.Returns(organizationAbility);
}
private static void ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
CurrentContextOrganization organization,
bool limitCollectionCreation,
bool limitCollectionDeletion,
bool allowAdminAccessToAllCollectionItems = true)
{
var organizationAbility = new OrganizationAbility();
organizationAbility.Id = organization.Id;
organizationAbility.LimitCollectionCreation = limitCollectionCreation;
organizationAbility.LimitCollectionDeletion = limitCollectionDeletion;
organizationAbility.AllowAdminAccessToAllCollectionItems = allowAdminAccessToAllCollectionItems; organizationAbility.AllowAdminAccessToAllCollectionItems = allowAdminAccessToAllCollectionItems;
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id) sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)

View File

@ -1,6 +1,5 @@
using AutoFixture; using AutoFixture;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Org.BouncyCastle.Security;
namespace Bit.Test.Common.AutoFixture; namespace Bit.Test.Common.AutoFixture;
@ -15,7 +14,7 @@ public class ControllerCustomization : ICustomization
{ {
if (!controllerType.IsAssignableTo(typeof(Controller))) if (!controllerType.IsAssignableTo(typeof(Controller)))
{ {
throw new InvalidParameterException($"{nameof(controllerType)} must derive from {typeof(Controller).Name}"); throw new Exception($"{nameof(controllerType)} must derive from {typeof(Controller).Name}");
} }
_controllerType = controllerType; _controllerType = controllerType;

View File

@ -127,7 +127,6 @@ public class SutProvider<TSut> : ISutProvider
return _sutProvider.GetDependency(parameterInfo.ParameterType, ""); return _sutProvider.GetDependency(parameterInfo.ParameterType, "");
} }
// This is the equivalent of _fixture.Create<parameterInfo.ParameterType>, but no overload for // This is the equivalent of _fixture.Create<parameterInfo.ParameterType>, but no overload for
// Create(Type type) exists. // Create(Type type) exists.
var dependency = new SpecimenContext(_fixture).Resolve(new SeededRequest(parameterInfo.ParameterType, var dependency = new SpecimenContext(_fixture).Resolve(new SeededRequest(parameterInfo.ParameterType,

View File

@ -1,6 +1,7 @@
using AutoFixture; using AutoFixture;
using Bit.Core.Services; using Bit.Core.Services;
using Bit.Core.Settings; using Bit.Core.Settings;
using Microsoft.Extensions.Time.Testing;
using NSubstitute; using NSubstitute;
using RichardSzalay.MockHttp; using RichardSzalay.MockHttp;
@ -47,4 +48,19 @@ public static class SutProviderExtensions
.SetDependency(mockHttpClientFactory) .SetDependency(mockHttpClientFactory)
.Create(); .Create();
} }
/// <summary>
/// Configures SutProvider to use FakeTimeProvider.
/// It is registered under both the TimeProvider type and the FakeTimeProvider type
/// so that it can be retrieved in a type-safe manner with GetDependency.
/// This can be chained with other builder methods; make sure to call
/// <see cref="ISutProvider.Create"/> before use.
/// </summary>
public static SutProvider<T> WithFakeTimeProvider<T>(this SutProvider<T> sutProvider)
{
var fakeTimeProvider = new FakeTimeProvider();
return sutProvider
.SetDependency((TimeProvider)fakeTimeProvider)
.SetDependency(fakeTimeProvider);
}
} }

View File

@ -5,6 +5,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="8.10.0" />
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" /> <PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
<PackageReference Include="xunit" Version="$(XUnitVersion)" /> <PackageReference Include="xunit" Version="$(XUnitVersion)" />
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)"> <PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)">

View File

@ -9,10 +9,12 @@ namespace Bit.Core.Test.AdminConsole.AutoFixture;
internal class PolicyCustomization : ICustomization internal class PolicyCustomization : ICustomization
{ {
public PolicyType Type { get; set; } public PolicyType Type { get; set; }
public bool Enabled { get; set; }
public PolicyCustomization(PolicyType type) public PolicyCustomization(PolicyType type, bool enabled)
{ {
Type = type; Type = type;
Enabled = enabled;
} }
public void Customize(IFixture fixture) public void Customize(IFixture fixture)
@ -20,21 +22,23 @@ internal class PolicyCustomization : ICustomization
fixture.Customize<Policy>(composer => composer fixture.Customize<Policy>(composer => composer
.With(o => o.OrganizationId, Guid.NewGuid()) .With(o => o.OrganizationId, Guid.NewGuid())
.With(o => o.Type, Type) .With(o => o.Type, Type)
.With(o => o.Enabled, true)); .With(o => o.Enabled, Enabled));
} }
} }
public class PolicyAttribute : CustomizeAttribute public class PolicyAttribute : CustomizeAttribute
{ {
private readonly PolicyType _type; private readonly PolicyType _type;
private readonly bool _enabled;
public PolicyAttribute(PolicyType type) public PolicyAttribute(PolicyType type, bool enabled = true)
{ {
_type = type; _type = type;
_enabled = enabled;
} }
public override ICustomization GetCustomization(ParameterInfo parameter) public override ICustomization GetCustomization(ParameterInfo parameter)
{ {
return new PolicyCustomization(_type); return new PolicyCustomization(_type, _enabled);
} }
} }

View File

@ -0,0 +1,25 @@
using System.Reflection;
using AutoFixture;
using AutoFixture.Xunit2;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
namespace Bit.Core.Test.AdminConsole.AutoFixture;
internal class PolicyUpdateCustomization(PolicyType type, bool enabled) : ICustomization
{
public void Customize(IFixture fixture)
{
fixture.Customize<PolicyUpdate>(composer => composer
.With(o => o.Type, type)
.With(o => o.Enabled, enabled));
}
}
public class PolicyUpdateAttribute(PolicyType type, bool enabled = true) : CustomizeAttribute
{
public override ICustomization GetCustomization(ParameterInfo parameter)
{
return new PolicyUpdateCustomization(type, enabled);
}
}

View File

@ -15,7 +15,7 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationDomains;
public class VerifyOrganizationDomainCommandTests public class VerifyOrganizationDomainCommandTests
{ {
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task VerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimed(Guid id, public async Task UserVerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimed(Guid id,
SutProvider<VerifyOrganizationDomainCommand> sutProvider) SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{ {
var expected = new OrganizationDomain var expected = new OrganizationDomain
@ -30,14 +30,14 @@ public class VerifyOrganizationDomainCommandTests
.GetByIdAsync(id) .GetByIdAsync(id)
.Returns(expected); .Returns(expected);
var requestAction = async () => await sutProvider.Sut.VerifyOrganizationDomainAsync(expected); var requestAction = async () => await sutProvider.Sut.UserVerifyOrganizationDomainAsync(expected);
var exception = await Assert.ThrowsAsync<ConflictException>(requestAction); var exception = await Assert.ThrowsAsync<ConflictException>(requestAction);
Assert.Contains("Domain has already been verified.", exception.Message); Assert.Contains("Domain has already been verified.", exception.Message);
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task VerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimedByAnotherOrganization(Guid id, public async Task UserVerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimedByAnotherOrganization(Guid id,
SutProvider<VerifyOrganizationDomainCommand> sutProvider) SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{ {
var expected = new OrganizationDomain var expected = new OrganizationDomain
@ -54,14 +54,14 @@ public class VerifyOrganizationDomainCommandTests
.GetClaimedDomainsByDomainNameAsync(expected.DomainName) .GetClaimedDomainsByDomainNameAsync(expected.DomainName)
.Returns(new List<OrganizationDomain> { expected }); .Returns(new List<OrganizationDomain> { expected });
var requestAction = async () => await sutProvider.Sut.VerifyOrganizationDomainAsync(expected); var requestAction = async () => await sutProvider.Sut.UserVerifyOrganizationDomainAsync(expected);
var exception = await Assert.ThrowsAsync<ConflictException>(requestAction); var exception = await Assert.ThrowsAsync<ConflictException>(requestAction);
Assert.Contains("The domain is not available to be claimed.", exception.Message); Assert.Contains("The domain is not available to be claimed.", exception.Message);
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task VerifyOrganizationDomain_ShouldVerifyDomainUpdateAndLogEvent_WhenTxtRecordExists(Guid id, public async Task UserVerifyOrganizationDomain_ShouldVerifyDomainUpdateAndLogEvent_WhenTxtRecordExists(Guid id,
SutProvider<VerifyOrganizationDomainCommand> sutProvider) SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{ {
var expected = new OrganizationDomain var expected = new OrganizationDomain
@ -81,7 +81,7 @@ public class VerifyOrganizationDomainCommandTests
.ResolveAsync(expected.DomainName, Arg.Any<string>()) .ResolveAsync(expected.DomainName, Arg.Any<string>())
.Returns(true); .Returns(true);
var result = await sutProvider.Sut.VerifyOrganizationDomainAsync(expected); var result = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(expected);
Assert.NotNull(result.VerifiedDate); Assert.NotNull(result.VerifiedDate);
await sutProvider.GetDependency<IOrganizationDomainRepository>().Received(1) await sutProvider.GetDependency<IOrganizationDomainRepository>().Received(1)
@ -91,7 +91,7 @@ public class VerifyOrganizationDomainCommandTests
} }
[Theory, BitAutoData] [Theory, BitAutoData]
public async Task VerifyOrganizationDomain_ShouldNotSetVerifiedDate_WhenTxtRecordDoesNotExist(Guid id, public async Task UserVerifyOrganizationDomain_ShouldNotSetVerifiedDate_WhenTxtRecordDoesNotExist(Guid id,
SutProvider<VerifyOrganizationDomainCommand> sutProvider) SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{ {
var expected = new OrganizationDomain var expected = new OrganizationDomain
@ -111,10 +111,30 @@ public class VerifyOrganizationDomainCommandTests
.ResolveAsync(expected.DomainName, Arg.Any<string>()) .ResolveAsync(expected.DomainName, Arg.Any<string>())
.Returns(false); .Returns(false);
var result = await sutProvider.Sut.VerifyOrganizationDomainAsync(expected); var result = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(expected);
Assert.Null(result.VerifiedDate); Assert.Null(result.VerifiedDate);
await sutProvider.GetDependency<IEventService>().Received(1) await sutProvider.GetDependency<IEventService>().Received(1)
.LogOrganizationDomainEventAsync(Arg.Any<OrganizationDomain>(), EventType.OrganizationDomain_NotVerified); .LogOrganizationDomainEventAsync(Arg.Any<OrganizationDomain>(), EventType.OrganizationDomain_NotVerified);
} }
[Theory, BitAutoData]
public async Task SystemVerifyOrganizationDomain_CallsEventServiceWithUpdatedJobRunCount(SutProvider<VerifyOrganizationDomainCommand> sutProvider)
{
var domain = new OrganizationDomain()
{
Id = Guid.NewGuid(),
OrganizationId = Guid.NewGuid(),
CreationDate = DateTime.UtcNow,
DomainName = "test.com",
Txt = "btw+12345",
};
_ = await sutProvider.Sut.SystemVerifyOrganizationDomainAsync(domain);
await sutProvider.GetDependency<IEventService>().ReceivedWithAnyArgs(1)
.LogOrganizationDomainEventAsync(default, EventType.OrganizationDomain_NotVerified,
EventSystemUser.DomainVerification);
}
} }

View File

@ -0,0 +1,43 @@
#nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
using NSubstitute;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;
public class FakeSingleOrgPolicyValidator : IPolicyValidator
{
public PolicyType Type => PolicyType.SingleOrg;
public IEnumerable<PolicyType> RequiredPolicies => Array.Empty<PolicyType>();
public readonly Func<PolicyUpdate, Policy?, Task<string>> ValidateAsyncMock = Substitute.For<Func<PolicyUpdate, Policy?, Task<string>>>();
public readonly Action<PolicyUpdate, Policy?> OnSaveSideEffectsAsyncMock = Substitute.For<Action<PolicyUpdate, Policy?>>();
public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
{
return ValidateAsyncMock(policyUpdate, currentPolicy);
}
public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy)
{
OnSaveSideEffectsAsyncMock(policyUpdate, currentPolicy);
return Task.FromResult(0);
}
}
public class FakeRequireSsoPolicyValidator : IPolicyValidator
{
public PolicyType Type => PolicyType.RequireSso;
public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];
public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult("");
public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(0);
}
public class FakeVaultTimeoutPolicyValidator : IPolicyValidator
{
public PolicyType Type => PolicyType.MaximumVaultTimeout;
public IEnumerable<PolicyType> RequiredPolicies => [PolicyType.SingleOrg];
public Task<string> ValidateAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult("");
public Task OnSaveSideEffectsAsync(PolicyUpdate policyUpdate, Policy? currentPolicy) => Task.FromResult(0);
}

View File

@ -0,0 +1,64 @@
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyValidators;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies;
public class PolicyValidatorHelpersTests
{
[Fact]
public void ValidateDecryptionOptionsNotEnabled_RequiredByKeyConnector_ValidationError()
{
var ssoConfig = new SsoConfig();
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });
var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]);
Assert.Contains("Key Connector is enabled", result);
}
[Fact]
public void ValidateDecryptionOptionsNotEnabled_RequiredByTDE_ValidationError()
{
var ssoConfig = new SsoConfig();
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.TrustedDeviceEncryption });
var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.TrustedDeviceEncryption]);
Assert.Contains("Trusted device encryption is on", result);
}
[Fact]
public void ValidateDecryptionOptionsNotEnabled_NullSsoConfig_NoValidationError()
{
var ssoConfig = new SsoConfig();
var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]);
Assert.True(string.IsNullOrEmpty(result));
}
[Fact]
public void ValidateDecryptionOptionsNotEnabled_RequiredOptionNotEnabled_NoValidationError()
{
var ssoConfig = new SsoConfig();
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });
var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.TrustedDeviceEncryption]);
Assert.True(string.IsNullOrEmpty(result));
}
[Fact]
public void ValidateDecryptionOptionsNotEnabled_SsoConfigDisabled_NoValidationError()
{
var ssoConfig = new SsoConfig();
ssoConfig.Enabled = false;
ssoConfig.SetData(new SsoConfigurationData { MemberDecryptionType = MemberDecryptionType.KeyConnector });
var result = ssoConfig.ValidateDecryptionOptionsNotEnabled([MemberDecryptionType.KeyConnector]);
Assert.True(string.IsNullOrEmpty(result));
}
}

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