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:
commit
aa6b91452d
23
.github/CODEOWNERS
vendored
23
.github/CODEOWNERS
vendored
@ -4,13 +4,22 @@
|
||||
#
|
||||
# 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
|
||||
.github/workflows @bitwarden/dept-devops
|
||||
## Docker files have shared ownership ##
|
||||
**/Dockerfile
|
||||
**/*.Dockerfile
|
||||
**/.dockerignore
|
||||
**/entrypoint.sh
|
||||
|
||||
# DevOps for Docker changes
|
||||
**/Dockerfile @bitwarden/dept-devops
|
||||
**/*.Dockerfile @bitwarden/dept-devops
|
||||
**/.dockerignore @bitwarden/dept-devops
|
||||
## BRE team owns these workflows ##
|
||||
.github/workflows/publish.yml @bitwarden/dept-bre
|
||||
|
||||
## 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
|
||||
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/Views/Tools @bitwarden/team-billing-dev
|
||||
|
||||
# Multiple owners - DO NOT REMOVE (DevOps)
|
||||
# Multiple owners - DO NOT REMOVE (BRE)
|
||||
**/packages.lock.json
|
||||
Directory.Build.props
|
||||
|
@ -1,4 +1,3 @@
|
||||
---
|
||||
name: _move_finalization_db_scripts
|
||||
run-name: Move finalization database scripts
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
---
|
||||
name: Automatic responses
|
||||
on:
|
||||
issues:
|
||||
|
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@ -1,4 +1,3 @@
|
||||
---
|
||||
name: Build
|
||||
|
||||
on:
|
||||
@ -595,7 +594,7 @@ jobs:
|
||||
workflow_id: '_update_ephemeral_tags.yml',
|
||||
ref: 'main',
|
||||
inputs: {
|
||||
ephemeral_env_branch: '${{ github.head_ref }}'
|
||||
ephemeral_env_branch: process.env.GITHUB_HEAD_REF
|
||||
}
|
||||
})
|
||||
|
||||
|
1
.github/workflows/cleanup-after-pr.yml
vendored
1
.github/workflows/cleanup-after-pr.yml
vendored
@ -1,4 +1,3 @@
|
||||
---
|
||||
name: Container registry cleanup
|
||||
|
||||
on:
|
||||
|
59
.github/workflows/cleanup-ephemeral-environment.yml
vendored
Normal file
59
.github/workflows/cleanup-ephemeral-environment.yml
vendored
Normal 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'
|
||||
}
|
||||
})
|
1
.github/workflows/cleanup-rc-branch.yml
vendored
1
.github/workflows/cleanup-rc-branch.yml
vendored
@ -1,4 +1,3 @@
|
||||
---
|
||||
name: Cleanup RC Branch
|
||||
|
||||
on:
|
||||
|
7
.github/workflows/enforce-labels.yml
vendored
7
.github/workflows/enforce-labels.yml
vendored
@ -1,4 +1,3 @@
|
||||
---
|
||||
name: Enforce PR labels
|
||||
|
||||
on:
|
||||
@ -7,13 +6,13 @@ on:
|
||||
types: [labeled, unlabeled, opened, reopened, synchronize]
|
||||
jobs:
|
||||
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
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Check for label
|
||||
run: |
|
||||
echo "PRs with the hold or needs-qa labels cannot be merged"
|
||||
echo "### :x: PRs with the hold or needs-qa labels cannot be merged" >> $GITHUB_STEP_SUMMARY
|
||||
echo "PRs with the hold, needs-qa or ephemeral-environment labels cannot be merged"
|
||||
echo "### :x: PRs with the hold, needs-qa or ephemeral-environment labels cannot be merged" >> $GITHUB_STEP_SUMMARY
|
||||
exit 1
|
||||
|
1
.github/workflows/protect-files.yml
vendored
1
.github/workflows/protect-files.yml
vendored
@ -1,7 +1,6 @@
|
||||
# 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.
|
||||
# The input decides if the label job is ran, adding a label to the PR.
|
||||
---
|
||||
name: Protect files
|
||||
|
||||
on:
|
||||
|
1
.github/workflows/publish.yml
vendored
1
.github/workflows/publish.yml
vendored
@ -1,4 +1,3 @@
|
||||
---
|
||||
name: Publish
|
||||
run-name: Publish ${{ inputs.publish_type }}
|
||||
|
||||
|
1
.github/workflows/release.yml
vendored
1
.github/workflows/release.yml
vendored
@ -1,4 +1,3 @@
|
||||
---
|
||||
name: Release
|
||||
run-name: Release ${{ inputs.release_type }}
|
||||
|
||||
|
1
.github/workflows/stale-bot.yml
vendored
1
.github/workflows/stale-bot.yml
vendored
@ -1,4 +1,3 @@
|
||||
---
|
||||
name: Staleness
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
1
.github/workflows/test-database.yml
vendored
1
.github/workflows/test-database.yml
vendored
@ -1,4 +1,3 @@
|
||||
---
|
||||
name: Database testing
|
||||
|
||||
on:
|
||||
|
@ -3,7 +3,7 @@
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
||||
<Version>2024.10.0</Version>
|
||||
<Version>2024.10.1</Version>
|
||||
|
||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
|
@ -11,7 +11,6 @@ using Bit.Core.Billing.Extensions;
|
||||
using Bit.Core.Billing.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Models.OrganizationConnectionConfigs;
|
||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||
using Bit.Core.Repositories;
|
||||
@ -236,7 +235,8 @@ public class OrganizationsController : Controller
|
||||
if (organization.UseSecretsManager &&
|
||||
!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);
|
||||
|
@ -181,7 +181,6 @@ public class OrganizationEditModel : OrganizationViewModel
|
||||
*/
|
||||
public object GetPlansHelper() =>
|
||||
StaticStore.Plans
|
||||
.Where(p => p.SupportsSecretsManager)
|
||||
.Select(p =>
|
||||
{
|
||||
var plan = new
|
||||
|
@ -1,4 +1,6 @@
|
||||
@model OrganizationViewModel
|
||||
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||
@model OrganizationViewModel
|
||||
|
||||
<dl class="row">
|
||||
<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>
|
||||
@ -53,8 +55,19 @@
|
||||
<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>
|
||||
|
||||
<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>
|
||||
@if (!FeatureService.IsEnabled(Bit.Core.FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
|
||||
{
|
||||
<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>
|
||||
|
||||
<h2>Secrets Manager</h2>
|
||||
|
@ -101,7 +101,7 @@ public class OrganizationDomainController : Controller
|
||||
throw new NotFoundException();
|
||||
}
|
||||
|
||||
organizationDomain = await _verifyOrganizationDomainCommand.VerifyOrganizationDomainAsync(organizationDomain);
|
||||
organizationDomain = await _verifyOrganizationDomainCommand.UserVerifyOrganizationDomainAsync(organizationDomain);
|
||||
|
||||
return new OrganizationDomainResponseModel(organizationDomain);
|
||||
}
|
||||
|
@ -520,9 +520,16 @@ public class OrganizationsController : Controller
|
||||
}
|
||||
|
||||
[HttpPut("{id}/collection-management")]
|
||||
[SelfHosted(NotSelfHostedOnly = true)]
|
||||
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);
|
||||
if (organization == null)
|
||||
{
|
||||
@ -534,7 +541,7 @@ public class OrganizationsController : Controller
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
@ -25,7 +25,6 @@ public class PoliciesController : Controller
|
||||
{
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IUserService _userService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
@ -36,7 +35,6 @@ public class PoliciesController : Controller
|
||||
public PoliciesController(
|
||||
IPolicyRepository policyRepository,
|
||||
IPolicyService policyService,
|
||||
IOrganizationService organizationService,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IUserService userService,
|
||||
ICurrentContext currentContext,
|
||||
@ -46,7 +44,6 @@ public class PoliciesController : Controller
|
||||
{
|
||||
_policyRepository = policyRepository;
|
||||
_policyService = policyService;
|
||||
_organizationService = organizationService;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_userService = userService;
|
||||
_currentContext = currentContext;
|
||||
@ -185,7 +182,7 @@ public class PoliciesController : Controller
|
||||
}
|
||||
|
||||
var userId = _userService.GetProperUserId(User);
|
||||
await _policyService.SaveAsync(policy, _organizationService, userId);
|
||||
await _policyService.SaveAsync(policy, userId);
|
||||
return new PolicyResponseModel(policy);
|
||||
}
|
||||
}
|
||||
|
@ -55,6 +55,9 @@ public class OrganizationResponseModel : ResponseModel
|
||||
SmServiceAccounts = organization.SmServiceAccounts;
|
||||
MaxAutoscaleSmSeats = organization.MaxAutoscaleSmSeats;
|
||||
MaxAutoscaleSmServiceAccounts = organization.MaxAutoscaleSmServiceAccounts;
|
||||
LimitCollectionCreation = organization.LimitCollectionCreation;
|
||||
LimitCollectionDeletion = organization.LimitCollectionDeletion;
|
||||
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||
}
|
||||
@ -98,6 +101,9 @@ public class OrganizationResponseModel : ResponseModel
|
||||
public int? SmServiceAccounts { get; set; }
|
||||
public int? MaxAutoscaleSmSeats { 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 AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
}
|
||||
|
@ -65,6 +65,9 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete;
|
||||
FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil;
|
||||
AccessSecretsManager = organization.AccessSecretsManager;
|
||||
LimitCollectionCreation = organization.LimitCollectionCreation;
|
||||
LimitCollectionDeletion = organization.LimitCollectionDeletion;
|
||||
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||
UserIsManagedByOrganization = organizationIdsManagingUser.Contains(organization.OrganizationId);
|
||||
@ -124,6 +127,9 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
||||
public DateTime? FamilySponsorshipValidUntil { get; set; }
|
||||
public bool? FamilySponsorshipToDelete { 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 AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
/// <summary>
|
||||
|
@ -44,6 +44,9 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
|
||||
ProviderId = organization.ProviderId;
|
||||
ProviderName = organization.ProviderName;
|
||||
ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier;
|
||||
LimitCollectionCreation = organization.LimitCollectionCreation;
|
||||
LimitCollectionDeletion = organization.LimitCollectionDeletion;
|
||||
// https://bitwarden.atlassian.net/browse/PM-10863
|
||||
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Services;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
@ -18,18 +17,15 @@ public class PoliciesController : Controller
|
||||
{
|
||||
private readonly IPolicyRepository _policyRepository;
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly ICurrentContext _currentContext;
|
||||
|
||||
public PoliciesController(
|
||||
IPolicyRepository policyRepository,
|
||||
IPolicyService policyService,
|
||||
IOrganizationService organizationService,
|
||||
ICurrentContext currentContext)
|
||||
{
|
||||
_policyRepository = policyRepository;
|
||||
_policyService = policyService;
|
||||
_organizationService = organizationService;
|
||||
_currentContext = currentContext;
|
||||
}
|
||||
|
||||
@ -96,7 +92,7 @@ public class PoliciesController : Controller
|
||||
{
|
||||
policy = model.ToPolicy(policy);
|
||||
}
|
||||
await _policyService.SaveAsync(policy, _organizationService, null);
|
||||
await _policyService.SaveAsync(policy, null);
|
||||
var response = new PolicyResponseModel(policy);
|
||||
return new JsonResult(response);
|
||||
}
|
||||
|
@ -3,8 +3,11 @@
|
||||
namespace Bit.Api.Billing.Models.Responses;
|
||||
|
||||
public record OrganizationMetadataResponse(
|
||||
bool IsEligibleForSelfHost,
|
||||
bool IsOnSecretsManagerStandalone)
|
||||
{
|
||||
public static OrganizationMetadataResponse From(OrganizationMetadata metadata)
|
||||
=> new(metadata.IsOnSecretsManagerStandalone);
|
||||
=> new(
|
||||
metadata.IsEligibleForSelfHost,
|
||||
metadata.IsOnSecretsManagerStandalone);
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ public class PushController : Controller
|
||||
public async Task PostDelete([FromBody] PushDeviceRequestModel model)
|
||||
{
|
||||
CheckUsage();
|
||||
await _pushRegistrationService.DeleteRegistrationAsync(Prefix(model.Id), model.Type);
|
||||
await _pushRegistrationService.DeleteRegistrationAsync(Prefix(model.Id));
|
||||
}
|
||||
|
||||
[HttpPut("add-organization")]
|
||||
@ -54,7 +54,7 @@ public class PushController : Controller
|
||||
{
|
||||
CheckUsage();
|
||||
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));
|
||||
}
|
||||
|
||||
@ -63,7 +63,7 @@ public class PushController : Controller
|
||||
{
|
||||
CheckUsage();
|
||||
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));
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
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 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;
|
||||
return existingOrganization;
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
#nullable enable
|
||||
using System.Diagnostics;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -123,10 +124,24 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
||||
return true;
|
||||
}
|
||||
|
||||
// If the limit collection management setting is disabled, allow any user to create collections
|
||||
if (await GetOrganizationAbilityAsync(org) is { LimitCollectionCreationDeletion: false })
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
|
||||
{
|
||||
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
|
||||
@ -246,21 +261,35 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
||||
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 })
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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))
|
||||
if (_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
|
||||
{
|
||||
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
|
||||
|
@ -7,6 +7,7 @@ using Bit.Core.Billing.Enums;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Business;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Entities;
|
||||
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
|
||||
/// they have Can Manage permissions for.
|
||||
/// </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>
|
||||
/// 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];
|
||||
}
|
||||
|
||||
public void UpdateFromLicense(OrganizationLicense license)
|
||||
public void UpdateFromLicense(OrganizationLicense license, IFeatureService featureService)
|
||||
{
|
||||
// The following properties are intentionally excluded from being updated:
|
||||
// - Id - self-hosted org will have its own unique Guid
|
||||
@ -300,7 +314,11 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
||||
UseSecretsManager = license.UseSecretsManager;
|
||||
SmSeats = license.SmSeats;
|
||||
SmServiceAccounts = license.SmServiceAccounts;
|
||||
LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems;
|
||||
|
||||
if (!featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
|
||||
{
|
||||
LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,3 +16,30 @@ public enum PolicyType : byte
|
||||
ActivateAutofill = 11,
|
||||
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",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,9 @@ public class OrganizationAbility
|
||||
UseResetPassword = organization.UseResetPassword;
|
||||
UseCustomPermissions = organization.UseCustomPermissions;
|
||||
UsePolicies = organization.UsePolicies;
|
||||
LimitCollectionCreation = organization.LimitCollectionCreation;
|
||||
LimitCollectionDeletion = organization.LimitCollectionDeletion;
|
||||
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||
}
|
||||
@ -37,6 +40,9 @@ public class OrganizationAbility
|
||||
public bool UseResetPassword { get; set; }
|
||||
public bool UseCustomPermissions { 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 AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
}
|
||||
|
@ -54,6 +54,9 @@ public class OrganizationUserOrganizationDetails
|
||||
public bool UsePasswordManager { get; set; }
|
||||
public int? SmSeats { 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 AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
}
|
||||
|
@ -144,6 +144,9 @@ public class SelfHostedOrganizationDetails : Organization
|
||||
RevisionDate = RevisionDate,
|
||||
MaxAutoscaleSeats = MaxAutoscaleSeats,
|
||||
OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling,
|
||||
LimitCollectionCreation = LimitCollectionCreation,
|
||||
LimitCollectionDeletion = LimitCollectionDeletion,
|
||||
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||
LimitCollectionCreationDeletion = LimitCollectionCreationDeletion,
|
||||
AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems,
|
||||
Status = Status
|
||||
|
@ -40,6 +40,8 @@ public class ProviderUserOrganizationDetails
|
||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||
public string ProviderName { get; set; }
|
||||
public PlanType PlanType { get; set; }
|
||||
public bool LimitCollectionCreation { get; set; }
|
||||
public bool LimitCollectionDeletion { get; set; }
|
||||
public bool LimitCollectionCreationDeletion { get; set; }
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||
}
|
||||
|
@ -6,7 +6,6 @@ using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Utilities;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
||||
|
||||
@ -14,21 +13,15 @@ public class CreateOrganizationDomainCommand : ICreateOrganizationDomainCommand
|
||||
{
|
||||
private readonly IOrganizationDomainRepository _organizationDomainRepository;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IDnsResolverService _dnsResolverService;
|
||||
private readonly ILogger<VerifyOrganizationDomainCommand> _logger;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
|
||||
public CreateOrganizationDomainCommand(
|
||||
IOrganizationDomainRepository organizationDomainRepository,
|
||||
IEventService eventService,
|
||||
IDnsResolverService dnsResolverService,
|
||||
ILogger<VerifyOrganizationDomainCommand> logger,
|
||||
IGlobalSettings globalSettings)
|
||||
{
|
||||
_organizationDomainRepository = organizationDomainRepository;
|
||||
_eventService = eventService;
|
||||
_dnsResolverService = dnsResolverService;
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
|
||||
|
@ -4,5 +4,6 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfa
|
||||
|
||||
public interface IVerifyOrganizationDomainCommand
|
||||
{
|
||||
Task<OrganizationDomain> VerifyOrganizationDomainAsync(OrganizationDomain organizationDomain);
|
||||
Task<OrganizationDomain> UserVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain);
|
||||
Task<OrganizationDomain> SystemVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain);
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Exceptions;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
||||
@ -13,34 +14,85 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
||||
private readonly IOrganizationDomainRepository _organizationDomainRepository;
|
||||
private readonly IDnsResolverService _dnsResolverService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly ILogger<VerifyOrganizationDomainCommand> _logger;
|
||||
|
||||
public VerifyOrganizationDomainCommand(
|
||||
IOrganizationDomainRepository organizationDomainRepository,
|
||||
IDnsResolverService dnsResolverService,
|
||||
IEventService eventService,
|
||||
IGlobalSettings globalSettings,
|
||||
ILogger<VerifyOrganizationDomainCommand> logger)
|
||||
{
|
||||
_organizationDomainRepository = organizationDomainRepository;
|
||||
_dnsResolverService = dnsResolverService;
|
||||
_eventService = eventService;
|
||||
_globalSettings = globalSettings;
|
||||
_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)
|
||||
{
|
||||
domain.SetLastCheckedDate();
|
||||
await _organizationDomainRepository.ReplaceAsync(domain);
|
||||
throw new ConflictException("Domain has already been verified.");
|
||||
}
|
||||
|
||||
var claimedDomain =
|
||||
await _organizationDomainRepository.GetClaimedDomainsByDomainNameAsync(domain.DomainName);
|
||||
if (claimedDomain.Any())
|
||||
|
||||
if (claimedDomain.Count > 0)
|
||||
{
|
||||
domain.SetLastCheckedDate();
|
||||
await _organizationDomainRepository.ReplaceAsync(domain);
|
||||
throw new ConflictException("The domain is not available to be claimed.");
|
||||
}
|
||||
@ -58,11 +110,6 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
return devices
|
||||
.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)
|
||||
|
@ -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);
|
||||
}
|
@ -0,0 +1,8 @@
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||
|
||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
|
||||
public interface ISavePolicyCommand
|
||||
{
|
||||
Task SaveAsync(PolicyUpdate policy);
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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>();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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.",
|
||||
_ => ""
|
||||
};
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
@ -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);
|
||||
}
|
@ -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 "";
|
||||
}
|
||||
}
|
@ -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("");
|
||||
}
|
@ -4,13 +4,12 @@ using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||
using Bit.Core.Services;
|
||||
|
||||
namespace Bit.Core.AdminConsole.Services;
|
||||
|
||||
public interface IPolicyService
|
||||
{
|
||||
Task SaveAsync(Policy policy, IOrganizationService organizationService, Guid? savingUserId);
|
||||
Task SaveAsync(Policy policy, Guid? savingUserId);
|
||||
|
||||
/// <summary>
|
||||
/// Get the combined master password policy options for the specified user.
|
||||
|
@ -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.Services;
|
||||
using Bit.Core.Settings;
|
||||
@ -10,26 +11,29 @@ public class OrganizationDomainService : IOrganizationDomainService
|
||||
{
|
||||
private readonly IOrganizationDomainRepository _domainRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IDnsResolverService _dnsResolverService;
|
||||
private readonly IEventService _eventService;
|
||||
private readonly IMailService _mailService;
|
||||
private readonly IVerifyOrganizationDomainCommand _verifyOrganizationDomainCommand;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly ILogger<OrganizationDomainService> _logger;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
|
||||
public OrganizationDomainService(
|
||||
IOrganizationDomainRepository domainRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IDnsResolverService dnsResolverService,
|
||||
IEventService eventService,
|
||||
IMailService mailService,
|
||||
IVerifyOrganizationDomainCommand verifyOrganizationDomainCommand,
|
||||
TimeProvider timeProvider,
|
||||
ILogger<OrganizationDomainService> logger,
|
||||
IGlobalSettings globalSettings)
|
||||
{
|
||||
_domainRepository = domainRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_dnsResolverService = dnsResolverService;
|
||||
_eventService = eventService;
|
||||
_mailService = mailService;
|
||||
_verifyOrganizationDomainCommand = verifyOrganizationDomainCommand;
|
||||
_timeProvider = timeProvider;
|
||||
_logger = logger;
|
||||
_globalSettings = globalSettings;
|
||||
}
|
||||
@ -37,7 +41,7 @@ public class OrganizationDomainService : IOrganizationDomainService
|
||||
public async Task ValidateOrganizationsDomainAsync()
|
||||
{
|
||||
//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);
|
||||
|
||||
@ -45,43 +49,17 @@ public class OrganizationDomainService : IOrganizationDomainService
|
||||
|
||||
foreach (var domain in verifiableDomains)
|
||||
{
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId,
|
||||
"Attempting verification for organization {OrgId} with domain {Domain}",
|
||||
domain.OrganizationId,
|
||||
domain.DomainName);
|
||||
|
||||
try
|
||||
{
|
||||
_logger.LogInformation(Constants.BypassFiltersEventId, "Attempting verification for organization {OrgId} with domain {Domain}", domain.OrganizationId, domain.DomainName);
|
||||
|
||||
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);
|
||||
}
|
||||
_ = await _verifyOrganizationDomainCommand.SystemVerifyOrganizationDomainAsync(domain);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Update entry on OrganizationDomain table
|
||||
domain.SetLastCheckedDate();
|
||||
domain.SetJobRunCount();
|
||||
domain.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval);
|
||||
await _domainRepository.ReplaceAsync(domain);
|
||||
|
||||
|
@ -708,10 +708,16 @@ public class OrganizationService : IOrganizationService
|
||||
UseSecretsManager = license.UseSecretsManager,
|
||||
SmSeats = license.SmSeats,
|
||||
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 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);
|
||||
return devices
|
||||
.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)
|
||||
|
@ -2,6 +2,8 @@
|
||||
using Bit.Core.AdminConsole.Enums;
|
||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||
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.Auth.Enums;
|
||||
using Bit.Core.Auth.Repositories;
|
||||
@ -27,6 +29,8 @@ public class PolicyService : IPolicyService
|
||||
private readonly IMailService _mailService;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||
private readonly IFeatureService _featureService;
|
||||
private readonly ISavePolicyCommand _savePolicyCommand;
|
||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||
|
||||
public PolicyService(
|
||||
@ -39,6 +43,8 @@ public class PolicyService : IPolicyService
|
||||
IMailService mailService,
|
||||
GlobalSettings globalSettings,
|
||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||
IFeatureService featureService,
|
||||
ISavePolicyCommand savePolicyCommand,
|
||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
|
||||
{
|
||||
_applicationCacheService = applicationCacheService;
|
||||
@ -50,11 +56,28 @@ public class PolicyService : IPolicyService
|
||||
_mailService = mailService;
|
||||
_globalSettings = globalSettings;
|
||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||
_featureService = featureService;
|
||||
_savePolicyCommand = savePolicyCommand;
|
||||
_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);
|
||||
if (org == null)
|
||||
{
|
||||
@ -88,7 +111,7 @@ public class PolicyService : IPolicyService
|
||||
return;
|
||||
}
|
||||
|
||||
await EnablePolicyAsync(policy, org, organizationService, savingUserId);
|
||||
await EnablePolicyAsync(policy, org, savingUserId);
|
||||
}
|
||||
|
||||
public async Task<MasterPasswordPolicyData> GetMasterPasswordPolicyForUserAsync(User user)
|
||||
@ -262,7 +285,7 @@ public class PolicyService : IPolicyService
|
||||
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);
|
||||
if (!currentPolicy?.Enabled ?? true)
|
||||
|
@ -20,7 +20,6 @@ public class SsoConfigService : ISsoConfigService
|
||||
private readonly IPolicyService _policyService;
|
||||
private readonly IOrganizationRepository _organizationRepository;
|
||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IEventService _eventService;
|
||||
|
||||
public SsoConfigService(
|
||||
@ -29,7 +28,6 @@ public class SsoConfigService : ISsoConfigService
|
||||
IPolicyService policyService,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IOrganizationUserRepository organizationUserRepository,
|
||||
IOrganizationService organizationService,
|
||||
IEventService eventService)
|
||||
{
|
||||
_ssoConfigRepository = ssoConfigRepository;
|
||||
@ -37,7 +35,6 @@ public class SsoConfigService : ISsoConfigService
|
||||
_policyService = policyService;
|
||||
_organizationRepository = organizationRepository;
|
||||
_organizationUserRepository = organizationUserRepository;
|
||||
_organizationService = organizationService;
|
||||
_eventService = eventService;
|
||||
}
|
||||
|
||||
@ -71,20 +68,20 @@ public class SsoConfigService : ISsoConfigService
|
||||
|
||||
singleOrgPolicy.Enabled = true;
|
||||
|
||||
await _policyService.SaveAsync(singleOrgPolicy, _organizationService, null);
|
||||
await _policyService.SaveAsync(singleOrgPolicy, null);
|
||||
|
||||
var resetPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.ResetPassword) ??
|
||||
new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.ResetPassword, };
|
||||
|
||||
resetPolicy.Enabled = 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) ??
|
||||
new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.RequireSso, };
|
||||
|
||||
ssoRequiredPolicy.Enabled = true;
|
||||
await _policyService.SaveAsync(ssoRequiredPolicy, _organizationService, null);
|
||||
await _policyService.SaveAsync(ssoRequiredPolicy, null);
|
||||
}
|
||||
|
||||
await LogEventsAsync(config, oldConfig);
|
||||
|
@ -3,17 +3,14 @@
|
||||
public enum ProviderMigrationProgress
|
||||
{
|
||||
Started = 1,
|
||||
ClientsMigrated = 2,
|
||||
TeamsPlanConfigured = 3,
|
||||
EnterprisePlanConfigured = 4,
|
||||
CustomerSetup = 5,
|
||||
SubscriptionSetup = 6,
|
||||
CreditApplied = 7,
|
||||
Completed = 8,
|
||||
|
||||
Reversing = 9,
|
||||
ReversedClientMigrations = 10,
|
||||
RemovedProviderPlans = 11
|
||||
NoClients = 2,
|
||||
ClientsMigrated = 3,
|
||||
TeamsPlanConfigured = 4,
|
||||
EnterprisePlanConfigured = 5,
|
||||
CustomerSetup = 6,
|
||||
SubscriptionSetup = 7,
|
||||
CreditApplied = 8,
|
||||
Completed = 9,
|
||||
}
|
||||
|
||||
public class ProviderMigrationTracker
|
||||
|
@ -41,7 +41,18 @@ public class ProviderMigrator(
|
||||
|
||||
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);
|
||||
|
||||
@ -65,6 +76,16 @@ public class ProviderMigrator(
|
||||
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 =>
|
||||
migrationTrackerCache.GetTracker(providerId, organizationId)));
|
||||
|
||||
@ -99,12 +120,10 @@ public class ProviderMigrator(
|
||||
|
||||
#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);
|
||||
|
||||
var organizations = await GetEnabledClientsAsync(providerId);
|
||||
|
||||
var organizationIds = organizations.Select(organization => organization.Id);
|
||||
|
||||
await migrationTrackerCache.SetOrganizationIds(providerId, organizationIds);
|
||||
@ -129,7 +148,7 @@ public class ProviderMigrator(
|
||||
{
|
||||
logger.LogInformation("CB: Configuring Teams plan for provider ({ProviderID})", providerId);
|
||||
|
||||
var organizations = await GetEnabledClientsAsync(providerId);
|
||||
var organizations = await GetClientsAsync(providerId);
|
||||
|
||||
var teamsSeats = organizations
|
||||
.Where(IsTeams)
|
||||
@ -172,7 +191,7 @@ public class ProviderMigrator(
|
||||
{
|
||||
logger.LogInformation("CB: Configuring Enterprise plan for provider ({ProviderID})", providerId);
|
||||
|
||||
var organizations = await GetEnabledClientsAsync(providerId);
|
||||
var organizations = await GetClientsAsync(providerId);
|
||||
|
||||
var enterpriseSeats = organizations
|
||||
.Where(IsEnterprise)
|
||||
@ -215,7 +234,7 @@ public class ProviderMigrator(
|
||||
{
|
||||
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));
|
||||
|
||||
@ -299,28 +318,43 @@ public class ProviderMigrator(
|
||||
|
||||
private async Task ApplyCreditAsync(Provider provider)
|
||||
{
|
||||
var organizations = await GetEnabledClientsAsync(provider.Id);
|
||||
var organizations = await GetClientsAsync(provider.Id);
|
||||
|
||||
var organizationCustomers =
|
||||
await Task.WhenAll(organizations.Select(organization => stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId)));
|
||||
|
||||
var organizationCancellationCredit = organizationCustomers.Sum(customer => customer.Balance);
|
||||
|
||||
var legacyOrganizations = organizations.Where(organization =>
|
||||
organization.PlanType is
|
||||
await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId,
|
||||
new CustomerBalanceTransactionCreateOptions
|
||||
{
|
||||
Amount = organizationCancellationCredit,
|
||||
Currency = "USD",
|
||||
Description = "Unused, prorated time for client organization subscriptions."
|
||||
});
|
||||
|
||||
var migrationRecords = await Task.WhenAll(organizations.Select(organization =>
|
||||
clientOrganizationMigrationRecordRepository.GetByOrganizationId(organization.Id)));
|
||||
|
||||
var legacyOrganizationMigrationRecords = migrationRecords.Where(migrationRecord =>
|
||||
migrationRecord.PlanType is
|
||||
PlanType.EnterpriseAnnually2020 or
|
||||
PlanType.EnterpriseMonthly2020 or
|
||||
PlanType.TeamsAnnually2020 or
|
||||
PlanType.TeamsMonthly2020);
|
||||
PlanType.TeamsAnnually2020);
|
||||
|
||||
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);
|
||||
}
|
||||
@ -340,13 +374,12 @@ public class ProviderMigrator(
|
||||
|
||||
#region Utilities
|
||||
|
||||
private async Task<List<Organization>> GetEnabledClientsAsync(Guid providerId)
|
||||
private async Task<List<Organization>> GetClientsAsync(Guid providerId)
|
||||
{
|
||||
var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId);
|
||||
|
||||
return (await Task.WhenAll(providerOrganizations.Select(providerOrganization =>
|
||||
organizationRepository.GetByIdAsync(providerOrganization.OrganizationId))))
|
||||
.Where(organization => organization.Enabled)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,10 @@
|
||||
namespace Bit.Core.Billing.Models;
|
||||
|
||||
public record OrganizationMetadata(
|
||||
bool IsEligibleForSelfHost,
|
||||
bool IsOnSecretsManagerStandalone)
|
||||
{
|
||||
public static OrganizationMetadata Default() => new(
|
||||
IsOnSecretsManagerStandalone: default);
|
||||
IsEligibleForSelfHost: false,
|
||||
IsOnSecretsManagerStandalone: false);
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
using Bit.Core.AdminConsole.Entities;
|
||||
using Bit.Core.AdminConsole.Repositories;
|
||||
using Bit.Core.Billing.Caches;
|
||||
using Bit.Core.Billing.Constants;
|
||||
using Bit.Core.Billing.Models;
|
||||
@ -26,6 +27,7 @@ public class OrganizationBillingService(
|
||||
IGlobalSettings globalSettings,
|
||||
ILogger<OrganizationBillingService> logger,
|
||||
IOrganizationRepository organizationRepository,
|
||||
IProviderRepository providerRepository,
|
||||
ISetupIntentCache setupIntentCache,
|
||||
IStripeAdapter stripeAdapter,
|
||||
ISubscriberService subscriberService) : IOrganizationBillingService
|
||||
@ -69,14 +71,11 @@ public class OrganizationBillingService(
|
||||
|
||||
var subscription = await subscriberService.GetSubscription(organization);
|
||||
|
||||
if (customer == null || subscription == null)
|
||||
{
|
||||
return OrganizationMetadata.Default();
|
||||
}
|
||||
var isEligibleForSelfHost = await IsEligibleForSelfHost(organization, subscription);
|
||||
|
||||
var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription);
|
||||
|
||||
return new OrganizationMetadata(isOnSecretsManagerStandalone);
|
||||
return new OrganizationMetadata(isEligibleForSelfHost, isOnSecretsManagerStandalone);
|
||||
}
|
||||
|
||||
public async Task UpdatePaymentMethod(
|
||||
@ -340,11 +339,38 @@ public class OrganizationBillingService(
|
||||
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(
|
||||
Organization organization,
|
||||
Customer customer,
|
||||
Subscription subscription)
|
||||
Customer? customer,
|
||||
Subscription? subscription)
|
||||
{
|
||||
if (customer == null || subscription == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||
|
||||
if (!plan.SupportsSecretsManager)
|
||||
|
@ -146,6 +146,8 @@ public static class FeatureFlagKeys
|
||||
public const string RemoveServerVersionHeader = "remove-server-version-header";
|
||||
public const string AccessIntelligence = "pm-13227-access-intelligence";
|
||||
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()
|
||||
{
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models.Api;
|
||||
|
||||
@ -7,6 +6,4 @@ public class PushDeviceRequestModel
|
||||
{
|
||||
[Required]
|
||||
public string Id { get; set; }
|
||||
[Required]
|
||||
public DeviceType Type { get; set; }
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Bit.Core.Enums;
|
||||
|
||||
namespace Bit.Core.Models.Api;
|
||||
|
||||
@ -8,9 +7,9 @@ public class 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;
|
||||
}
|
||||
|
||||
|
@ -53,8 +53,11 @@ public class OrganizationLicense : ILicense
|
||||
UseSecretsManager = org.UseSecretsManager;
|
||||
SmSeats = org.SmSeats;
|
||||
SmServiceAccounts = org.SmServiceAccounts;
|
||||
|
||||
// Deprecated. Left for backwards compatibility with old license versions.
|
||||
LimitCollectionCreationDeletion = org.LimitCollectionCreationDeletion;
|
||||
AllowAdminAccessToAllCollectionItems = org.AllowAdminAccessToAllCollectionItems;
|
||||
//
|
||||
|
||||
if (subscriptionInfo?.Subscription == null)
|
||||
{
|
||||
@ -138,8 +141,12 @@ public class OrganizationLicense : ILicense
|
||||
public bool UseSecretsManager { get; set; }
|
||||
public int? SmSeats { get; set; }
|
||||
public int? SmServiceAccounts { get; set; }
|
||||
|
||||
// Deprecated. Left for backwards compatibility with old license versions.
|
||||
public bool LimitCollectionCreationDeletion { get; set; } = true;
|
||||
public bool AllowAdminAccessToAllCollectionItems { get; set; } = true;
|
||||
//
|
||||
|
||||
public bool Trial { get; set; }
|
||||
public LicenseType? LicenseType { 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.
|
||||
/// </summary>
|
||||
/// <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;
|
||||
private bool ValidLicenseVersion
|
||||
{
|
||||
@ -368,10 +376,11 @@ public class OrganizationLicense : ILicense
|
||||
}
|
||||
|
||||
/*
|
||||
* Version 14 added LimitCollectionCreationDeletion and Version 15 added AllowAdminAccessToAllCollectionItems,
|
||||
* however these are just user settings and it is not worth failing validation if they mismatch.
|
||||
* They are intentionally excluded.
|
||||
*/
|
||||
* Version 14 added LimitCollectionCreationDeletion and Version
|
||||
* 15 added AllowAdminAccessToAllCollectionItems, however they
|
||||
* are no longer used and are intentionally excluded from
|
||||
* validation.
|
||||
*/
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
@ -37,4 +37,25 @@ public class InstallationDeviceEntity : ITableEntity
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -49,11 +49,11 @@ public class MarkNotificationDeletedCommand : IMarkNotificationDeletedCommand
|
||||
|
||||
if (notificationStatus == null)
|
||||
{
|
||||
notificationStatus = new NotificationStatus()
|
||||
notificationStatus = new NotificationStatus
|
||||
{
|
||||
NotificationId = notificationId,
|
||||
UserId = _currentContext.UserId.Value,
|
||||
DeletedDate = DateTime.Now
|
||||
DeletedDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus,
|
||||
|
@ -49,11 +49,11 @@ public class MarkNotificationReadCommand : IMarkNotificationReadCommand
|
||||
|
||||
if (notificationStatus == null)
|
||||
{
|
||||
notificationStatus = new NotificationStatus()
|
||||
notificationStatus = new NotificationStatus
|
||||
{
|
||||
NotificationId = notificationId,
|
||||
UserId = _currentContext.UserId.Value,
|
||||
ReadDate = DateTime.Now
|
||||
ReadDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus,
|
||||
|
8
src/Core/NotificationHub/INotificationHubClientProxy.cs
Normal file
8
src/Core/NotificationHub/INotificationHubClientProxy.cs
Normal 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);
|
||||
}
|
9
src/Core/NotificationHub/INotificationHubPool.cs
Normal file
9
src/Core/NotificationHub/INotificationHubPool.cs
Normal file
@ -0,0 +1,9 @@
|
||||
using Microsoft.Azure.NotificationHubs;
|
||||
|
||||
namespace Bit.Core.NotificationHub;
|
||||
|
||||
public interface INotificationHubPool
|
||||
{
|
||||
NotificationHubClient ClientFor(Guid comb);
|
||||
INotificationHubProxy AllClients { get; }
|
||||
}
|
26
src/Core/NotificationHub/NotificationHubClientProxy.cs
Normal file
26
src/Core/NotificationHub/NotificationHubClientProxy.cs
Normal 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));
|
||||
}
|
||||
}
|
128
src/Core/NotificationHub/NotificationHubConnection.cs
Normal file
128
src/Core/NotificationHub/NotificationHubConnection.cs
Normal 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));
|
||||
}
|
||||
}
|
62
src/Core/NotificationHub/NotificationHubPool.cs
Normal file
62
src/Core/NotificationHub/NotificationHubPool.cs
Normal 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); } }
|
||||
}
|
@ -6,45 +6,31 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.Models;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Azure.NotificationHubs;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
namespace Bit.Core.NotificationHub;
|
||||
|
||||
public class NotificationHubPushNotificationService : IPushNotificationService
|
||||
{
|
||||
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly List<NotificationHubClient> _clients = [];
|
||||
private readonly bool _enableTracing = false;
|
||||
private readonly INotificationHubPool _notificationHubPool;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public NotificationHubPushNotificationService(
|
||||
IInstallationDeviceRepository installationDeviceRepository,
|
||||
GlobalSettings globalSettings,
|
||||
INotificationHubPool notificationHubPool,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<NotificationsApiPushNotificationService> logger)
|
||||
{
|
||||
_installationDeviceRepository = installationDeviceRepository;
|
||||
_globalSettings = globalSettings;
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
|
||||
foreach (var hub in globalSettings.NotificationHubs)
|
||||
{
|
||||
var client = NotificationHubClient.CreateClientFromConnectionString(
|
||||
hub.ConnectionString,
|
||||
hub.HubName,
|
||||
hub.EnableSendTracing);
|
||||
_clients.Add(client);
|
||||
|
||||
_enableTracing = _enableTracing || hub.EnableSendTracing;
|
||||
}
|
||||
|
||||
_notificationHubPool = notificationHubPool;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@ -264,30 +250,23 @@ public class NotificationHubPushNotificationService : IPushNotificationService
|
||||
|
||||
private async Task SendPayloadAsync(string tag, PushType type, object payload)
|
||||
{
|
||||
var tasks = new List<Task<NotificationOutcome>>();
|
||||
foreach (var client in _clients)
|
||||
{
|
||||
var task = client.SendTemplateNotificationAsync(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "type", ((byte)type).ToString() },
|
||||
{ "payload", JsonSerializer.Serialize(payload) }
|
||||
}, tag);
|
||||
tasks.Add(task);
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
var results = await _notificationHubPool.AllClients.SendTemplateNotificationAsync(
|
||||
new Dictionary<string, string>
|
||||
{
|
||||
{ "type", ((byte)type).ToString() },
|
||||
{ "payload", JsonSerializer.Serialize(payload) }
|
||||
}, tag);
|
||||
|
||||
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];
|
||||
_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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,50 +1,34 @@
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Models.Data;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Azure.NotificationHubs;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
namespace Bit.Core.NotificationHub;
|
||||
|
||||
public class NotificationHubPushRegistrationService : IPushRegistrationService
|
||||
{
|
||||
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
||||
private readonly GlobalSettings _globalSettings;
|
||||
private readonly INotificationHubPool _notificationHubPool;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<NotificationHubPushRegistrationService> _logger;
|
||||
private Dictionary<NotificationHubType, NotificationHubClient> _clients = [];
|
||||
|
||||
public NotificationHubPushRegistrationService(
|
||||
IInstallationDeviceRepository installationDeviceRepository,
|
||||
GlobalSettings globalSettings,
|
||||
INotificationHubPool notificationHubPool,
|
||||
IServiceProvider serviceProvider,
|
||||
ILogger<NotificationHubPushRegistrationService> logger)
|
||||
{
|
||||
_installationDeviceRepository = installationDeviceRepository;
|
||||
_globalSettings = globalSettings;
|
||||
_notificationHubPool = notificationHubPool;
|
||||
_serviceProvider = serviceProvider;
|
||||
_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,
|
||||
@ -117,7 +101,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
||||
BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate,
|
||||
userId, identifier);
|
||||
|
||||
await GetClient(type).CreateOrUpdateInstallationAsync(installation);
|
||||
await ClientFor(GetComb(deviceId)).CreateOrUpdateInstallationAsync(installation);
|
||||
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
||||
{
|
||||
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
|
||||
@ -152,11 +136,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
||||
installation.Templates.Add(fullTemplateId, template);
|
||||
}
|
||||
|
||||
public async Task DeleteRegistrationAsync(string deviceId, DeviceType deviceType)
|
||||
public async Task DeleteRegistrationAsync(string deviceId)
|
||||
{
|
||||
try
|
||||
{
|
||||
await GetClient(deviceType).DeleteInstallationAsync(deviceId);
|
||||
await ClientFor(GetComb(deviceId)).DeleteInstallationAsync(deviceId);
|
||||
if (InstallationDeviceEntity.IsInstallationDeviceId(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}");
|
||||
if (devices.Any() && InstallationDeviceEntity.IsInstallationDeviceId(devices.First().Key))
|
||||
await PatchTagsForUserDevicesAsync(deviceIds, UpdateOperationType.Add, $"organizationId:{organizationId}");
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
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}");
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
||||
private async Task PatchTagsForUserDevicesAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, UpdateOperationType op,
|
||||
private async Task PatchTagsForUserDevicesAsync(IEnumerable<string> deviceIds, UpdateOperationType op,
|
||||
string tag)
|
||||
{
|
||||
if (!devices.Any())
|
||||
if (!deviceIds.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
@ -212,11 +196,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
||||
operation.Path += $"/{tag}";
|
||||
}
|
||||
|
||||
foreach (var device in devices)
|
||||
foreach (var deviceId in deviceIds)
|
||||
{
|
||||
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"))
|
||||
{
|
||||
@ -225,53 +209,29 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
||||
}
|
||||
}
|
||||
|
||||
private NotificationHubClient GetClient(DeviceType deviceType)
|
||||
private NotificationHubClient ClientFor(Guid deviceId)
|
||||
{
|
||||
var hubType = NotificationHubType.General;
|
||||
switch (deviceType)
|
||||
return _notificationHubPool.ClientFor(deviceId);
|
||||
}
|
||||
|
||||
private Guid GetComb(string deviceId)
|
||||
{
|
||||
var deviceIdString = deviceId;
|
||||
InstallationDeviceEntity installationDeviceEntity;
|
||||
Guid deviceIdGuid;
|
||||
if (InstallationDeviceEntity.TryParse(deviceIdString, out installationDeviceEntity))
|
||||
{
|
||||
case DeviceType.Android:
|
||||
hubType = NotificationHubType.Android;
|
||||
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;
|
||||
// Strip off the installation id (PartitionId). RowKey is the ID in the Installation's table.
|
||||
deviceIdString = installationDeviceEntity.RowKey;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -17,15 +17,18 @@ public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseComman
|
||||
private readonly ILicensingService _licensingService;
|
||||
private readonly IGlobalSettings _globalSettings;
|
||||
private readonly IOrganizationService _organizationService;
|
||||
private readonly IFeatureService _featureService;
|
||||
|
||||
public UpdateOrganizationLicenseCommand(
|
||||
ILicensingService licensingService,
|
||||
IGlobalSettings globalSettings,
|
||||
IOrganizationService organizationService)
|
||||
IOrganizationService organizationService,
|
||||
IFeatureService featureService)
|
||||
{
|
||||
_licensingService = licensingService;
|
||||
_globalSettings = globalSettings;
|
||||
_organizationService = organizationService;
|
||||
_featureService = featureService;
|
||||
}
|
||||
|
||||
public async Task UpdateLicenseAsync(SelfHostedOrganizationDetails selfHostedOrganization,
|
||||
@ -59,7 +62,8 @@ public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseComman
|
||||
private async Task UpdateOrganizationAsync(SelfHostedOrganizationDetails selfHostedOrganizationDetails, OrganizationLicense license)
|
||||
{
|
||||
var organization = selfHostedOrganizationDetails.ToOrganization();
|
||||
organization.UpdateFromLicense(license);
|
||||
|
||||
organization.UpdateFromLicense(license, _featureService);
|
||||
|
||||
await _organizationService.ReplaceAndUpdateCacheAsync(organization);
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ public interface IPushRegistrationService
|
||||
{
|
||||
Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
|
||||
string identifier, DeviceType type);
|
||||
Task DeleteRegistrationAsync(string deviceId, DeviceType type);
|
||||
Task AddUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId);
|
||||
Task DeleteUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId);
|
||||
Task DeleteRegistrationAsync(string deviceId);
|
||||
Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId);
|
||||
Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId);
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ public interface IStripeAdapter
|
||||
Task<Stripe.Customer> CustomerUpdateAsync(string id, Stripe.CustomerUpdateOptions options = null);
|
||||
Task<Stripe.Customer> CustomerDeleteAsync(string id);
|
||||
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> SubscriptionGetAsync(string id, Stripe.SubscriptionGetOptions options = null);
|
||||
Task<List<Stripe.Subscription>> SubscriptionListAsync(StripeSubscriptionListOptions subscriptionSearchOptions);
|
||||
|
@ -38,13 +38,13 @@ public class DeviceService : IDeviceService
|
||||
public async Task ClearTokenAsync(Device device)
|
||||
{
|
||||
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)
|
||||
{
|
||||
await _deviceRepository.DeleteAsync(device);
|
||||
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString(), device.Type);
|
||||
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
|
||||
}
|
||||
|
||||
public async Task UpdateDevicesTrustAsync(string currentDeviceIdentifier,
|
||||
|
@ -1,61 +1,31 @@
|
||||
using Bit.Core.Auth.Entities;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Settings;
|
||||
using Bit.Core.Tools.Entities;
|
||||
using Bit.Core.Utilities;
|
||||
using Bit.Core.Vault.Entities;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Bit.Core.Services;
|
||||
|
||||
public class MultiServicePushNotificationService : IPushNotificationService
|
||||
{
|
||||
private readonly List<IPushNotificationService> _services = new List<IPushNotificationService>();
|
||||
private readonly IEnumerable<IPushNotificationService> _services;
|
||||
private readonly ILogger<MultiServicePushNotificationService> _logger;
|
||||
|
||||
public MultiServicePushNotificationService(
|
||||
IHttpClientFactory httpFactory,
|
||||
IDeviceRepository deviceRepository,
|
||||
IInstallationDeviceRepository installationDeviceRepository,
|
||||
GlobalSettings globalSettings,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
[FromKeyedServices("implementation")] IEnumerable<IPushNotificationService> services,
|
||||
ILogger<MultiServicePushNotificationService> logger,
|
||||
ILogger<RelayPushNotificationService> relayLogger,
|
||||
ILogger<NotificationsApiPushNotificationService> hubLogger)
|
||||
GlobalSettings globalSettings)
|
||||
{
|
||||
if (globalSettings.SelfHosted)
|
||||
{
|
||||
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));
|
||||
}
|
||||
}
|
||||
_services = services;
|
||||
|
||||
_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)
|
||||
|
@ -38,37 +38,36 @@ public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegi
|
||||
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
|
||||
{
|
||||
Id = deviceId,
|
||||
Type = type,
|
||||
};
|
||||
await SendAsync(HttpMethod.Post, "push/delete", requestModel);
|
||||
}
|
||||
|
||||
public async Task AddUserRegistrationOrganizationAsync(
|
||||
IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
||||
IEnumerable<string> deviceIds, string organizationId)
|
||||
{
|
||||
if (!devices.Any())
|
||||
if (!deviceIds.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var requestModel = new PushUpdateRequestModel(devices, organizationId);
|
||||
var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);
|
||||
await SendAsync(HttpMethod.Put, "push/add-organization", requestModel);
|
||||
}
|
||||
|
||||
public async Task DeleteUserRegistrationOrganizationAsync(
|
||||
IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
||||
IEnumerable<string> deviceIds, string organizationId)
|
||||
{
|
||||
if (!devices.Any())
|
||||
if (!deviceIds.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var requestModel = new PushUpdateRequestModel(devices, organizationId);
|
||||
var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);
|
||||
await SendAsync(HttpMethod.Put, "push/delete-organization", requestModel);
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ public class StripeAdapter : IStripeAdapter
|
||||
private readonly Stripe.PriceService _priceService;
|
||||
private readonly Stripe.SetupIntentService _setupIntentService;
|
||||
private readonly Stripe.TestHelpers.TestClockService _testClockService;
|
||||
private readonly CustomerBalanceTransactionService _customerBalanceTransactionService;
|
||||
|
||||
public StripeAdapter()
|
||||
{
|
||||
@ -34,6 +35,7 @@ public class StripeAdapter : IStripeAdapter
|
||||
_priceService = new Stripe.PriceService();
|
||||
_setupIntentService = new SetupIntentService();
|
||||
_testClockService = new Stripe.TestHelpers.TestClockService();
|
||||
_customerBalanceTransactionService = new CustomerBalanceTransactionService();
|
||||
}
|
||||
|
||||
public Task<Stripe.Customer> CustomerCreateAsync(Stripe.CustomerCreateOptions options)
|
||||
@ -63,6 +65,10 @@ public class StripeAdapter : IStripeAdapter
|
||||
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)
|
||||
{
|
||||
return _subscriptionService.CreateAsync(options);
|
||||
|
@ -4,7 +4,7 @@ namespace Bit.Core.Services;
|
||||
|
||||
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);
|
||||
}
|
||||
@ -15,12 +15,12 @@ public class NoopPushRegistrationService : IPushRegistrationService
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
|
||||
public Task DeleteRegistrationAsync(string deviceId, DeviceType deviceType)
|
||||
public Task DeleteRegistrationAsync(string deviceId)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
@ -1,5 +1,4 @@
|
||||
using Bit.Core.Auth.Settings;
|
||||
using Bit.Core.Enums;
|
||||
using Bit.Core.Settings.LoggingSettings;
|
||||
|
||||
namespace Bit.Core.Settings;
|
||||
@ -65,7 +64,7 @@ public class GlobalSettings : IGlobalSettings
|
||||
public virtual SentrySettings Sentry { get; set; } = new SentrySettings();
|
||||
public virtual SyslogSettings Syslog { get; set; } = new SyslogSettings();
|
||||
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 DuoSettings Duo { get; set; } = new DuoSettings();
|
||||
public virtual BraintreeSettings Braintree { get; set; } = new BraintreeSettings();
|
||||
@ -424,7 +423,7 @@ public class GlobalSettings : IGlobalSettings
|
||||
public string ConnectionString
|
||||
{
|
||||
get => _connectionString;
|
||||
set => _connectionString = value.Trim('"');
|
||||
set => _connectionString = value?.Trim('"');
|
||||
}
|
||||
public string HubName { get; set; }
|
||||
/// <summary>
|
||||
@ -433,10 +432,32 @@ public class GlobalSettings : IGlobalSettings
|
||||
/// </summary>
|
||||
public bool EnableSendTracing { get; set; } = false;
|
||||
/// <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>
|
||||
public bool EnableRegistration { get; set; }
|
||||
public NotificationHubType HubType { get; set; }
|
||||
public DateTime? RegistrationStartDate { 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
|
||||
|
@ -76,6 +76,39 @@ public static class CoreHelpers
|
||||
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)
|
||||
{
|
||||
// Clean possible garbage characters from thumbprint copy/paste
|
||||
|
@ -9,10 +9,6 @@ namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||
|
||||
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<OrganizationUser> OrganizationUsers { 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.Connections, 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();
|
||||
|
||||
CreateProjection<Organization, SelfHostedOrganizationDetails>()
|
||||
|
@ -99,6 +99,9 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
|
||||
UseScim = e.UseScim,
|
||||
UseCustomPermissions = e.UseCustomPermissions,
|
||||
UsePolicies = e.UsePolicies,
|
||||
LimitCollectionCreation = e.LimitCollectionCreation,
|
||||
LimitCollectionDeletion = e.LimitCollectionDeletion,
|
||||
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||
LimitCollectionCreationDeletion = e.LimitCollectionCreationDeletion,
|
||||
AllowAdminAccessToAllCollectionItems = e.AllowAdminAccessToAllCollectionItems
|
||||
}).ToListAsync();
|
||||
|
@ -66,6 +66,9 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery<OrganizationU
|
||||
UsePasswordManager = o.UsePasswordManager,
|
||||
SmSeats = o.SmSeats,
|
||||
SmServiceAccounts = o.SmServiceAccounts,
|
||||
LimitCollectionCreation = o.LimitCollectionCreation,
|
||||
LimitCollectionDeletion = o.LimitCollectionDeletion,
|
||||
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||
LimitCollectionCreationDeletion = o.LimitCollectionCreationDeletion,
|
||||
AllowAdminAccessToAllCollectionItems = o.AllowAdminAccessToAllCollectionItems,
|
||||
};
|
||||
|
@ -44,6 +44,9 @@ public class ProviderUserOrganizationDetailsViewQuery : IQuery<ProviderUserOrgan
|
||||
ProviderId = x.p.Id,
|
||||
ProviderName = x.p.Name,
|
||||
PlanType = x.o.PlanType,
|
||||
LimitCollectionCreation = x.o.LimitCollectionCreation,
|
||||
LimitCollectionDeletion = x.o.LimitCollectionDeletion,
|
||||
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||
LimitCollectionCreationDeletion = x.o.LimitCollectionCreationDeletion,
|
||||
AllowAdminAccessToAllCollectionItems = x.o.AllowAdminAccessToAllCollectionItems,
|
||||
});
|
||||
|
@ -4,6 +4,7 @@ using System.Security.Claims;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using AspNetCoreRateLimit;
|
||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||
using Bit.Core.AdminConsole.Services;
|
||||
using Bit.Core.AdminConsole.Services.Implementations;
|
||||
using Bit.Core.AdminConsole.Services.NoopImplementations;
|
||||
@ -24,6 +25,7 @@ using Bit.Core.Enums;
|
||||
using Bit.Core.HostedServices;
|
||||
using Bit.Core.Identity;
|
||||
using Bit.Core.IdentityServer;
|
||||
using Bit.Core.NotificationHub;
|
||||
using Bit.Core.OrganizationFeatures;
|
||||
using Bit.Core.Repositories;
|
||||
using Bit.Core.Resources;
|
||||
@ -102,9 +104,9 @@ public static class ServiceCollectionExtensions
|
||||
services.AddUserServices(globalSettings);
|
||||
services.AddTrialInitiationServices();
|
||||
services.AddOrganizationServices(globalSettings);
|
||||
services.AddPolicyServices();
|
||||
services.AddScoped<ICollectionService, CollectionService>();
|
||||
services.AddScoped<IGroupService, GroupService>();
|
||||
services.AddScoped<IPolicyService, PolicyService>();
|
||||
services.AddScoped<IEventService, EventService>();
|
||||
services.AddScoped<IEmergencyAccessService, EmergencyAccessService>();
|
||||
services.AddSingleton<IDeviceService, DeviceService>();
|
||||
@ -263,16 +265,30 @@ public static class ServiceCollectionExtensions
|
||||
}
|
||||
|
||||
services.AddSingleton<IPushNotificationService, MultiServicePushNotificationService>();
|
||||
if (globalSettings.SelfHosted &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) &&
|
||||
globalSettings.Installation?.Id != null &&
|
||||
CoreHelpers.SettingHasValue(globalSettings.Installation?.Key))
|
||||
if (globalSettings.SelfHosted)
|
||||
{
|
||||
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)
|
||||
{
|
||||
services.AddSingleton<INotificationHubPool, NotificationHubPool>();
|
||||
services.AddSingleton<IPushRegistrationService, NotificationHubPushRegistrationService>();
|
||||
services.AddKeyedSingleton<IPushNotificationService, NotificationHubPushNotificationService>("implementation");
|
||||
if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString))
|
||||
{
|
||||
services.AddKeyedSingleton<IPushNotificationService, AzureQueuePushNotificationService>("implementation");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -12,18 +12,28 @@ BEGIN
|
||||
[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].[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
|
||||
[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
|
||||
OU.[Status] = 2 -- Confirmed
|
||||
AND (
|
||||
CU.[CollectionId] IS NOT NULL
|
||||
OR CG.[CollectionId] IS NOT NULL
|
||||
)
|
||||
OU.[Status] = 2
|
||||
AND CU.[CollectionId] IS NULL
|
||||
END
|
||||
|
@ -229,13 +229,13 @@ public class OrganizationDomainControllerTests
|
||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||
.GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId)
|
||||
.Returns(organizationDomain);
|
||||
sutProvider.GetDependency<IVerifyOrganizationDomainCommand>().VerifyOrganizationDomainAsync(organizationDomain)
|
||||
sutProvider.GetDependency<IVerifyOrganizationDomainCommand>().UserVerifyOrganizationDomainAsync(organizationDomain)
|
||||
.Returns(new OrganizationDomain());
|
||||
|
||||
var result = await sutProvider.Sut.Verify(organizationDomain.OrganizationId, organizationDomain.Id);
|
||||
|
||||
await sutProvider.GetDependency<IVerifyOrganizationDomainCommand>().Received(1)
|
||||
.VerifyOrganizationDomainAsync(organizationDomain);
|
||||
.UserVerifyOrganizationDomainAsync(organizationDomain);
|
||||
Assert.IsType<OrganizationDomainResponseModel>(result);
|
||||
}
|
||||
|
||||
|
@ -52,7 +52,7 @@ public class OrganizationBillingControllerTests
|
||||
{
|
||||
sutProvider.GetDependency<ICurrentContext>().AccessMembersTab(organizationId).Returns(true);
|
||||
sutProvider.GetDependency<IOrganizationBillingService>().GetMetadata(organizationId)
|
||||
.Returns(new OrganizationMetadata(true));
|
||||
.Returns(new OrganizationMetadata(true, true));
|
||||
|
||||
var result = await sutProvider.Sut.GetMetadataAsync(organizationId);
|
||||
|
||||
@ -60,6 +60,7 @@ public class OrganizationBillingControllerTests
|
||||
|
||||
var organizationMetadataResponse = ((Ok<OrganizationMetadataResponse>)result).Value;
|
||||
|
||||
Assert.True(organizationMetadataResponse.IsEligibleForSelfHost);
|
||||
Assert.True(organizationMetadataResponse.IsOnSecretsManagerStandalone);
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
using System.Security.Claims;
|
||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||
using Bit.Core;
|
||||
using Bit.Core.Context;
|
||||
using Bit.Core.Entities;
|
||||
using Bit.Core.Enums;
|
||||
@ -32,7 +33,10 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
organization.Type = userType;
|
||||
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(
|
||||
new[] { BulkCollectionOperations.Create },
|
||||
@ -44,11 +48,12 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task CanCreateAsync_WhenUser_WithLimitCollectionCreationDeletionFalse_Success(
|
||||
public async Task CanCreateAsync_WhenUser_WithLimitCollectionCreationFalse_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<Collection> collections,
|
||||
CurrentContextOrganization organization)
|
||||
@ -57,7 +62,7 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
|
||||
organization.Type = OrganizationUserType.User;
|
||||
|
||||
ArrangeOrganizationAbility(sutProvider, organization, false);
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, false, false);
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { BulkCollectionOperations.Create },
|
||||
@ -66,16 +71,49 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
|
||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||
sutProvider.GetDependency<IFeatureService>()
|
||||
.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)
|
||||
.Returns(false);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
[Theory, CollectionCustomization]
|
||||
[BitAutoData(OrganizationUserType.User)]
|
||||
[BitAutoData(OrganizationUserType.Custom)]
|
||||
public async Task CanCreateAsync_WhenMissingPermissions_NoSuccess(
|
||||
public async Task CanCreateAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureDisabled_NoSuccess(
|
||||
OrganizationUserType userType,
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<Collection> collections,
|
||||
@ -92,7 +130,7 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
ManageUsers = false
|
||||
};
|
||||
|
||||
ArrangeOrganizationAbility(sutProvider, organization, true);
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true);
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { BulkCollectionOperations.Create },
|
||||
@ -102,21 +140,61 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
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(false);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task CanCreateAsync_WhenMissingOrgAccess_NoSuccess(
|
||||
public async Task CanCreateAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitDisabled_NoSuccess(
|
||||
Guid userId,
|
||||
CurrentContextOrganization organization,
|
||||
List<Collection> collections,
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider)
|
||||
{
|
||||
collections.ForEach(c => c.OrganizationId = organization.Id);
|
||||
ArrangeOrganizationAbility(sutProvider, organization, true);
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true);
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { BulkCollectionOperations.Create },
|
||||
@ -127,8 +205,38 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
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(false);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -904,7 +1012,10 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
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(
|
||||
new[] { BulkCollectionOperations.Delete },
|
||||
@ -916,6 +1027,7 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
@ -931,7 +1043,10 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
organization.Type = userType;
|
||||
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(
|
||||
new[] { BulkCollectionOperations.Delete },
|
||||
@ -943,11 +1058,12 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
|
||||
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
Assert.True(context.HasSucceeded);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task CanDeleteAsync_WhenUser_LimitCollectionCreationDeletionFalse_WithCanManagePermission_Success(
|
||||
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<CollectionDetails> collections,
|
||||
CurrentContextOrganization organization)
|
||||
@ -957,11 +1073,12 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
organization.Type = OrganizationUserType.User;
|
||||
organization.Permissions = new Permissions();
|
||||
|
||||
ArrangeOrganizationAbility(sutProvider, organization, false);
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(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(false);
|
||||
|
||||
foreach (var c in collections)
|
||||
{
|
||||
@ -975,6 +1092,41 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -982,7 +1134,7 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
[BitAutoData(OrganizationUserType.Admin)]
|
||||
[BitAutoData(OrganizationUserType.Owner)]
|
||||
[BitAutoData(OrganizationUserType.User)]
|
||||
public async Task CanDeleteAsync_LimitCollectionCreationDeletionFalse_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_Success(
|
||||
public async Task CanDeleteAsync_LimitCollectionDeletionFalse_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
|
||||
OrganizationUserType userType,
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<CollectionDetails> collections,
|
||||
@ -993,11 +1145,12 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
organization.Type = userType;
|
||||
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>().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)
|
||||
{
|
||||
@ -1011,13 +1164,15 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
|
||||
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_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_Success(
|
||||
[BitAutoData(OrganizationUserType.User)]
|
||||
public async Task CanDeleteAsync_LimitCollectionDeletionFalse_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success(
|
||||
OrganizationUserType userType,
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<CollectionDetails> collections,
|
||||
@ -1028,11 +1183,12 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
organization.Type = userType;
|
||||
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>().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)
|
||||
{
|
||||
@ -1046,13 +1202,14 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
|
||||
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_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithoutCanManagePermission_Failure(
|
||||
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
|
||||
OrganizationUserType userType,
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<CollectionDetails> collections,
|
||||
@ -1063,12 +1220,87 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
organization.Type = userType;
|
||||
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>().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)
|
||||
{
|
||||
@ -1082,11 +1314,50 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task CanDeleteAsync_WhenUser_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsTrue_Failure(
|
||||
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsTrue_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Failure(
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<CollectionDetails> collections,
|
||||
CurrentContextOrganization organization)
|
||||
@ -1096,12 +1367,13 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
organization.Type = OrganizationUserType.User;
|
||||
organization.Permissions = new Permissions();
|
||||
|
||||
ArrangeOrganizationAbility(sutProvider, organization, true);
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true);
|
||||
|
||||
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)
|
||||
{
|
||||
@ -1115,11 +1387,12 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
|
||||
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_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_Failure(
|
||||
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsTrue_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Failure(
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<CollectionDetails> collections,
|
||||
CurrentContextOrganization organization)
|
||||
@ -1129,12 +1402,13 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
organization.Type = OrganizationUserType.User;
|
||||
organization.Permissions = new Permissions();
|
||||
|
||||
ArrangeOrganizationAbility(sutProvider, organization, true, false);
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
|
||||
|
||||
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)
|
||||
{
|
||||
@ -1148,13 +1422,88 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
[Theory, CollectionCustomization]
|
||||
[BitAutoData(OrganizationUserType.User)]
|
||||
[BitAutoData(OrganizationUserType.Custom)]
|
||||
public async Task CanDeleteAsync_WhenMissingPermissions_NoSuccess(
|
||||
public async Task CanDeleteAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureDisabled_NoSuccess(
|
||||
OrganizationUserType userType,
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<Collection> collections,
|
||||
@ -1171,7 +1520,7 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
ManageUsers = false
|
||||
};
|
||||
|
||||
ArrangeOrganizationAbility(sutProvider, organization, true);
|
||||
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true);
|
||||
|
||||
var context = new AuthorizationHandlerContext(
|
||||
new[] { BulkCollectionOperations.Delete },
|
||||
@ -1181,14 +1530,54 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
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(false);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task CanDeleteAsync_WhenMissingOrgAccess_NoSuccess(
|
||||
public async Task CanDeleteAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitFeatureDisabled_NoSuccess(
|
||||
Guid userId,
|
||||
ICollection<Collection> collections,
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider)
|
||||
@ -1202,8 +1591,34 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
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(false);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -1224,6 +1639,7 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
await sutProvider.Sut.HandleAsync(context);
|
||||
Assert.True(context.HasFailed);
|
||||
sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs();
|
||||
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
@ -1247,10 +1663,11 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.HandleAsync(context));
|
||||
Assert.Equal("Requested collections must belong to the same organization.", exception.Message);
|
||||
sutProvider.GetDependency<ICurrentContext>().DidNotReceiveWithAnyArgs().GetOrganization(default);
|
||||
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData, CollectionCustomization]
|
||||
public async Task HandleRequirementAsync_Provider_Success(
|
||||
public async Task HandleRequirementAsync_Provider_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
ICollection<Collection> collections)
|
||||
{
|
||||
@ -1286,6 +1703,63 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync()
|
||||
.Returns(organizationAbilities);
|
||||
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(
|
||||
new[] { op },
|
||||
@ -1336,14 +1810,37 @@ public class BulkCollectionAuthorizationHandlerTests
|
||||
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdAsync(Arg.Any<Guid>());
|
||||
}
|
||||
|
||||
private static void ArrangeOrganizationAbility(
|
||||
private static void ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(
|
||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||
CurrentContextOrganization organization, bool limitCollectionCreationDeletion,
|
||||
CurrentContextOrganization organization,
|
||||
bool limitCollectionCreation,
|
||||
bool limitCollectionDeletion,
|
||||
bool allowAdminAccessToAllCollectionItems = true)
|
||||
{
|
||||
var organizationAbility = new OrganizationAbility();
|
||||
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;
|
||||
|
||||
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)
|
||||
|
@ -1,6 +1,5 @@
|
||||
using AutoFixture;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Org.BouncyCastle.Security;
|
||||
|
||||
namespace Bit.Test.Common.AutoFixture;
|
||||
|
||||
@ -15,7 +14,7 @@ public class ControllerCustomization : ICustomization
|
||||
{
|
||||
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;
|
||||
|
@ -127,7 +127,6 @@ public class SutProvider<TSut> : ISutProvider
|
||||
return _sutProvider.GetDependency(parameterInfo.ParameterType, "");
|
||||
}
|
||||
|
||||
|
||||
// This is the equivalent of _fixture.Create<parameterInfo.ParameterType>, but no overload for
|
||||
// Create(Type type) exists.
|
||||
var dependency = new SpecimenContext(_fixture).Resolve(new SeededRequest(parameterInfo.ParameterType,
|
||||
|
@ -1,6 +1,7 @@
|
||||
using AutoFixture;
|
||||
using Bit.Core.Services;
|
||||
using Bit.Core.Settings;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using NSubstitute;
|
||||
using RichardSzalay.MockHttp;
|
||||
|
||||
@ -47,4 +48,19 @@ public static class SutProviderExtensions
|
||||
.SetDependency(mockHttpClientFactory)
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
@ -5,6 +5,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="8.10.0" />
|
||||
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
|
||||
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)">
|
||||
|
@ -9,10 +9,12 @@ namespace Bit.Core.Test.AdminConsole.AutoFixture;
|
||||
internal class PolicyCustomization : ICustomization
|
||||
{
|
||||
public PolicyType Type { get; set; }
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
public PolicyCustomization(PolicyType type)
|
||||
public PolicyCustomization(PolicyType type, bool enabled)
|
||||
{
|
||||
Type = type;
|
||||
Enabled = enabled;
|
||||
}
|
||||
|
||||
public void Customize(IFixture fixture)
|
||||
@ -20,21 +22,23 @@ internal class PolicyCustomization : ICustomization
|
||||
fixture.Customize<Policy>(composer => composer
|
||||
.With(o => o.OrganizationId, Guid.NewGuid())
|
||||
.With(o => o.Type, Type)
|
||||
.With(o => o.Enabled, true));
|
||||
.With(o => o.Enabled, Enabled));
|
||||
}
|
||||
}
|
||||
|
||||
public class PolicyAttribute : CustomizeAttribute
|
||||
{
|
||||
private readonly PolicyType _type;
|
||||
private readonly bool _enabled;
|
||||
|
||||
public PolicyAttribute(PolicyType type)
|
||||
public PolicyAttribute(PolicyType type, bool enabled = true)
|
||||
{
|
||||
_type = type;
|
||||
_enabled = enabled;
|
||||
}
|
||||
|
||||
public override ICustomization GetCustomization(ParameterInfo parameter)
|
||||
{
|
||||
return new PolicyCustomization(_type);
|
||||
return new PolicyCustomization(_type, _enabled);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
||||
public class VerifyOrganizationDomainCommandTests
|
||||
{
|
||||
[Theory, BitAutoData]
|
||||
public async Task VerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimed(Guid id,
|
||||
public async Task UserVerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimed(Guid id,
|
||||
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
var expected = new OrganizationDomain
|
||||
@ -30,14 +30,14 @@ public class VerifyOrganizationDomainCommandTests
|
||||
.GetByIdAsync(id)
|
||||
.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);
|
||||
Assert.Contains("Domain has already been verified.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task VerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimedByAnotherOrganization(Guid id,
|
||||
public async Task UserVerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimedByAnotherOrganization(Guid id,
|
||||
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
var expected = new OrganizationDomain
|
||||
@ -54,14 +54,14 @@ public class VerifyOrganizationDomainCommandTests
|
||||
.GetClaimedDomainsByDomainNameAsync(expected.DomainName)
|
||||
.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);
|
||||
Assert.Contains("The domain is not available to be claimed.", exception.Message);
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task VerifyOrganizationDomain_ShouldVerifyDomainUpdateAndLogEvent_WhenTxtRecordExists(Guid id,
|
||||
public async Task UserVerifyOrganizationDomain_ShouldVerifyDomainUpdateAndLogEvent_WhenTxtRecordExists(Guid id,
|
||||
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
var expected = new OrganizationDomain
|
||||
@ -81,7 +81,7 @@ public class VerifyOrganizationDomainCommandTests
|
||||
.ResolveAsync(expected.DomainName, Arg.Any<string>())
|
||||
.Returns(true);
|
||||
|
||||
var result = await sutProvider.Sut.VerifyOrganizationDomainAsync(expected);
|
||||
var result = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(expected);
|
||||
|
||||
Assert.NotNull(result.VerifiedDate);
|
||||
await sutProvider.GetDependency<IOrganizationDomainRepository>().Received(1)
|
||||
@ -91,7 +91,7 @@ public class VerifyOrganizationDomainCommandTests
|
||||
}
|
||||
|
||||
[Theory, BitAutoData]
|
||||
public async Task VerifyOrganizationDomain_ShouldNotSetVerifiedDate_WhenTxtRecordDoesNotExist(Guid id,
|
||||
public async Task UserVerifyOrganizationDomain_ShouldNotSetVerifiedDate_WhenTxtRecordDoesNotExist(Guid id,
|
||||
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||
{
|
||||
var expected = new OrganizationDomain
|
||||
@ -111,10 +111,30 @@ public class VerifyOrganizationDomainCommandTests
|
||||
.ResolveAsync(expected.DomainName, Arg.Any<string>())
|
||||
.Returns(false);
|
||||
|
||||
var result = await sutProvider.Sut.VerifyOrganizationDomainAsync(expected);
|
||||
var result = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(expected);
|
||||
|
||||
Assert.Null(result.VerifiedDate);
|
||||
await sutProvider.GetDependency<IEventService>().Received(1)
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
@ -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
Loading…
Reference in New Issue
Block a user