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
|
# https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
|
||||||
|
|
||||||
# DevOps for Actions and other workflow changes
|
## Docker files have shared ownership ##
|
||||||
.github/workflows @bitwarden/dept-devops
|
**/Dockerfile
|
||||||
|
**/*.Dockerfile
|
||||||
|
**/.dockerignore
|
||||||
|
**/entrypoint.sh
|
||||||
|
|
||||||
# DevOps for Docker changes
|
## BRE team owns these workflows ##
|
||||||
**/Dockerfile @bitwarden/dept-devops
|
.github/workflows/publish.yml @bitwarden/dept-bre
|
||||||
**/*.Dockerfile @bitwarden/dept-devops
|
|
||||||
**/.dockerignore @bitwarden/dept-devops
|
## These are shared workflows ##
|
||||||
|
.github/workflows/_move_finalization_db_scripts.yml
|
||||||
|
.github/workflows/build.yml
|
||||||
|
.github/workflows/cleanup-after-pr.yml
|
||||||
|
.github/workflows/cleanup-rc-branch.yml
|
||||||
|
.github/workflows/release.yml
|
||||||
|
.github/workflows/repository-management.yml
|
||||||
|
|
||||||
# Database Operations for database changes
|
# Database Operations for database changes
|
||||||
src/Sql/** @bitwarden/dept-dbops
|
src/Sql/** @bitwarden/dept-dbops
|
||||||
@ -60,6 +69,6 @@ src/EventsProcessor @bitwarden/team-admin-console-dev
|
|||||||
src/Admin/Controllers/ToolsController.cs @bitwarden/team-billing-dev
|
src/Admin/Controllers/ToolsController.cs @bitwarden/team-billing-dev
|
||||||
src/Admin/Views/Tools @bitwarden/team-billing-dev
|
src/Admin/Views/Tools @bitwarden/team-billing-dev
|
||||||
|
|
||||||
# Multiple owners - DO NOT REMOVE (DevOps)
|
# Multiple owners - DO NOT REMOVE (BRE)
|
||||||
**/packages.lock.json
|
**/packages.lock.json
|
||||||
Directory.Build.props
|
Directory.Build.props
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
---
|
|
||||||
name: _move_finalization_db_scripts
|
name: _move_finalization_db_scripts
|
||||||
run-name: Move finalization database scripts
|
run-name: Move finalization database scripts
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
---
|
|
||||||
name: Automatic responses
|
name: Automatic responses
|
||||||
on:
|
on:
|
||||||
issues:
|
issues:
|
||||||
|
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@ -1,4 +1,3 @@
|
|||||||
---
|
|
||||||
name: Build
|
name: Build
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@ -595,7 +594,7 @@ jobs:
|
|||||||
workflow_id: '_update_ephemeral_tags.yml',
|
workflow_id: '_update_ephemeral_tags.yml',
|
||||||
ref: 'main',
|
ref: 'main',
|
||||||
inputs: {
|
inputs: {
|
||||||
ephemeral_env_branch: '${{ github.head_ref }}'
|
ephemeral_env_branch: process.env.GITHUB_HEAD_REF
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
1
.github/workflows/cleanup-after-pr.yml
vendored
1
.github/workflows/cleanup-after-pr.yml
vendored
@ -1,4 +1,3 @@
|
|||||||
---
|
|
||||||
name: Container registry cleanup
|
name: Container registry cleanup
|
||||||
|
|
||||||
on:
|
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
|
name: Cleanup RC Branch
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
7
.github/workflows/enforce-labels.yml
vendored
7
.github/workflows/enforce-labels.yml
vendored
@ -1,4 +1,3 @@
|
|||||||
---
|
|
||||||
name: Enforce PR labels
|
name: Enforce PR labels
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@ -7,13 +6,13 @@ on:
|
|||||||
types: [labeled, unlabeled, opened, reopened, synchronize]
|
types: [labeled, unlabeled, opened, reopened, synchronize]
|
||||||
jobs:
|
jobs:
|
||||||
enforce-label:
|
enforce-label:
|
||||||
if: ${{ contains(github.event.*.labels.*.name, 'hold') || contains(github.event.*.labels.*.name, 'needs-qa') || contains(github.event.*.labels.*.name, 'DB-migrations-changed') }}
|
if: ${{ contains(github.event.*.labels.*.name, 'hold') || contains(github.event.*.labels.*.name, 'needs-qa') || contains(github.event.*.labels.*.name, 'DB-migrations-changed') || contains(github.event.*.labels.*.name, 'ephemeral-environment') }}
|
||||||
name: Enforce label
|
name: Enforce label
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check for label
|
- name: Check for label
|
||||||
run: |
|
run: |
|
||||||
echo "PRs with the hold or needs-qa labels cannot be merged"
|
echo "PRs with the hold, needs-qa or ephemeral-environment labels cannot be merged"
|
||||||
echo "### :x: PRs with the hold or needs-qa labels cannot be merged" >> $GITHUB_STEP_SUMMARY
|
echo "### :x: PRs with the hold, needs-qa or ephemeral-environment labels cannot be merged" >> $GITHUB_STEP_SUMMARY
|
||||||
exit 1
|
exit 1
|
||||||
|
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.
|
# Runs if there are changes to the paths: list.
|
||||||
# Starts a matrix job to check for modified files, then sets output based on the results.
|
# Starts a matrix job to check for modified files, then sets output based on the results.
|
||||||
# The input decides if the label job is ran, adding a label to the PR.
|
# The input decides if the label job is ran, adding a label to the PR.
|
||||||
---
|
|
||||||
name: Protect files
|
name: Protect files
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
1
.github/workflows/publish.yml
vendored
1
.github/workflows/publish.yml
vendored
@ -1,4 +1,3 @@
|
|||||||
---
|
|
||||||
name: Publish
|
name: Publish
|
||||||
run-name: Publish ${{ inputs.publish_type }}
|
run-name: Publish ${{ inputs.publish_type }}
|
||||||
|
|
||||||
|
1
.github/workflows/release.yml
vendored
1
.github/workflows/release.yml
vendored
@ -1,4 +1,3 @@
|
|||||||
---
|
|
||||||
name: Release
|
name: Release
|
||||||
run-name: Release ${{ inputs.release_type }}
|
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
|
name: Staleness
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
1
.github/workflows/test-database.yml
vendored
1
.github/workflows/test-database.yml
vendored
@ -1,4 +1,3 @@
|
|||||||
---
|
|
||||||
name: Database testing
|
name: Database testing
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
|
||||||
<Version>2024.10.0</Version>
|
<Version>2024.10.1</Version>
|
||||||
|
|
||||||
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
<RootNamespace>Bit.$(MSBuildProjectName)</RootNamespace>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
@ -11,7 +11,6 @@ using Bit.Core.Billing.Extensions;
|
|||||||
using Bit.Core.Billing.Services;
|
using Bit.Core.Billing.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
|
||||||
using Bit.Core.Models.OrganizationConnectionConfigs;
|
using Bit.Core.Models.OrganizationConnectionConfigs;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationSponsorships.FamiliesForEnterprise.Interfaces;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@ -236,7 +235,8 @@ public class OrganizationsController : Controller
|
|||||||
if (organization.UseSecretsManager &&
|
if (organization.UseSecretsManager &&
|
||||||
!StaticStore.GetPlan(organization.PlanType).SupportsSecretsManager)
|
!StaticStore.GetPlan(organization.PlanType).SupportsSecretsManager)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Plan does not support Secrets Manager");
|
TempData["Error"] = "Plan does not support Secrets Manager";
|
||||||
|
return RedirectToAction("Edit", new { id });
|
||||||
}
|
}
|
||||||
|
|
||||||
await _organizationRepository.ReplaceAsync(organization);
|
await _organizationRepository.ReplaceAsync(organization);
|
||||||
|
@ -181,7 +181,6 @@ public class OrganizationEditModel : OrganizationViewModel
|
|||||||
*/
|
*/
|
||||||
public object GetPlansHelper() =>
|
public object GetPlansHelper() =>
|
||||||
StaticStore.Plans
|
StaticStore.Plans
|
||||||
.Where(p => p.SupportsSecretsManager)
|
|
||||||
.Select(p =>
|
.Select(p =>
|
||||||
{
|
{
|
||||||
var plan = new
|
var plan = new
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
@model OrganizationViewModel
|
@inject Bit.Core.Services.IFeatureService FeatureService
|
||||||
|
@model OrganizationViewModel
|
||||||
|
|
||||||
<dl class="row">
|
<dl class="row">
|
||||||
<dt class="col-sm-4 col-lg-3">Id</dt>
|
<dt class="col-sm-4 col-lg-3">Id</dt>
|
||||||
<dd id="org-id" class="col-sm-8 col-lg-9"><code>@Model.Organization.Id</code></dd>
|
<dd id="org-id" class="col-sm-8 col-lg-9"><code>@Model.Organization.Id</code></dd>
|
||||||
@ -53,8 +55,19 @@
|
|||||||
<dt class="col-sm-4 col-lg-3">Administrators manage all collections</dt>
|
<dt class="col-sm-4 col-lg-3">Administrators manage all collections</dt>
|
||||||
<dd id="pm-manage-collections" class="col-sm-8 col-lg-9">@(Model.Organization.AllowAdminAccessToAllCollectionItems ? "On" : "Off")</dd>
|
<dd id="pm-manage-collections" class="col-sm-8 col-lg-9">@(Model.Organization.AllowAdminAccessToAllCollectionItems ? "On" : "Off")</dd>
|
||||||
|
|
||||||
<dt class="col-sm-4 col-lg-3">Limit collection creation to administrators</dt>
|
@if (!FeatureService.IsEnabled(Bit.Core.FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
|
||||||
<dd id="pm-collection-creation" class="col-sm-8 col-lg-9">@(Model.Organization.LimitCollectionCreationDeletion ? "On" : "Off")</dd>
|
{
|
||||||
|
<dt class="col-sm-4 col-lg-3">Limit collection creation to administrators</dt>
|
||||||
|
<dd id="pm-collection-creation" class="col-sm-8 col-lg-9">@(Model.Organization.LimitCollectionCreationDeletion ? "On" : "Off")</dd>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<dt class="col-sm-4 col-lg-3">Limit collection creation to administrators</dt>
|
||||||
|
<dd id="pm-collection-creation" class="col-sm-8 col-lg-9">@(Model.Organization.LimitCollectionCreation ? "On" : "Off")</dd>
|
||||||
|
|
||||||
|
<dt class="col-sm-4 col-lg-3">Limit collection deletion to administrators</dt>
|
||||||
|
<dd id="pm-collection-deletion" class="col-sm-8 col-lg-9">@(Model.Organization.LimitCollectionDeletion ? "On" : "Off")</dd>
|
||||||
|
}
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<h2>Secrets Manager</h2>
|
<h2>Secrets Manager</h2>
|
||||||
|
@ -101,7 +101,7 @@ public class OrganizationDomainController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
organizationDomain = await _verifyOrganizationDomainCommand.VerifyOrganizationDomainAsync(organizationDomain);
|
organizationDomain = await _verifyOrganizationDomainCommand.UserVerifyOrganizationDomainAsync(organizationDomain);
|
||||||
|
|
||||||
return new OrganizationDomainResponseModel(organizationDomain);
|
return new OrganizationDomainResponseModel(organizationDomain);
|
||||||
}
|
}
|
||||||
|
@ -520,9 +520,16 @@ public class OrganizationsController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("{id}/collection-management")]
|
[HttpPut("{id}/collection-management")]
|
||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
|
||||||
public async Task<OrganizationResponseModel> PutCollectionManagement(Guid id, [FromBody] OrganizationCollectionManagementUpdateRequestModel model)
|
public async Task<OrganizationResponseModel> PutCollectionManagement(Guid id, [FromBody] OrganizationCollectionManagementUpdateRequestModel model)
|
||||||
{
|
{
|
||||||
|
if (
|
||||||
|
_globalSettings.SelfHosted &&
|
||||||
|
!_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Only allowed when not self hosted.");
|
||||||
|
}
|
||||||
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(id);
|
var organization = await _organizationRepository.GetByIdAsync(id);
|
||||||
if (organization == null)
|
if (organization == null)
|
||||||
{
|
{
|
||||||
@ -534,7 +541,7 @@ public class OrganizationsController : Controller
|
|||||||
throw new NotFoundException();
|
throw new NotFoundException();
|
||||||
}
|
}
|
||||||
|
|
||||||
await _organizationService.UpdateAsync(model.ToOrganization(organization), eventType: EventType.Organization_CollectionManagement_Updated);
|
await _organizationService.UpdateAsync(model.ToOrganization(organization, _featureService), eventType: EventType.Organization_CollectionManagement_Updated);
|
||||||
return new OrganizationResponseModel(organization);
|
return new OrganizationResponseModel(organization);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,6 @@ public class PoliciesController : Controller
|
|||||||
{
|
{
|
||||||
private readonly IPolicyRepository _policyRepository;
|
private readonly IPolicyRepository _policyRepository;
|
||||||
private readonly IPolicyService _policyService;
|
private readonly IPolicyService _policyService;
|
||||||
private readonly IOrganizationService _organizationService;
|
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IUserService _userService;
|
private readonly IUserService _userService;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
@ -36,7 +35,6 @@ public class PoliciesController : Controller
|
|||||||
public PoliciesController(
|
public PoliciesController(
|
||||||
IPolicyRepository policyRepository,
|
IPolicyRepository policyRepository,
|
||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
IOrganizationService organizationService,
|
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IUserService userService,
|
IUserService userService,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
@ -46,7 +44,6 @@ public class PoliciesController : Controller
|
|||||||
{
|
{
|
||||||
_policyRepository = policyRepository;
|
_policyRepository = policyRepository;
|
||||||
_policyService = policyService;
|
_policyService = policyService;
|
||||||
_organizationService = organizationService;
|
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_userService = userService;
|
_userService = userService;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
@ -185,7 +182,7 @@ public class PoliciesController : Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
var userId = _userService.GetProperUserId(User);
|
var userId = _userService.GetProperUserId(User);
|
||||||
await _policyService.SaveAsync(policy, _organizationService, userId);
|
await _policyService.SaveAsync(policy, userId);
|
||||||
return new PolicyResponseModel(policy);
|
return new PolicyResponseModel(policy);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,6 +55,9 @@ public class OrganizationResponseModel : ResponseModel
|
|||||||
SmServiceAccounts = organization.SmServiceAccounts;
|
SmServiceAccounts = organization.SmServiceAccounts;
|
||||||
MaxAutoscaleSmSeats = organization.MaxAutoscaleSmSeats;
|
MaxAutoscaleSmSeats = organization.MaxAutoscaleSmSeats;
|
||||||
MaxAutoscaleSmServiceAccounts = organization.MaxAutoscaleSmServiceAccounts;
|
MaxAutoscaleSmServiceAccounts = organization.MaxAutoscaleSmServiceAccounts;
|
||||||
|
LimitCollectionCreation = organization.LimitCollectionCreation;
|
||||||
|
LimitCollectionDeletion = organization.LimitCollectionDeletion;
|
||||||
|
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||||
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
|
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
|
||||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||||
}
|
}
|
||||||
@ -98,6 +101,9 @@ public class OrganizationResponseModel : ResponseModel
|
|||||||
public int? SmServiceAccounts { get; set; }
|
public int? SmServiceAccounts { get; set; }
|
||||||
public int? MaxAutoscaleSmSeats { get; set; }
|
public int? MaxAutoscaleSmSeats { get; set; }
|
||||||
public int? MaxAutoscaleSmServiceAccounts { get; set; }
|
public int? MaxAutoscaleSmServiceAccounts { get; set; }
|
||||||
|
public bool LimitCollectionCreation { get; set; }
|
||||||
|
public bool LimitCollectionDeletion { get; set; }
|
||||||
|
// Deperectated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||||
public bool LimitCollectionCreationDeletion { get; set; }
|
public bool LimitCollectionCreationDeletion { get; set; }
|
||||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -65,6 +65,9 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
|||||||
FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete;
|
FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete;
|
||||||
FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil;
|
FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil;
|
||||||
AccessSecretsManager = organization.AccessSecretsManager;
|
AccessSecretsManager = organization.AccessSecretsManager;
|
||||||
|
LimitCollectionCreation = organization.LimitCollectionCreation;
|
||||||
|
LimitCollectionDeletion = organization.LimitCollectionDeletion;
|
||||||
|
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||||
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
|
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
|
||||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||||
UserIsManagedByOrganization = organizationIdsManagingUser.Contains(organization.OrganizationId);
|
UserIsManagedByOrganization = organizationIdsManagingUser.Contains(organization.OrganizationId);
|
||||||
@ -124,6 +127,9 @@ public class ProfileOrganizationResponseModel : ResponseModel
|
|||||||
public DateTime? FamilySponsorshipValidUntil { get; set; }
|
public DateTime? FamilySponsorshipValidUntil { get; set; }
|
||||||
public bool? FamilySponsorshipToDelete { get; set; }
|
public bool? FamilySponsorshipToDelete { get; set; }
|
||||||
public bool AccessSecretsManager { get; set; }
|
public bool AccessSecretsManager { get; set; }
|
||||||
|
public bool LimitCollectionCreation { get; set; }
|
||||||
|
public bool LimitCollectionDeletion { get; set; }
|
||||||
|
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||||
public bool LimitCollectionCreationDeletion { get; set; }
|
public bool LimitCollectionCreationDeletion { get; set; }
|
||||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -44,6 +44,9 @@ public class ProfileProviderOrganizationResponseModel : ProfileOrganizationRespo
|
|||||||
ProviderId = organization.ProviderId;
|
ProviderId = organization.ProviderId;
|
||||||
ProviderName = organization.ProviderName;
|
ProviderName = organization.ProviderName;
|
||||||
ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier;
|
ProductTierType = StaticStore.GetPlan(organization.PlanType).ProductTier;
|
||||||
|
LimitCollectionCreation = organization.LimitCollectionCreation;
|
||||||
|
LimitCollectionDeletion = organization.LimitCollectionDeletion;
|
||||||
|
// https://bitwarden.atlassian.net/browse/PM-10863
|
||||||
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
|
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
|
||||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ using Bit.Core.AdminConsole.Enums;
|
|||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Services;
|
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
|
||||||
@ -18,18 +17,15 @@ public class PoliciesController : Controller
|
|||||||
{
|
{
|
||||||
private readonly IPolicyRepository _policyRepository;
|
private readonly IPolicyRepository _policyRepository;
|
||||||
private readonly IPolicyService _policyService;
|
private readonly IPolicyService _policyService;
|
||||||
private readonly IOrganizationService _organizationService;
|
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
|
|
||||||
public PoliciesController(
|
public PoliciesController(
|
||||||
IPolicyRepository policyRepository,
|
IPolicyRepository policyRepository,
|
||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
IOrganizationService organizationService,
|
|
||||||
ICurrentContext currentContext)
|
ICurrentContext currentContext)
|
||||||
{
|
{
|
||||||
_policyRepository = policyRepository;
|
_policyRepository = policyRepository;
|
||||||
_policyService = policyService;
|
_policyService = policyService;
|
||||||
_organizationService = organizationService;
|
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -96,7 +92,7 @@ public class PoliciesController : Controller
|
|||||||
{
|
{
|
||||||
policy = model.ToPolicy(policy);
|
policy = model.ToPolicy(policy);
|
||||||
}
|
}
|
||||||
await _policyService.SaveAsync(policy, _organizationService, null);
|
await _policyService.SaveAsync(policy, null);
|
||||||
var response = new PolicyResponseModel(policy);
|
var response = new PolicyResponseModel(policy);
|
||||||
return new JsonResult(response);
|
return new JsonResult(response);
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,11 @@
|
|||||||
namespace Bit.Api.Billing.Models.Responses;
|
namespace Bit.Api.Billing.Models.Responses;
|
||||||
|
|
||||||
public record OrganizationMetadataResponse(
|
public record OrganizationMetadataResponse(
|
||||||
|
bool IsEligibleForSelfHost,
|
||||||
bool IsOnSecretsManagerStandalone)
|
bool IsOnSecretsManagerStandalone)
|
||||||
{
|
{
|
||||||
public static OrganizationMetadataResponse From(OrganizationMetadata metadata)
|
public static OrganizationMetadataResponse From(OrganizationMetadata metadata)
|
||||||
=> new(metadata.IsOnSecretsManagerStandalone);
|
=> new(
|
||||||
|
metadata.IsEligibleForSelfHost,
|
||||||
|
metadata.IsOnSecretsManagerStandalone);
|
||||||
}
|
}
|
||||||
|
@ -46,7 +46,7 @@ public class PushController : Controller
|
|||||||
public async Task PostDelete([FromBody] PushDeviceRequestModel model)
|
public async Task PostDelete([FromBody] PushDeviceRequestModel model)
|
||||||
{
|
{
|
||||||
CheckUsage();
|
CheckUsage();
|
||||||
await _pushRegistrationService.DeleteRegistrationAsync(Prefix(model.Id), model.Type);
|
await _pushRegistrationService.DeleteRegistrationAsync(Prefix(model.Id));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPut("add-organization")]
|
[HttpPut("add-organization")]
|
||||||
@ -54,7 +54,7 @@ public class PushController : Controller
|
|||||||
{
|
{
|
||||||
CheckUsage();
|
CheckUsage();
|
||||||
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(
|
await _pushRegistrationService.AddUserRegistrationOrganizationAsync(
|
||||||
model.Devices.Select(d => new KeyValuePair<string, Core.Enums.DeviceType>(Prefix(d.Id), d.Type)),
|
model.Devices.Select(d => Prefix(d.Id)),
|
||||||
Prefix(model.OrganizationId));
|
Prefix(model.OrganizationId));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +63,7 @@ public class PushController : Controller
|
|||||||
{
|
{
|
||||||
CheckUsage();
|
CheckUsage();
|
||||||
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(
|
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(
|
||||||
model.Devices.Select(d => new KeyValuePair<string, Core.Enums.DeviceType>(Prefix(d.Id), d.Type)),
|
model.Devices.Select(d => Prefix(d.Id)),
|
||||||
Prefix(model.OrganizationId));
|
Prefix(model.OrganizationId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,29 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core;
|
||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
|
||||||
namespace Bit.Api.Models.Request.Organizations;
|
namespace Bit.Api.Models.Request.Organizations;
|
||||||
|
|
||||||
public class OrganizationCollectionManagementUpdateRequestModel
|
public class OrganizationCollectionManagementUpdateRequestModel
|
||||||
{
|
{
|
||||||
|
public bool LimitCollectionCreation { get; set; }
|
||||||
|
public bool LimitCollectionDeletion { get; set; }
|
||||||
|
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||||
public bool LimitCreateDeleteOwnerAdmin { get; set; }
|
public bool LimitCreateDeleteOwnerAdmin { get; set; }
|
||||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||||
|
|
||||||
public virtual Organization ToOrganization(Organization existingOrganization)
|
public virtual Organization ToOrganization(Organization existingOrganization, IFeatureService featureService)
|
||||||
{
|
{
|
||||||
existingOrganization.LimitCollectionCreationDeletion = LimitCreateDeleteOwnerAdmin;
|
if (featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
|
||||||
|
{
|
||||||
|
existingOrganization.LimitCollectionCreation = LimitCollectionCreation;
|
||||||
|
existingOrganization.LimitCollectionDeletion = LimitCollectionDeletion;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existingOrganization.LimitCollectionCreationDeletion = LimitCreateDeleteOwnerAdmin || LimitCollectionCreation || LimitCollectionDeletion;
|
||||||
|
}
|
||||||
|
|
||||||
existingOrganization.AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems;
|
existingOrganization.AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems;
|
||||||
return existingOrganization;
|
return existingOrganization;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
#nullable enable
|
#nullable enable
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -123,10 +124,24 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the limit collection management setting is disabled, allow any user to create collections
|
if (_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
|
||||||
if (await GetOrganizationAbilityAsync(org) is { LimitCollectionCreationDeletion: false })
|
|
||||||
{
|
{
|
||||||
return true;
|
var userIsMemberOfOrg = org is not null;
|
||||||
|
var limitCollectionCreationEnabled = await GetOrganizationAbilityAsync(org) is { LimitCollectionCreation: true };
|
||||||
|
var userIsOrgOwnerOrAdmin = org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin };
|
||||||
|
// If the limit collection management setting is disabled, allow any user to create collections
|
||||||
|
if (userIsMemberOfOrg && (!limitCollectionCreationEnabled || userIsOrgOwnerOrAdmin))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If the limit collection management setting is disabled, allow any user to create collections
|
||||||
|
if (await GetOrganizationAbilityAsync(org) is { LimitCollectionCreationDeletion: false })
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow provider users to create collections if they are a provider for the target organization
|
// Allow provider users to create collections if they are a provider for the target organization
|
||||||
@ -246,21 +261,35 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If AllowAdminAccessToAllCollectionItems is true, Owners and Admins can delete any collection, regardless of LimitCollectionCreationDeletion setting
|
// If AllowAdminAccessToAllCollectionItems is true, Owners and Admins can delete any collection, regardless of LimitCollectionDeletion setting
|
||||||
if (await AllowAdminAccessToAllCollectionItems(org) && org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin })
|
if (await AllowAdminAccessToAllCollectionItems(org) && org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin })
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If LimitCollectionCreationDeletion is false, AllowAdminAccessToAllCollectionItems setting is irrelevant.
|
if (_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
|
||||||
// Ensure acting user has manage permissions for all collections being deleted
|
|
||||||
// If LimitCollectionCreationDeletion is true, only Owners and Admins can delete collections they manage
|
|
||||||
var organizationAbility = await GetOrganizationAbilityAsync(org);
|
|
||||||
var canDeleteManagedCollections = organizationAbility is { LimitCollectionCreationDeletion: false } ||
|
|
||||||
org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin };
|
|
||||||
if (canDeleteManagedCollections && await CanManageCollectionsAsync(resources, org))
|
|
||||||
{
|
{
|
||||||
return true;
|
var userIsMemberOfOrg = org is not null;
|
||||||
|
var limitCollectionDeletionEnabled = await GetOrganizationAbilityAsync(org) is { LimitCollectionDeletion: true };
|
||||||
|
var userIsOrgOwnerOrAdmin = org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin };
|
||||||
|
// If the limit collection management setting is disabled, allow any user to delete collections
|
||||||
|
if (userIsMemberOfOrg && (!limitCollectionDeletionEnabled || userIsOrgOwnerOrAdmin) && await CanManageCollectionsAsync(resources, org))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If LimitCollectionCreationDeletion is false, AllowAdminAccessToAllCollectionItems setting is irrelevant.
|
||||||
|
// Ensure acting user has manage permissions for all collections being deleted
|
||||||
|
// If LimitCollectionCreationDeletion is true, only Owners and Admins can delete collections they manage
|
||||||
|
var organizationAbility = await GetOrganizationAbilityAsync(org);
|
||||||
|
var canDeleteManagedCollections = organizationAbility is { LimitCollectionCreationDeletion: false } ||
|
||||||
|
org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin };
|
||||||
|
if (canDeleteManagedCollections && await CanManageCollectionsAsync(resources, org))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allow providers to delete collections if they are a provider for the target organization
|
// Allow providers to delete collections if they are a provider for the target organization
|
||||||
|
@ -7,6 +7,7 @@ using Bit.Core.Billing.Enums;
|
|||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Business;
|
using Bit.Core.Models.Business;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tools.Entities;
|
using Bit.Core.Tools.Entities;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
@ -93,7 +94,20 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
|||||||
/// If set to false, any organization member can create a collection, and any member can delete a collection that
|
/// If set to false, any organization member can create a collection, and any member can delete a collection that
|
||||||
/// they have Can Manage permissions for.
|
/// they have Can Manage permissions for.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool LimitCollectionCreationDeletion { get; set; }
|
public bool LimitCollectionCreation { get; set; }
|
||||||
|
public bool LimitCollectionDeletion { get; set; }
|
||||||
|
// Deprecated by https://bitwarden.atlassian.net/browse/PM-10863. This
|
||||||
|
// was replaced with `LimitCollectionCreation` and
|
||||||
|
// `LimitCollectionDeletion`.
|
||||||
|
public bool LimitCollectionCreationDeletion
|
||||||
|
{
|
||||||
|
get => LimitCollectionCreation || LimitCollectionDeletion;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
LimitCollectionCreation = value;
|
||||||
|
LimitCollectionDeletion = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// If set to true, admins, owners, and some custom users can read/write all collections and items in the Admin Console.
|
/// If set to true, admins, owners, and some custom users can read/write all collections and items in the Admin Console.
|
||||||
@ -265,7 +279,7 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
|||||||
return providers[provider];
|
return providers[provider];
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateFromLicense(OrganizationLicense license)
|
public void UpdateFromLicense(OrganizationLicense license, IFeatureService featureService)
|
||||||
{
|
{
|
||||||
// The following properties are intentionally excluded from being updated:
|
// The following properties are intentionally excluded from being updated:
|
||||||
// - Id - self-hosted org will have its own unique Guid
|
// - Id - self-hosted org will have its own unique Guid
|
||||||
@ -300,7 +314,11 @@ public class Organization : ITableObject<Guid>, IStorableSubscriber, IRevisable,
|
|||||||
UseSecretsManager = license.UseSecretsManager;
|
UseSecretsManager = license.UseSecretsManager;
|
||||||
SmSeats = license.SmSeats;
|
SmSeats = license.SmSeats;
|
||||||
SmServiceAccounts = license.SmServiceAccounts;
|
SmServiceAccounts = license.SmServiceAccounts;
|
||||||
LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion;
|
|
||||||
AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems;
|
if (!featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
|
||||||
|
{
|
||||||
|
LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion;
|
||||||
|
AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,3 +16,30 @@ public enum PolicyType : byte
|
|||||||
ActivateAutofill = 11,
|
ActivateAutofill = 11,
|
||||||
AutomaticAppLogIn = 12,
|
AutomaticAppLogIn = 12,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class PolicyTypeExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the name of the policy for display to the user.
|
||||||
|
/// Do not include the word "policy" in the return value.
|
||||||
|
/// </summary>
|
||||||
|
public static string GetName(this PolicyType type)
|
||||||
|
{
|
||||||
|
return type switch
|
||||||
|
{
|
||||||
|
PolicyType.TwoFactorAuthentication => "Require two-step login",
|
||||||
|
PolicyType.MasterPassword => "Master password requirements",
|
||||||
|
PolicyType.PasswordGenerator => "Password generator",
|
||||||
|
PolicyType.SingleOrg => "Single organization",
|
||||||
|
PolicyType.RequireSso => "Require single sign-on authentication",
|
||||||
|
PolicyType.PersonalOwnership => "Remove individual vault",
|
||||||
|
PolicyType.DisableSend => "Remove Send",
|
||||||
|
PolicyType.SendOptions => "Send options",
|
||||||
|
PolicyType.ResetPassword => "Account recovery administration",
|
||||||
|
PolicyType.MaximumVaultTimeout => "Vault timeout",
|
||||||
|
PolicyType.DisablePersonalVaultExport => "Remove individual vault export",
|
||||||
|
PolicyType.ActivateAutofill => "Active auto-fill",
|
||||||
|
PolicyType.AutomaticAppLogIn => "Automatically log in users for allowed applications",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -21,6 +21,9 @@ public class OrganizationAbility
|
|||||||
UseResetPassword = organization.UseResetPassword;
|
UseResetPassword = organization.UseResetPassword;
|
||||||
UseCustomPermissions = organization.UseCustomPermissions;
|
UseCustomPermissions = organization.UseCustomPermissions;
|
||||||
UsePolicies = organization.UsePolicies;
|
UsePolicies = organization.UsePolicies;
|
||||||
|
LimitCollectionCreation = organization.LimitCollectionCreation;
|
||||||
|
LimitCollectionDeletion = organization.LimitCollectionDeletion;
|
||||||
|
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||||
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
|
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
|
||||||
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
|
||||||
}
|
}
|
||||||
@ -37,6 +40,9 @@ public class OrganizationAbility
|
|||||||
public bool UseResetPassword { get; set; }
|
public bool UseResetPassword { get; set; }
|
||||||
public bool UseCustomPermissions { get; set; }
|
public bool UseCustomPermissions { get; set; }
|
||||||
public bool UsePolicies { get; set; }
|
public bool UsePolicies { get; set; }
|
||||||
|
public bool LimitCollectionCreation { get; set; }
|
||||||
|
public bool LimitCollectionDeletion { get; set; }
|
||||||
|
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||||
public bool LimitCollectionCreationDeletion { get; set; }
|
public bool LimitCollectionCreationDeletion { get; set; }
|
||||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -54,6 +54,9 @@ public class OrganizationUserOrganizationDetails
|
|||||||
public bool UsePasswordManager { get; set; }
|
public bool UsePasswordManager { get; set; }
|
||||||
public int? SmSeats { get; set; }
|
public int? SmSeats { get; set; }
|
||||||
public int? SmServiceAccounts { get; set; }
|
public int? SmServiceAccounts { get; set; }
|
||||||
|
public bool LimitCollectionCreation { get; set; }
|
||||||
|
public bool LimitCollectionDeletion { get; set; }
|
||||||
|
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||||
public bool LimitCollectionCreationDeletion { get; set; }
|
public bool LimitCollectionCreationDeletion { get; set; }
|
||||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -144,6 +144,9 @@ public class SelfHostedOrganizationDetails : Organization
|
|||||||
RevisionDate = RevisionDate,
|
RevisionDate = RevisionDate,
|
||||||
MaxAutoscaleSeats = MaxAutoscaleSeats,
|
MaxAutoscaleSeats = MaxAutoscaleSeats,
|
||||||
OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling,
|
OwnersNotifiedOfAutoscaling = OwnersNotifiedOfAutoscaling,
|
||||||
|
LimitCollectionCreation = LimitCollectionCreation,
|
||||||
|
LimitCollectionDeletion = LimitCollectionDeletion,
|
||||||
|
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||||
LimitCollectionCreationDeletion = LimitCollectionCreationDeletion,
|
LimitCollectionCreationDeletion = LimitCollectionCreationDeletion,
|
||||||
AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems,
|
AllowAdminAccessToAllCollectionItems = AllowAdminAccessToAllCollectionItems,
|
||||||
Status = Status
|
Status = Status
|
||||||
|
@ -40,6 +40,8 @@ public class ProviderUserOrganizationDetails
|
|||||||
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
[JsonConverter(typeof(HtmlEncodingStringConverter))]
|
||||||
public string ProviderName { get; set; }
|
public string ProviderName { get; set; }
|
||||||
public PlanType PlanType { get; set; }
|
public PlanType PlanType { get; set; }
|
||||||
|
public bool LimitCollectionCreation { get; set; }
|
||||||
|
public bool LimitCollectionDeletion { get; set; }
|
||||||
public bool LimitCollectionCreationDeletion { get; set; }
|
public bool LimitCollectionCreationDeletion { get; set; }
|
||||||
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
public bool AllowAdminAccessToAllCollectionItems { get; set; }
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ using Bit.Core.Repositories;
|
|||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
||||||
|
|
||||||
@ -14,21 +13,15 @@ public class CreateOrganizationDomainCommand : ICreateOrganizationDomainCommand
|
|||||||
{
|
{
|
||||||
private readonly IOrganizationDomainRepository _organizationDomainRepository;
|
private readonly IOrganizationDomainRepository _organizationDomainRepository;
|
||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
private readonly IDnsResolverService _dnsResolverService;
|
|
||||||
private readonly ILogger<VerifyOrganizationDomainCommand> _logger;
|
|
||||||
private readonly IGlobalSettings _globalSettings;
|
private readonly IGlobalSettings _globalSettings;
|
||||||
|
|
||||||
public CreateOrganizationDomainCommand(
|
public CreateOrganizationDomainCommand(
|
||||||
IOrganizationDomainRepository organizationDomainRepository,
|
IOrganizationDomainRepository organizationDomainRepository,
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IDnsResolverService dnsResolverService,
|
|
||||||
ILogger<VerifyOrganizationDomainCommand> logger,
|
|
||||||
IGlobalSettings globalSettings)
|
IGlobalSettings globalSettings)
|
||||||
{
|
{
|
||||||
_organizationDomainRepository = organizationDomainRepository;
|
_organizationDomainRepository = organizationDomainRepository;
|
||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
_dnsResolverService = dnsResolverService;
|
|
||||||
_logger = logger;
|
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,5 +4,6 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfa
|
|||||||
|
|
||||||
public interface IVerifyOrganizationDomainCommand
|
public interface IVerifyOrganizationDomainCommand
|
||||||
{
|
{
|
||||||
Task<OrganizationDomain> VerifyOrganizationDomainAsync(OrganizationDomain organizationDomain);
|
Task<OrganizationDomain> UserVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain);
|
||||||
|
Task<OrganizationDomain> SystemVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain);
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Settings;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains;
|
||||||
@ -13,34 +14,85 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
|||||||
private readonly IOrganizationDomainRepository _organizationDomainRepository;
|
private readonly IOrganizationDomainRepository _organizationDomainRepository;
|
||||||
private readonly IDnsResolverService _dnsResolverService;
|
private readonly IDnsResolverService _dnsResolverService;
|
||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
|
private readonly IGlobalSettings _globalSettings;
|
||||||
private readonly ILogger<VerifyOrganizationDomainCommand> _logger;
|
private readonly ILogger<VerifyOrganizationDomainCommand> _logger;
|
||||||
|
|
||||||
public VerifyOrganizationDomainCommand(
|
public VerifyOrganizationDomainCommand(
|
||||||
IOrganizationDomainRepository organizationDomainRepository,
|
IOrganizationDomainRepository organizationDomainRepository,
|
||||||
IDnsResolverService dnsResolverService,
|
IDnsResolverService dnsResolverService,
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
|
IGlobalSettings globalSettings,
|
||||||
ILogger<VerifyOrganizationDomainCommand> logger)
|
ILogger<VerifyOrganizationDomainCommand> logger)
|
||||||
{
|
{
|
||||||
_organizationDomainRepository = organizationDomainRepository;
|
_organizationDomainRepository = organizationDomainRepository;
|
||||||
_dnsResolverService = dnsResolverService;
|
_dnsResolverService = dnsResolverService;
|
||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
|
_globalSettings = globalSettings;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<OrganizationDomain> VerifyOrganizationDomainAsync(OrganizationDomain domain)
|
|
||||||
|
public async Task<OrganizationDomain> UserVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain)
|
||||||
{
|
{
|
||||||
|
var domainVerificationResult = await VerifyOrganizationDomainAsync(organizationDomain);
|
||||||
|
|
||||||
|
await _eventService.LogOrganizationDomainEventAsync(domainVerificationResult,
|
||||||
|
domainVerificationResult.VerifiedDate != null
|
||||||
|
? EventType.OrganizationDomain_Verified
|
||||||
|
: EventType.OrganizationDomain_NotVerified);
|
||||||
|
|
||||||
|
await _organizationDomainRepository.ReplaceAsync(domainVerificationResult);
|
||||||
|
|
||||||
|
return domainVerificationResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<OrganizationDomain> SystemVerifyOrganizationDomainAsync(OrganizationDomain organizationDomain)
|
||||||
|
{
|
||||||
|
organizationDomain.SetJobRunCount();
|
||||||
|
|
||||||
|
var domainVerificationResult = await VerifyOrganizationDomainAsync(organizationDomain);
|
||||||
|
|
||||||
|
if (domainVerificationResult.VerifiedDate is not null)
|
||||||
|
{
|
||||||
|
_logger.LogInformation(Constants.BypassFiltersEventId, "Successfully validated domain");
|
||||||
|
|
||||||
|
await _eventService.LogOrganizationDomainEventAsync(domainVerificationResult,
|
||||||
|
EventType.OrganizationDomain_Verified,
|
||||||
|
EventSystemUser.DomainVerification);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
domainVerificationResult.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval);
|
||||||
|
|
||||||
|
await _eventService.LogOrganizationDomainEventAsync(domainVerificationResult,
|
||||||
|
EventType.OrganizationDomain_NotVerified,
|
||||||
|
EventSystemUser.DomainVerification);
|
||||||
|
|
||||||
|
_logger.LogInformation(Constants.BypassFiltersEventId,
|
||||||
|
"Verification for organization {OrgId} with domain {Domain} failed",
|
||||||
|
domainVerificationResult.OrganizationId, domainVerificationResult.DomainName);
|
||||||
|
}
|
||||||
|
|
||||||
|
await _organizationDomainRepository.ReplaceAsync(domainVerificationResult);
|
||||||
|
|
||||||
|
return domainVerificationResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<OrganizationDomain> VerifyOrganizationDomainAsync(OrganizationDomain domain)
|
||||||
|
{
|
||||||
|
domain.SetLastCheckedDate();
|
||||||
|
|
||||||
if (domain.VerifiedDate is not null)
|
if (domain.VerifiedDate is not null)
|
||||||
{
|
{
|
||||||
domain.SetLastCheckedDate();
|
|
||||||
await _organizationDomainRepository.ReplaceAsync(domain);
|
await _organizationDomainRepository.ReplaceAsync(domain);
|
||||||
throw new ConflictException("Domain has already been verified.");
|
throw new ConflictException("Domain has already been verified.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var claimedDomain =
|
var claimedDomain =
|
||||||
await _organizationDomainRepository.GetClaimedDomainsByDomainNameAsync(domain.DomainName);
|
await _organizationDomainRepository.GetClaimedDomainsByDomainNameAsync(domain.DomainName);
|
||||||
if (claimedDomain.Any())
|
|
||||||
|
if (claimedDomain.Count > 0)
|
||||||
{
|
{
|
||||||
domain.SetLastCheckedDate();
|
|
||||||
await _organizationDomainRepository.ReplaceAsync(domain);
|
await _organizationDomainRepository.ReplaceAsync(domain);
|
||||||
throw new ConflictException("The domain is not available to be claimed.");
|
throw new ConflictException("The domain is not available to be claimed.");
|
||||||
}
|
}
|
||||||
@ -58,11 +110,6 @@ public class VerifyOrganizationDomainCommand : IVerifyOrganizationDomainCommand
|
|||||||
domain.DomainName, e.Message);
|
domain.DomainName, e.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
domain.SetLastCheckedDate();
|
|
||||||
await _organizationDomainRepository.ReplaceAsync(domain);
|
|
||||||
|
|
||||||
await _eventService.LogOrganizationDomainEventAsync(domain,
|
|
||||||
domain.VerifiedDate != null ? EventType.OrganizationDomain_Verified : EventType.OrganizationDomain_NotVerified);
|
|
||||||
return domain;
|
return domain;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -180,12 +180,12 @@ public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IEnumerable<KeyValuePair<string, DeviceType>>> GetUserDeviceIdsAsync(Guid userId)
|
private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)
|
||||||
{
|
{
|
||||||
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
|
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
|
||||||
return devices
|
return devices
|
||||||
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
|
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
|
||||||
.Select(d => new KeyValuePair<string, DeviceType>(d.Id.ToString(), d.Type));
|
.Select(d => d.Id.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)
|
private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)
|
||||||
|
@ -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.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
using Bit.Core.Services;
|
|
||||||
|
|
||||||
namespace Bit.Core.AdminConsole.Services;
|
namespace Bit.Core.AdminConsole.Services;
|
||||||
|
|
||||||
public interface IPolicyService
|
public interface IPolicyService
|
||||||
{
|
{
|
||||||
Task SaveAsync(Policy policy, IOrganizationService organizationService, Guid? savingUserId);
|
Task SaveAsync(Policy policy, Guid? savingUserId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Get the combined master password policy options for the specified user.
|
/// Get the combined master password policy options for the specified user.
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
|
||||||
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
@ -10,26 +11,29 @@ public class OrganizationDomainService : IOrganizationDomainService
|
|||||||
{
|
{
|
||||||
private readonly IOrganizationDomainRepository _domainRepository;
|
private readonly IOrganizationDomainRepository _domainRepository;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IDnsResolverService _dnsResolverService;
|
|
||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
private readonly IMailService _mailService;
|
private readonly IMailService _mailService;
|
||||||
|
private readonly IVerifyOrganizationDomainCommand _verifyOrganizationDomainCommand;
|
||||||
|
private readonly TimeProvider _timeProvider;
|
||||||
private readonly ILogger<OrganizationDomainService> _logger;
|
private readonly ILogger<OrganizationDomainService> _logger;
|
||||||
private readonly IGlobalSettings _globalSettings;
|
private readonly IGlobalSettings _globalSettings;
|
||||||
|
|
||||||
public OrganizationDomainService(
|
public OrganizationDomainService(
|
||||||
IOrganizationDomainRepository domainRepository,
|
IOrganizationDomainRepository domainRepository,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IDnsResolverService dnsResolverService,
|
|
||||||
IEventService eventService,
|
IEventService eventService,
|
||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
|
IVerifyOrganizationDomainCommand verifyOrganizationDomainCommand,
|
||||||
|
TimeProvider timeProvider,
|
||||||
ILogger<OrganizationDomainService> logger,
|
ILogger<OrganizationDomainService> logger,
|
||||||
IGlobalSettings globalSettings)
|
IGlobalSettings globalSettings)
|
||||||
{
|
{
|
||||||
_domainRepository = domainRepository;
|
_domainRepository = domainRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_dnsResolverService = dnsResolverService;
|
|
||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
_mailService = mailService;
|
_mailService = mailService;
|
||||||
|
_verifyOrganizationDomainCommand = verifyOrganizationDomainCommand;
|
||||||
|
_timeProvider = timeProvider;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
}
|
}
|
||||||
@ -37,7 +41,7 @@ public class OrganizationDomainService : IOrganizationDomainService
|
|||||||
public async Task ValidateOrganizationsDomainAsync()
|
public async Task ValidateOrganizationsDomainAsync()
|
||||||
{
|
{
|
||||||
//Date should be set 1 hour behind to ensure it selects all domains that should be verified
|
//Date should be set 1 hour behind to ensure it selects all domains that should be verified
|
||||||
var runDate = DateTime.UtcNow.AddHours(-1);
|
var runDate = _timeProvider.GetUtcNow().UtcDateTime.AddHours(-1);
|
||||||
|
|
||||||
var verifiableDomains = await _domainRepository.GetManyByNextRunDateAsync(runDate);
|
var verifiableDomains = await _domainRepository.GetManyByNextRunDateAsync(runDate);
|
||||||
|
|
||||||
@ -45,43 +49,17 @@ public class OrganizationDomainService : IOrganizationDomainService
|
|||||||
|
|
||||||
foreach (var domain in verifiableDomains)
|
foreach (var domain in verifiableDomains)
|
||||||
{
|
{
|
||||||
|
_logger.LogInformation(Constants.BypassFiltersEventId,
|
||||||
|
"Attempting verification for organization {OrgId} with domain {Domain}",
|
||||||
|
domain.OrganizationId,
|
||||||
|
domain.DomainName);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_logger.LogInformation(Constants.BypassFiltersEventId, "Attempting verification for organization {OrgId} with domain {Domain}", domain.OrganizationId, domain.DomainName);
|
_ = await _verifyOrganizationDomainCommand.SystemVerifyOrganizationDomainAsync(domain);
|
||||||
|
|
||||||
var status = await _dnsResolverService.ResolveAsync(domain.DomainName, domain.Txt);
|
|
||||||
if (status)
|
|
||||||
{
|
|
||||||
_logger.LogInformation(Constants.BypassFiltersEventId, "Successfully validated domain");
|
|
||||||
|
|
||||||
// Update entry on OrganizationDomain table
|
|
||||||
domain.SetLastCheckedDate();
|
|
||||||
domain.SetVerifiedDate();
|
|
||||||
domain.SetJobRunCount();
|
|
||||||
await _domainRepository.ReplaceAsync(domain);
|
|
||||||
|
|
||||||
await _eventService.LogOrganizationDomainEventAsync(domain, EventType.OrganizationDomain_Verified,
|
|
||||||
EventSystemUser.DomainVerification);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Update entry on OrganizationDomain table
|
|
||||||
domain.SetLastCheckedDate();
|
|
||||||
domain.SetJobRunCount();
|
|
||||||
domain.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval);
|
|
||||||
await _domainRepository.ReplaceAsync(domain);
|
|
||||||
|
|
||||||
await _eventService.LogOrganizationDomainEventAsync(domain, EventType.OrganizationDomain_NotVerified,
|
|
||||||
EventSystemUser.DomainVerification);
|
|
||||||
_logger.LogInformation(Constants.BypassFiltersEventId, "Verification for organization {OrgId} with domain {Domain} failed",
|
|
||||||
domain.OrganizationId, domain.DomainName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// Update entry on OrganizationDomain table
|
|
||||||
domain.SetLastCheckedDate();
|
|
||||||
domain.SetJobRunCount();
|
|
||||||
domain.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval);
|
domain.SetNextRunDate(_globalSettings.DomainVerification.VerificationInterval);
|
||||||
await _domainRepository.ReplaceAsync(domain);
|
await _domainRepository.ReplaceAsync(domain);
|
||||||
|
|
||||||
|
@ -708,10 +708,16 @@ public class OrganizationService : IOrganizationService
|
|||||||
UseSecretsManager = license.UseSecretsManager,
|
UseSecretsManager = license.UseSecretsManager,
|
||||||
SmSeats = license.SmSeats,
|
SmSeats = license.SmSeats,
|
||||||
SmServiceAccounts = license.SmServiceAccounts,
|
SmServiceAccounts = license.SmServiceAccounts,
|
||||||
LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion,
|
|
||||||
AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// These fields are being removed from consideration when processing
|
||||||
|
// licenses.
|
||||||
|
if (!_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
|
||||||
|
{
|
||||||
|
organization.LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion;
|
||||||
|
organization.AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems;
|
||||||
|
}
|
||||||
|
|
||||||
var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);
|
var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);
|
||||||
|
|
||||||
var dir = $"{_globalSettings.LicenseDirectory}/organization";
|
var dir = $"{_globalSettings.LicenseDirectory}/organization";
|
||||||
@ -1832,12 +1838,12 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private async Task<IEnumerable<KeyValuePair<string, DeviceType>>> GetUserDeviceIdsAsync(Guid userId)
|
private async Task<IEnumerable<string>> GetUserDeviceIdsAsync(Guid userId)
|
||||||
{
|
{
|
||||||
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
|
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
|
||||||
return devices
|
return devices
|
||||||
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
|
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
|
||||||
.Select(d => new KeyValuePair<string, DeviceType>(d.Id.ToString(), d.Type));
|
.Select(d => d.Id.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null)
|
public async Task ReplaceAndUpdateCacheAsync(Organization org, EventType? orgEvent = null)
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.Models;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Auth.Enums;
|
using Bit.Core.Auth.Enums;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
@ -27,6 +29,8 @@ public class PolicyService : IPolicyService
|
|||||||
private readonly IMailService _mailService;
|
private readonly IMailService _mailService;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
private readonly ISavePolicyCommand _savePolicyCommand;
|
||||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||||
|
|
||||||
public PolicyService(
|
public PolicyService(
|
||||||
@ -39,6 +43,8 @@ public class PolicyService : IPolicyService
|
|||||||
IMailService mailService,
|
IMailService mailService,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
|
IFeatureService featureService,
|
||||||
|
ISavePolicyCommand savePolicyCommand,
|
||||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
|
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
|
||||||
{
|
{
|
||||||
_applicationCacheService = applicationCacheService;
|
_applicationCacheService = applicationCacheService;
|
||||||
@ -50,11 +56,28 @@ public class PolicyService : IPolicyService
|
|||||||
_mailService = mailService;
|
_mailService = mailService;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
|
_featureService = featureService;
|
||||||
|
_savePolicyCommand = savePolicyCommand;
|
||||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveAsync(Policy policy, IOrganizationService organizationService, Guid? savingUserId)
|
public async Task SaveAsync(Policy policy, Guid? savingUserId)
|
||||||
{
|
{
|
||||||
|
if (_featureService.IsEnabled(FeatureFlagKeys.Pm13322AddPolicyDefinitions))
|
||||||
|
{
|
||||||
|
// Transitional mapping - this will be moved to callers once the feature flag is removed
|
||||||
|
var policyUpdate = new PolicyUpdate
|
||||||
|
{
|
||||||
|
OrganizationId = policy.OrganizationId,
|
||||||
|
Type = policy.Type,
|
||||||
|
Enabled = policy.Enabled,
|
||||||
|
Data = policy.Data
|
||||||
|
};
|
||||||
|
|
||||||
|
await _savePolicyCommand.SaveAsync(policyUpdate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var org = await _organizationRepository.GetByIdAsync(policy.OrganizationId);
|
var org = await _organizationRepository.GetByIdAsync(policy.OrganizationId);
|
||||||
if (org == null)
|
if (org == null)
|
||||||
{
|
{
|
||||||
@ -88,7 +111,7 @@ public class PolicyService : IPolicyService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await EnablePolicyAsync(policy, org, organizationService, savingUserId);
|
await EnablePolicyAsync(policy, org, savingUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<MasterPasswordPolicyData> GetMasterPasswordPolicyForUserAsync(User user)
|
public async Task<MasterPasswordPolicyData> GetMasterPasswordPolicyForUserAsync(User user)
|
||||||
@ -262,7 +285,7 @@ public class PolicyService : IPolicyService
|
|||||||
await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated);
|
await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EnablePolicyAsync(Policy policy, Organization org, IOrganizationService organizationService, Guid? savingUserId)
|
private async Task EnablePolicyAsync(Policy policy, Organization org, Guid? savingUserId)
|
||||||
{
|
{
|
||||||
var currentPolicy = await _policyRepository.GetByIdAsync(policy.Id);
|
var currentPolicy = await _policyRepository.GetByIdAsync(policy.Id);
|
||||||
if (!currentPolicy?.Enabled ?? true)
|
if (!currentPolicy?.Enabled ?? true)
|
||||||
|
@ -20,7 +20,6 @@ public class SsoConfigService : ISsoConfigService
|
|||||||
private readonly IPolicyService _policyService;
|
private readonly IPolicyService _policyService;
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IOrganizationService _organizationService;
|
|
||||||
private readonly IEventService _eventService;
|
private readonly IEventService _eventService;
|
||||||
|
|
||||||
public SsoConfigService(
|
public SsoConfigService(
|
||||||
@ -29,7 +28,6 @@ public class SsoConfigService : ISsoConfigService
|
|||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IOrganizationService organizationService,
|
|
||||||
IEventService eventService)
|
IEventService eventService)
|
||||||
{
|
{
|
||||||
_ssoConfigRepository = ssoConfigRepository;
|
_ssoConfigRepository = ssoConfigRepository;
|
||||||
@ -37,7 +35,6 @@ public class SsoConfigService : ISsoConfigService
|
|||||||
_policyService = policyService;
|
_policyService = policyService;
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_organizationService = organizationService;
|
|
||||||
_eventService = eventService;
|
_eventService = eventService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,20 +68,20 @@ public class SsoConfigService : ISsoConfigService
|
|||||||
|
|
||||||
singleOrgPolicy.Enabled = true;
|
singleOrgPolicy.Enabled = true;
|
||||||
|
|
||||||
await _policyService.SaveAsync(singleOrgPolicy, _organizationService, null);
|
await _policyService.SaveAsync(singleOrgPolicy, null);
|
||||||
|
|
||||||
var resetPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.ResetPassword) ??
|
var resetPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.ResetPassword) ??
|
||||||
new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.ResetPassword, };
|
new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.ResetPassword, };
|
||||||
|
|
||||||
resetPolicy.Enabled = true;
|
resetPolicy.Enabled = true;
|
||||||
resetPolicy.SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = true });
|
resetPolicy.SetDataModel(new ResetPasswordDataModel { AutoEnrollEnabled = true });
|
||||||
await _policyService.SaveAsync(resetPolicy, _organizationService, null);
|
await _policyService.SaveAsync(resetPolicy, null);
|
||||||
|
|
||||||
var ssoRequiredPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.RequireSso) ??
|
var ssoRequiredPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(config.OrganizationId, PolicyType.RequireSso) ??
|
||||||
new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.RequireSso, };
|
new Policy { OrganizationId = config.OrganizationId, Type = PolicyType.RequireSso, };
|
||||||
|
|
||||||
ssoRequiredPolicy.Enabled = true;
|
ssoRequiredPolicy.Enabled = true;
|
||||||
await _policyService.SaveAsync(ssoRequiredPolicy, _organizationService, null);
|
await _policyService.SaveAsync(ssoRequiredPolicy, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
await LogEventsAsync(config, oldConfig);
|
await LogEventsAsync(config, oldConfig);
|
||||||
|
@ -3,17 +3,14 @@
|
|||||||
public enum ProviderMigrationProgress
|
public enum ProviderMigrationProgress
|
||||||
{
|
{
|
||||||
Started = 1,
|
Started = 1,
|
||||||
ClientsMigrated = 2,
|
NoClients = 2,
|
||||||
TeamsPlanConfigured = 3,
|
ClientsMigrated = 3,
|
||||||
EnterprisePlanConfigured = 4,
|
TeamsPlanConfigured = 4,
|
||||||
CustomerSetup = 5,
|
EnterprisePlanConfigured = 5,
|
||||||
SubscriptionSetup = 6,
|
CustomerSetup = 6,
|
||||||
CreditApplied = 7,
|
SubscriptionSetup = 7,
|
||||||
Completed = 8,
|
CreditApplied = 8,
|
||||||
|
Completed = 9,
|
||||||
Reversing = 9,
|
|
||||||
ReversedClientMigrations = 10,
|
|
||||||
RemovedProviderPlans = 11
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ProviderMigrationTracker
|
public class ProviderMigrationTracker
|
||||||
|
@ -41,7 +41,18 @@ public class ProviderMigrator(
|
|||||||
|
|
||||||
await migrationTrackerCache.StartTracker(provider);
|
await migrationTrackerCache.StartTracker(provider);
|
||||||
|
|
||||||
await MigrateClientsAsync(providerId);
|
var organizations = await GetClientsAsync(provider.Id);
|
||||||
|
|
||||||
|
if (organizations.Count == 0)
|
||||||
|
{
|
||||||
|
logger.LogInformation("CB: Skipping migration for provider ({ProviderID}) with no clients", providerId);
|
||||||
|
|
||||||
|
await migrationTrackerCache.UpdateTrackingStatus(providerId, ProviderMigrationProgress.NoClients);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await MigrateClientsAsync(providerId, organizations);
|
||||||
|
|
||||||
await ConfigureTeamsPlanAsync(providerId);
|
await ConfigureTeamsPlanAsync(providerId);
|
||||||
|
|
||||||
@ -65,6 +76,16 @@ public class ProviderMigrator(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (providerTracker.Progress == ProviderMigrationProgress.NoClients)
|
||||||
|
{
|
||||||
|
return new ProviderMigrationResult
|
||||||
|
{
|
||||||
|
ProviderId = providerTracker.ProviderId,
|
||||||
|
ProviderName = providerTracker.ProviderName,
|
||||||
|
Result = providerTracker.Progress.ToString()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var clientTrackers = await Task.WhenAll(providerTracker.OrganizationIds.Select(organizationId =>
|
var clientTrackers = await Task.WhenAll(providerTracker.OrganizationIds.Select(organizationId =>
|
||||||
migrationTrackerCache.GetTracker(providerId, organizationId)));
|
migrationTrackerCache.GetTracker(providerId, organizationId)));
|
||||||
|
|
||||||
@ -99,12 +120,10 @@ public class ProviderMigrator(
|
|||||||
|
|
||||||
#region Steps
|
#region Steps
|
||||||
|
|
||||||
private async Task MigrateClientsAsync(Guid providerId)
|
private async Task MigrateClientsAsync(Guid providerId, List<Organization> organizations)
|
||||||
{
|
{
|
||||||
logger.LogInformation("CB: Migrating clients for provider ({ProviderID})", providerId);
|
logger.LogInformation("CB: Migrating clients for provider ({ProviderID})", providerId);
|
||||||
|
|
||||||
var organizations = await GetEnabledClientsAsync(providerId);
|
|
||||||
|
|
||||||
var organizationIds = organizations.Select(organization => organization.Id);
|
var organizationIds = organizations.Select(organization => organization.Id);
|
||||||
|
|
||||||
await migrationTrackerCache.SetOrganizationIds(providerId, organizationIds);
|
await migrationTrackerCache.SetOrganizationIds(providerId, organizationIds);
|
||||||
@ -129,7 +148,7 @@ public class ProviderMigrator(
|
|||||||
{
|
{
|
||||||
logger.LogInformation("CB: Configuring Teams plan for provider ({ProviderID})", providerId);
|
logger.LogInformation("CB: Configuring Teams plan for provider ({ProviderID})", providerId);
|
||||||
|
|
||||||
var organizations = await GetEnabledClientsAsync(providerId);
|
var organizations = await GetClientsAsync(providerId);
|
||||||
|
|
||||||
var teamsSeats = organizations
|
var teamsSeats = organizations
|
||||||
.Where(IsTeams)
|
.Where(IsTeams)
|
||||||
@ -172,7 +191,7 @@ public class ProviderMigrator(
|
|||||||
{
|
{
|
||||||
logger.LogInformation("CB: Configuring Enterprise plan for provider ({ProviderID})", providerId);
|
logger.LogInformation("CB: Configuring Enterprise plan for provider ({ProviderID})", providerId);
|
||||||
|
|
||||||
var organizations = await GetEnabledClientsAsync(providerId);
|
var organizations = await GetClientsAsync(providerId);
|
||||||
|
|
||||||
var enterpriseSeats = organizations
|
var enterpriseSeats = organizations
|
||||||
.Where(IsEnterprise)
|
.Where(IsEnterprise)
|
||||||
@ -215,7 +234,7 @@ public class ProviderMigrator(
|
|||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(provider.GatewayCustomerId))
|
if (string.IsNullOrEmpty(provider.GatewayCustomerId))
|
||||||
{
|
{
|
||||||
var organizations = await GetEnabledClientsAsync(provider.Id);
|
var organizations = await GetClientsAsync(provider.Id);
|
||||||
|
|
||||||
var sampleOrganization = organizations.FirstOrDefault(organization => !string.IsNullOrEmpty(organization.GatewayCustomerId));
|
var sampleOrganization = organizations.FirstOrDefault(organization => !string.IsNullOrEmpty(organization.GatewayCustomerId));
|
||||||
|
|
||||||
@ -299,28 +318,43 @@ public class ProviderMigrator(
|
|||||||
|
|
||||||
private async Task ApplyCreditAsync(Provider provider)
|
private async Task ApplyCreditAsync(Provider provider)
|
||||||
{
|
{
|
||||||
var organizations = await GetEnabledClientsAsync(provider.Id);
|
var organizations = await GetClientsAsync(provider.Id);
|
||||||
|
|
||||||
var organizationCustomers =
|
var organizationCustomers =
|
||||||
await Task.WhenAll(organizations.Select(organization => stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId)));
|
await Task.WhenAll(organizations.Select(organization => stripeAdapter.CustomerGetAsync(organization.GatewayCustomerId)));
|
||||||
|
|
||||||
var organizationCancellationCredit = organizationCustomers.Sum(customer => customer.Balance);
|
var organizationCancellationCredit = organizationCustomers.Sum(customer => customer.Balance);
|
||||||
|
|
||||||
var legacyOrganizations = organizations.Where(organization =>
|
await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId,
|
||||||
organization.PlanType is
|
new CustomerBalanceTransactionCreateOptions
|
||||||
|
{
|
||||||
|
Amount = organizationCancellationCredit,
|
||||||
|
Currency = "USD",
|
||||||
|
Description = "Unused, prorated time for client organization subscriptions."
|
||||||
|
});
|
||||||
|
|
||||||
|
var migrationRecords = await Task.WhenAll(organizations.Select(organization =>
|
||||||
|
clientOrganizationMigrationRecordRepository.GetByOrganizationId(organization.Id)));
|
||||||
|
|
||||||
|
var legacyOrganizationMigrationRecords = migrationRecords.Where(migrationRecord =>
|
||||||
|
migrationRecord.PlanType is
|
||||||
PlanType.EnterpriseAnnually2020 or
|
PlanType.EnterpriseAnnually2020 or
|
||||||
PlanType.EnterpriseMonthly2020 or
|
PlanType.TeamsAnnually2020);
|
||||||
PlanType.TeamsAnnually2020 or
|
|
||||||
PlanType.TeamsMonthly2020);
|
|
||||||
|
|
||||||
var legacyOrganizationCredit = legacyOrganizations.Sum(organization => organization.Seats ?? 0);
|
var legacyOrganizationCredit = legacyOrganizationMigrationRecords.Sum(migrationRecord => migrationRecord.Seats) * 12 * -100;
|
||||||
|
|
||||||
await stripeAdapter.CustomerUpdateAsync(provider.GatewayCustomerId, new CustomerUpdateOptions
|
if (legacyOrganizationCredit < 0)
|
||||||
{
|
{
|
||||||
Balance = organizationCancellationCredit + legacyOrganizationCredit
|
await stripeAdapter.CustomerBalanceTransactionCreate(provider.GatewayCustomerId,
|
||||||
});
|
new CustomerBalanceTransactionCreateOptions
|
||||||
|
{
|
||||||
|
Amount = legacyOrganizationCredit,
|
||||||
|
Currency = "USD",
|
||||||
|
Description = "1 year rebate for legacy client organizations."
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
logger.LogInformation("CB: Applied {Credit} credit to provider ({ProviderID})", organizationCancellationCredit, provider.Id);
|
logger.LogInformation("CB: Applied {Credit} credit to provider ({ProviderID})", organizationCancellationCredit + legacyOrganizationCredit, provider.Id);
|
||||||
|
|
||||||
await migrationTrackerCache.UpdateTrackingStatus(provider.Id, ProviderMigrationProgress.CreditApplied);
|
await migrationTrackerCache.UpdateTrackingStatus(provider.Id, ProviderMigrationProgress.CreditApplied);
|
||||||
}
|
}
|
||||||
@ -340,13 +374,12 @@ public class ProviderMigrator(
|
|||||||
|
|
||||||
#region Utilities
|
#region Utilities
|
||||||
|
|
||||||
private async Task<List<Organization>> GetEnabledClientsAsync(Guid providerId)
|
private async Task<List<Organization>> GetClientsAsync(Guid providerId)
|
||||||
{
|
{
|
||||||
var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId);
|
var providerOrganizations = await providerOrganizationRepository.GetManyDetailsByProviderAsync(providerId);
|
||||||
|
|
||||||
return (await Task.WhenAll(providerOrganizations.Select(providerOrganization =>
|
return (await Task.WhenAll(providerOrganizations.Select(providerOrganization =>
|
||||||
organizationRepository.GetByIdAsync(providerOrganization.OrganizationId))))
|
organizationRepository.GetByIdAsync(providerOrganization.OrganizationId))))
|
||||||
.Where(organization => organization.Enabled)
|
|
||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
namespace Bit.Core.Billing.Models;
|
namespace Bit.Core.Billing.Models;
|
||||||
|
|
||||||
public record OrganizationMetadata(
|
public record OrganizationMetadata(
|
||||||
|
bool IsEligibleForSelfHost,
|
||||||
bool IsOnSecretsManagerStandalone)
|
bool IsOnSecretsManagerStandalone)
|
||||||
{
|
{
|
||||||
public static OrganizationMetadata Default() => new(
|
public static OrganizationMetadata Default() => new(
|
||||||
IsOnSecretsManagerStandalone: default);
|
IsEligibleForSelfHost: false,
|
||||||
|
IsOnSecretsManagerStandalone: false);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Billing.Caches;
|
using Bit.Core.Billing.Caches;
|
||||||
using Bit.Core.Billing.Constants;
|
using Bit.Core.Billing.Constants;
|
||||||
using Bit.Core.Billing.Models;
|
using Bit.Core.Billing.Models;
|
||||||
@ -26,6 +27,7 @@ public class OrganizationBillingService(
|
|||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings,
|
||||||
ILogger<OrganizationBillingService> logger,
|
ILogger<OrganizationBillingService> logger,
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
|
IProviderRepository providerRepository,
|
||||||
ISetupIntentCache setupIntentCache,
|
ISetupIntentCache setupIntentCache,
|
||||||
IStripeAdapter stripeAdapter,
|
IStripeAdapter stripeAdapter,
|
||||||
ISubscriberService subscriberService) : IOrganizationBillingService
|
ISubscriberService subscriberService) : IOrganizationBillingService
|
||||||
@ -69,14 +71,11 @@ public class OrganizationBillingService(
|
|||||||
|
|
||||||
var subscription = await subscriberService.GetSubscription(organization);
|
var subscription = await subscriberService.GetSubscription(organization);
|
||||||
|
|
||||||
if (customer == null || subscription == null)
|
var isEligibleForSelfHost = await IsEligibleForSelfHost(organization, subscription);
|
||||||
{
|
|
||||||
return OrganizationMetadata.Default();
|
|
||||||
}
|
|
||||||
|
|
||||||
var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription);
|
var isOnSecretsManagerStandalone = IsOnSecretsManagerStandalone(organization, customer, subscription);
|
||||||
|
|
||||||
return new OrganizationMetadata(isOnSecretsManagerStandalone);
|
return new OrganizationMetadata(isEligibleForSelfHost, isOnSecretsManagerStandalone);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdatePaymentMethod(
|
public async Task UpdatePaymentMethod(
|
||||||
@ -340,11 +339,38 @@ public class OrganizationBillingService(
|
|||||||
return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
return await stripeAdapter.SubscriptionCreateAsync(subscriptionCreateOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<bool> IsEligibleForSelfHost(
|
||||||
|
Organization organization,
|
||||||
|
Subscription? organizationSubscription)
|
||||||
|
{
|
||||||
|
if (organization.Status != OrganizationStatusType.Managed)
|
||||||
|
{
|
||||||
|
return organization.Plan.Contains("Families") ||
|
||||||
|
organization.Plan.Contains("Enterprise") && IsActive(organizationSubscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
var provider = await providerRepository.GetByOrganizationIdAsync(organization.Id);
|
||||||
|
|
||||||
|
var providerSubscription = await subscriberService.GetSubscriptionOrThrow(provider);
|
||||||
|
|
||||||
|
return organization.Plan.Contains("Enterprise") && IsActive(providerSubscription);
|
||||||
|
|
||||||
|
bool IsActive(Subscription? subscription) => subscription?.Status is
|
||||||
|
StripeConstants.SubscriptionStatus.Active or
|
||||||
|
StripeConstants.SubscriptionStatus.Trialing or
|
||||||
|
StripeConstants.SubscriptionStatus.PastDue;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsOnSecretsManagerStandalone(
|
private static bool IsOnSecretsManagerStandalone(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
Customer customer,
|
Customer? customer,
|
||||||
Subscription subscription)
|
Subscription? subscription)
|
||||||
{
|
{
|
||||||
|
if (customer == null || subscription == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
var plan = StaticStore.GetPlan(organization.PlanType);
|
var plan = StaticStore.GetPlan(organization.PlanType);
|
||||||
|
|
||||||
if (!plan.SupportsSecretsManager)
|
if (!plan.SupportsSecretsManager)
|
||||||
|
@ -146,6 +146,8 @@ public static class FeatureFlagKeys
|
|||||||
public const string RemoveServerVersionHeader = "remove-server-version-header";
|
public const string RemoveServerVersionHeader = "remove-server-version-header";
|
||||||
public const string AccessIntelligence = "pm-13227-access-intelligence";
|
public const string AccessIntelligence = "pm-13227-access-intelligence";
|
||||||
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
|
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
|
||||||
|
public const string Pm13322AddPolicyDefinitions = "pm-13322-add-policy-definitions";
|
||||||
|
public const string LimitCollectionCreationDeletionSplit = "pm-10863-limit-collection-creation-deletion-split";
|
||||||
|
|
||||||
public static List<string> GetAllKeys()
|
public static List<string> GetAllKeys()
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Core.Enums;
|
|
||||||
|
|
||||||
namespace Bit.Core.Models.Api;
|
namespace Bit.Core.Models.Api;
|
||||||
|
|
||||||
@ -7,6 +6,4 @@ public class PushDeviceRequestModel
|
|||||||
{
|
{
|
||||||
[Required]
|
[Required]
|
||||||
public string Id { get; set; }
|
public string Id { get; set; }
|
||||||
[Required]
|
|
||||||
public DeviceType Type { get; set; }
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
using System.ComponentModel.DataAnnotations;
|
||||||
using Bit.Core.Enums;
|
|
||||||
|
|
||||||
namespace Bit.Core.Models.Api;
|
namespace Bit.Core.Models.Api;
|
||||||
|
|
||||||
@ -8,9 +7,9 @@ public class PushUpdateRequestModel
|
|||||||
public PushUpdateRequestModel()
|
public PushUpdateRequestModel()
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
public PushUpdateRequestModel(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
public PushUpdateRequestModel(IEnumerable<string> deviceIds, string organizationId)
|
||||||
{
|
{
|
||||||
Devices = devices.Select(d => new PushDeviceRequestModel { Id = d.Key, Type = d.Value });
|
Devices = deviceIds.Select(d => new PushDeviceRequestModel { Id = d });
|
||||||
OrganizationId = organizationId;
|
OrganizationId = organizationId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,8 +53,11 @@ public class OrganizationLicense : ILicense
|
|||||||
UseSecretsManager = org.UseSecretsManager;
|
UseSecretsManager = org.UseSecretsManager;
|
||||||
SmSeats = org.SmSeats;
|
SmSeats = org.SmSeats;
|
||||||
SmServiceAccounts = org.SmServiceAccounts;
|
SmServiceAccounts = org.SmServiceAccounts;
|
||||||
|
|
||||||
|
// Deprecated. Left for backwards compatibility with old license versions.
|
||||||
LimitCollectionCreationDeletion = org.LimitCollectionCreationDeletion;
|
LimitCollectionCreationDeletion = org.LimitCollectionCreationDeletion;
|
||||||
AllowAdminAccessToAllCollectionItems = org.AllowAdminAccessToAllCollectionItems;
|
AllowAdminAccessToAllCollectionItems = org.AllowAdminAccessToAllCollectionItems;
|
||||||
|
//
|
||||||
|
|
||||||
if (subscriptionInfo?.Subscription == null)
|
if (subscriptionInfo?.Subscription == null)
|
||||||
{
|
{
|
||||||
@ -138,8 +141,12 @@ public class OrganizationLicense : ILicense
|
|||||||
public bool UseSecretsManager { get; set; }
|
public bool UseSecretsManager { get; set; }
|
||||||
public int? SmSeats { get; set; }
|
public int? SmSeats { get; set; }
|
||||||
public int? SmServiceAccounts { get; set; }
|
public int? SmServiceAccounts { get; set; }
|
||||||
|
|
||||||
|
// Deprecated. Left for backwards compatibility with old license versions.
|
||||||
public bool LimitCollectionCreationDeletion { get; set; } = true;
|
public bool LimitCollectionCreationDeletion { get; set; } = true;
|
||||||
public bool AllowAdminAccessToAllCollectionItems { get; set; } = true;
|
public bool AllowAdminAccessToAllCollectionItems { get; set; } = true;
|
||||||
|
//
|
||||||
|
|
||||||
public bool Trial { get; set; }
|
public bool Trial { get; set; }
|
||||||
public LicenseType? LicenseType { get; set; }
|
public LicenseType? LicenseType { get; set; }
|
||||||
public string Hash { get; set; }
|
public string Hash { get; set; }
|
||||||
@ -150,7 +157,8 @@ public class OrganizationLicense : ILicense
|
|||||||
/// Represents the current version of the license format. Should be updated whenever new fields are added.
|
/// Represents the current version of the license format. Should be updated whenever new fields are added.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Intentionally set one version behind to allow self hosted users some time to update before
|
/// <remarks>Intentionally set one version behind to allow self hosted users some time to update before
|
||||||
/// getting out of date license errors</remarks>
|
/// getting out of date license errors
|
||||||
|
/// </remarks>
|
||||||
public const int CurrentLicenseFileVersion = 14;
|
public const int CurrentLicenseFileVersion = 14;
|
||||||
private bool ValidLicenseVersion
|
private bool ValidLicenseVersion
|
||||||
{
|
{
|
||||||
@ -368,10 +376,11 @@ public class OrganizationLicense : ILicense
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Version 14 added LimitCollectionCreationDeletion and Version 15 added AllowAdminAccessToAllCollectionItems,
|
* Version 14 added LimitCollectionCreationDeletion and Version
|
||||||
* however these are just user settings and it is not worth failing validation if they mismatch.
|
* 15 added AllowAdminAccessToAllCollectionItems, however they
|
||||||
* They are intentionally excluded.
|
* are no longer used and are intentionally excluded from
|
||||||
*/
|
* validation.
|
||||||
|
*/
|
||||||
|
|
||||||
return valid;
|
return valid;
|
||||||
}
|
}
|
||||||
|
@ -37,4 +37,25 @@ public class InstallationDeviceEntity : ITableEntity
|
|||||||
{
|
{
|
||||||
return deviceId != null && deviceId.Length == 73 && deviceId[36] == '_';
|
return deviceId != null && deviceId.Length == 73 && deviceId[36] == '_';
|
||||||
}
|
}
|
||||||
|
public static bool TryParse(string deviceId, out InstallationDeviceEntity installationDeviceEntity)
|
||||||
|
{
|
||||||
|
installationDeviceEntity = null;
|
||||||
|
var installationId = Guid.Empty;
|
||||||
|
var deviceIdGuid = Guid.Empty;
|
||||||
|
if (!IsInstallationDeviceId(deviceId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
var parts = deviceId.Split("_");
|
||||||
|
if (parts.Length < 2)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!Guid.TryParse(parts[0], out installationId) || !Guid.TryParse(parts[1], out deviceIdGuid))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
installationDeviceEntity = new InstallationDeviceEntity(installationId, deviceIdGuid);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -49,11 +49,11 @@ public class MarkNotificationDeletedCommand : IMarkNotificationDeletedCommand
|
|||||||
|
|
||||||
if (notificationStatus == null)
|
if (notificationStatus == null)
|
||||||
{
|
{
|
||||||
notificationStatus = new NotificationStatus()
|
notificationStatus = new NotificationStatus
|
||||||
{
|
{
|
||||||
NotificationId = notificationId,
|
NotificationId = notificationId,
|
||||||
UserId = _currentContext.UserId.Value,
|
UserId = _currentContext.UserId.Value,
|
||||||
DeletedDate = DateTime.Now
|
DeletedDate = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus,
|
await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus,
|
||||||
|
@ -49,11 +49,11 @@ public class MarkNotificationReadCommand : IMarkNotificationReadCommand
|
|||||||
|
|
||||||
if (notificationStatus == null)
|
if (notificationStatus == null)
|
||||||
{
|
{
|
||||||
notificationStatus = new NotificationStatus()
|
notificationStatus = new NotificationStatus
|
||||||
{
|
{
|
||||||
NotificationId = notificationId,
|
NotificationId = notificationId,
|
||||||
UserId = _currentContext.UserId.Value,
|
UserId = _currentContext.UserId.Value,
|
||||||
ReadDate = DateTime.Now
|
ReadDate = DateTime.UtcNow
|
||||||
};
|
};
|
||||||
|
|
||||||
await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus,
|
await _authorizationService.AuthorizeOrThrowAsync(_currentContext.HttpContext.User, notificationStatus,
|
||||||
|
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;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tools.Entities;
|
using Bit.Core.Tools.Entities;
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Azure.NotificationHubs;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.NotificationHub;
|
||||||
|
|
||||||
public class NotificationHubPushNotificationService : IPushNotificationService
|
public class NotificationHubPushNotificationService : IPushNotificationService
|
||||||
{
|
{
|
||||||
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
||||||
private readonly GlobalSettings _globalSettings;
|
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
private readonly List<NotificationHubClient> _clients = [];
|
|
||||||
private readonly bool _enableTracing = false;
|
private readonly bool _enableTracing = false;
|
||||||
|
private readonly INotificationHubPool _notificationHubPool;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
public NotificationHubPushNotificationService(
|
public NotificationHubPushNotificationService(
|
||||||
IInstallationDeviceRepository installationDeviceRepository,
|
IInstallationDeviceRepository installationDeviceRepository,
|
||||||
GlobalSettings globalSettings,
|
INotificationHubPool notificationHubPool,
|
||||||
IHttpContextAccessor httpContextAccessor,
|
IHttpContextAccessor httpContextAccessor,
|
||||||
ILogger<NotificationsApiPushNotificationService> logger)
|
ILogger<NotificationsApiPushNotificationService> logger)
|
||||||
{
|
{
|
||||||
_installationDeviceRepository = installationDeviceRepository;
|
_installationDeviceRepository = installationDeviceRepository;
|
||||||
_globalSettings = globalSettings;
|
|
||||||
_httpContextAccessor = httpContextAccessor;
|
_httpContextAccessor = httpContextAccessor;
|
||||||
|
_notificationHubPool = notificationHubPool;
|
||||||
foreach (var hub in globalSettings.NotificationHubs)
|
|
||||||
{
|
|
||||||
var client = NotificationHubClient.CreateClientFromConnectionString(
|
|
||||||
hub.ConnectionString,
|
|
||||||
hub.HubName,
|
|
||||||
hub.EnableSendTracing);
|
|
||||||
_clients.Add(client);
|
|
||||||
|
|
||||||
_enableTracing = _enableTracing || hub.EnableSendTracing;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -264,30 +250,23 @@ public class NotificationHubPushNotificationService : IPushNotificationService
|
|||||||
|
|
||||||
private async Task SendPayloadAsync(string tag, PushType type, object payload)
|
private async Task SendPayloadAsync(string tag, PushType type, object payload)
|
||||||
{
|
{
|
||||||
var tasks = new List<Task<NotificationOutcome>>();
|
var results = await _notificationHubPool.AllClients.SendTemplateNotificationAsync(
|
||||||
foreach (var client in _clients)
|
new Dictionary<string, string>
|
||||||
{
|
{
|
||||||
var task = client.SendTemplateNotificationAsync(
|
{ "type", ((byte)type).ToString() },
|
||||||
new Dictionary<string, string>
|
{ "payload", JsonSerializer.Serialize(payload) }
|
||||||
{
|
}, tag);
|
||||||
{ "type", ((byte)type).ToString() },
|
|
||||||
{ "payload", JsonSerializer.Serialize(payload) }
|
|
||||||
}, tag);
|
|
||||||
tasks.Add(task);
|
|
||||||
}
|
|
||||||
|
|
||||||
await Task.WhenAll(tasks);
|
|
||||||
|
|
||||||
if (_enableTracing)
|
if (_enableTracing)
|
||||||
{
|
{
|
||||||
for (var i = 0; i < tasks.Count; i++)
|
foreach (var (client, outcome) in results)
|
||||||
{
|
{
|
||||||
if (_clients[i].EnableTestSend)
|
if (!client.EnableTestSend)
|
||||||
{
|
{
|
||||||
var outcome = await tasks[i];
|
continue;
|
||||||
_logger.LogInformation("Azure Notification Hub Tracking ID: {id} | {type} push notification with {success} successes and {failure} failures with a payload of {@payload} and result of {@results}",
|
|
||||||
outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results);
|
|
||||||
}
|
}
|
||||||
|
_logger.LogInformation("Azure Notification Hub Tracking ID: {Id} | {Type} push notification with {Success} successes and {Failure} failures with a payload of {@Payload} and result of {@Results}",
|
||||||
|
outcome.TrackingId, type, outcome.Success, outcome.Failure, payload, outcome.Results);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,50 +1,34 @@
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Models.Data;
|
using Bit.Core.Models.Data;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Microsoft.Azure.NotificationHubs;
|
using Microsoft.Azure.NotificationHubs;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.NotificationHub;
|
||||||
|
|
||||||
public class NotificationHubPushRegistrationService : IPushRegistrationService
|
public class NotificationHubPushRegistrationService : IPushRegistrationService
|
||||||
{
|
{
|
||||||
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
private readonly IInstallationDeviceRepository _installationDeviceRepository;
|
||||||
private readonly GlobalSettings _globalSettings;
|
private readonly GlobalSettings _globalSettings;
|
||||||
|
private readonly INotificationHubPool _notificationHubPool;
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
private readonly ILogger<NotificationHubPushRegistrationService> _logger;
|
private readonly ILogger<NotificationHubPushRegistrationService> _logger;
|
||||||
private Dictionary<NotificationHubType, NotificationHubClient> _clients = [];
|
|
||||||
|
|
||||||
public NotificationHubPushRegistrationService(
|
public NotificationHubPushRegistrationService(
|
||||||
IInstallationDeviceRepository installationDeviceRepository,
|
IInstallationDeviceRepository installationDeviceRepository,
|
||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
|
INotificationHubPool notificationHubPool,
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
ILogger<NotificationHubPushRegistrationService> logger)
|
ILogger<NotificationHubPushRegistrationService> logger)
|
||||||
{
|
{
|
||||||
_installationDeviceRepository = installationDeviceRepository;
|
_installationDeviceRepository = installationDeviceRepository;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
|
_notificationHubPool = notificationHubPool;
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
|
||||||
// Is this dirty to do in the ctor?
|
|
||||||
void addHub(NotificationHubType type)
|
|
||||||
{
|
|
||||||
var hubRegistration = globalSettings.NotificationHubs.FirstOrDefault(
|
|
||||||
h => h.HubType == type && h.EnableRegistration);
|
|
||||||
if (hubRegistration != null)
|
|
||||||
{
|
|
||||||
var client = NotificationHubClient.CreateClientFromConnectionString(
|
|
||||||
hubRegistration.ConnectionString,
|
|
||||||
hubRegistration.HubName,
|
|
||||||
hubRegistration.EnableSendTracing);
|
|
||||||
_clients.Add(type, client);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addHub(NotificationHubType.General);
|
|
||||||
addHub(NotificationHubType.iOS);
|
|
||||||
addHub(NotificationHubType.Android);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
|
public async Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
|
||||||
@ -117,7 +101,7 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
|||||||
BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate,
|
BuildInstallationTemplate(installation, "badgeMessage", badgeMessageTemplate ?? messageTemplate,
|
||||||
userId, identifier);
|
userId, identifier);
|
||||||
|
|
||||||
await GetClient(type).CreateOrUpdateInstallationAsync(installation);
|
await ClientFor(GetComb(deviceId)).CreateOrUpdateInstallationAsync(installation);
|
||||||
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
||||||
{
|
{
|
||||||
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
|
await _installationDeviceRepository.UpsertAsync(new InstallationDeviceEntity(deviceId));
|
||||||
@ -152,11 +136,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
|||||||
installation.Templates.Add(fullTemplateId, template);
|
installation.Templates.Add(fullTemplateId, template);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteRegistrationAsync(string deviceId, DeviceType deviceType)
|
public async Task DeleteRegistrationAsync(string deviceId)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await GetClient(deviceType).DeleteInstallationAsync(deviceId);
|
await ClientFor(GetComb(deviceId)).DeleteInstallationAsync(deviceId);
|
||||||
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
if (InstallationDeviceEntity.IsInstallationDeviceId(deviceId))
|
||||||
{
|
{
|
||||||
await _installationDeviceRepository.DeleteAsync(new InstallationDeviceEntity(deviceId));
|
await _installationDeviceRepository.DeleteAsync(new InstallationDeviceEntity(deviceId));
|
||||||
@ -168,31 +152,31 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
public async Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
|
||||||
{
|
{
|
||||||
await PatchTagsForUserDevicesAsync(devices, UpdateOperationType.Add, $"organizationId:{organizationId}");
|
await PatchTagsForUserDevicesAsync(deviceIds, UpdateOperationType.Add, $"organizationId:{organizationId}");
|
||||||
if (devices.Any() && InstallationDeviceEntity.IsInstallationDeviceId(devices.First().Key))
|
if (deviceIds.Any() && InstallationDeviceEntity.IsInstallationDeviceId(deviceIds.First()))
|
||||||
{
|
{
|
||||||
var entities = devices.Select(e => new InstallationDeviceEntity(e.Key));
|
var entities = deviceIds.Select(e => new InstallationDeviceEntity(e));
|
||||||
await _installationDeviceRepository.UpsertManyAsync(entities.ToList());
|
await _installationDeviceRepository.UpsertManyAsync(entities.ToList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
public async Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
|
||||||
{
|
{
|
||||||
await PatchTagsForUserDevicesAsync(devices, UpdateOperationType.Remove,
|
await PatchTagsForUserDevicesAsync(deviceIds, UpdateOperationType.Remove,
|
||||||
$"organizationId:{organizationId}");
|
$"organizationId:{organizationId}");
|
||||||
if (devices.Any() && InstallationDeviceEntity.IsInstallationDeviceId(devices.First().Key))
|
if (deviceIds.Any() && InstallationDeviceEntity.IsInstallationDeviceId(deviceIds.First()))
|
||||||
{
|
{
|
||||||
var entities = devices.Select(e => new InstallationDeviceEntity(e.Key));
|
var entities = deviceIds.Select(e => new InstallationDeviceEntity(e));
|
||||||
await _installationDeviceRepository.UpsertManyAsync(entities.ToList());
|
await _installationDeviceRepository.UpsertManyAsync(entities.ToList());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task PatchTagsForUserDevicesAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, UpdateOperationType op,
|
private async Task PatchTagsForUserDevicesAsync(IEnumerable<string> deviceIds, UpdateOperationType op,
|
||||||
string tag)
|
string tag)
|
||||||
{
|
{
|
||||||
if (!devices.Any())
|
if (!deviceIds.Any())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -212,11 +196,11 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
|||||||
operation.Path += $"/{tag}";
|
operation.Path += $"/{tag}";
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var device in devices)
|
foreach (var deviceId in deviceIds)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await GetClient(device.Value).PatchInstallationAsync(device.Key, new List<PartialUpdateOperation> { operation });
|
await ClientFor(GetComb(deviceId)).PatchInstallationAsync(deviceId, new List<PartialUpdateOperation> { operation });
|
||||||
}
|
}
|
||||||
catch (Exception e) when (e.InnerException == null || !e.InnerException.Message.Contains("(404) Not Found"))
|
catch (Exception e) when (e.InnerException == null || !e.InnerException.Message.Contains("(404) Not Found"))
|
||||||
{
|
{
|
||||||
@ -225,53 +209,29 @@ public class NotificationHubPushRegistrationService : IPushRegistrationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private NotificationHubClient GetClient(DeviceType deviceType)
|
private NotificationHubClient ClientFor(Guid deviceId)
|
||||||
{
|
{
|
||||||
var hubType = NotificationHubType.General;
|
return _notificationHubPool.ClientFor(deviceId);
|
||||||
switch (deviceType)
|
}
|
||||||
|
|
||||||
|
private Guid GetComb(string deviceId)
|
||||||
|
{
|
||||||
|
var deviceIdString = deviceId;
|
||||||
|
InstallationDeviceEntity installationDeviceEntity;
|
||||||
|
Guid deviceIdGuid;
|
||||||
|
if (InstallationDeviceEntity.TryParse(deviceIdString, out installationDeviceEntity))
|
||||||
{
|
{
|
||||||
case DeviceType.Android:
|
// Strip off the installation id (PartitionId). RowKey is the ID in the Installation's table.
|
||||||
hubType = NotificationHubType.Android;
|
deviceIdString = installationDeviceEntity.RowKey;
|
||||||
break;
|
|
||||||
case DeviceType.iOS:
|
|
||||||
hubType = NotificationHubType.iOS;
|
|
||||||
break;
|
|
||||||
case DeviceType.ChromeExtension:
|
|
||||||
case DeviceType.FirefoxExtension:
|
|
||||||
case DeviceType.OperaExtension:
|
|
||||||
case DeviceType.EdgeExtension:
|
|
||||||
case DeviceType.VivaldiExtension:
|
|
||||||
case DeviceType.SafariExtension:
|
|
||||||
hubType = NotificationHubType.GeneralBrowserExtension;
|
|
||||||
break;
|
|
||||||
case DeviceType.WindowsDesktop:
|
|
||||||
case DeviceType.MacOsDesktop:
|
|
||||||
case DeviceType.LinuxDesktop:
|
|
||||||
hubType = NotificationHubType.GeneralDesktop;
|
|
||||||
break;
|
|
||||||
case DeviceType.ChromeBrowser:
|
|
||||||
case DeviceType.FirefoxBrowser:
|
|
||||||
case DeviceType.OperaBrowser:
|
|
||||||
case DeviceType.EdgeBrowser:
|
|
||||||
case DeviceType.IEBrowser:
|
|
||||||
case DeviceType.UnknownBrowser:
|
|
||||||
case DeviceType.SafariBrowser:
|
|
||||||
case DeviceType.VivaldiBrowser:
|
|
||||||
hubType = NotificationHubType.GeneralWeb;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_clients.ContainsKey(hubType))
|
if (Guid.TryParse(deviceIdString, out deviceIdGuid))
|
||||||
{
|
{
|
||||||
_logger.LogWarning("No hub client for '{0}'. Using general hub instead.", hubType);
|
|
||||||
hubType = NotificationHubType.General;
|
|
||||||
if (!_clients.ContainsKey(hubType))
|
|
||||||
{
|
|
||||||
throw new Exception("No general hub client found.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return _clients[hubType];
|
else
|
||||||
|
{
|
||||||
|
throw new Exception($"Invalid device id {deviceId}.");
|
||||||
|
}
|
||||||
|
return deviceIdGuid;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -17,15 +17,18 @@ public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseComman
|
|||||||
private readonly ILicensingService _licensingService;
|
private readonly ILicensingService _licensingService;
|
||||||
private readonly IGlobalSettings _globalSettings;
|
private readonly IGlobalSettings _globalSettings;
|
||||||
private readonly IOrganizationService _organizationService;
|
private readonly IOrganizationService _organizationService;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
|
||||||
public UpdateOrganizationLicenseCommand(
|
public UpdateOrganizationLicenseCommand(
|
||||||
ILicensingService licensingService,
|
ILicensingService licensingService,
|
||||||
IGlobalSettings globalSettings,
|
IGlobalSettings globalSettings,
|
||||||
IOrganizationService organizationService)
|
IOrganizationService organizationService,
|
||||||
|
IFeatureService featureService)
|
||||||
{
|
{
|
||||||
_licensingService = licensingService;
|
_licensingService = licensingService;
|
||||||
_globalSettings = globalSettings;
|
_globalSettings = globalSettings;
|
||||||
_organizationService = organizationService;
|
_organizationService = organizationService;
|
||||||
|
_featureService = featureService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateLicenseAsync(SelfHostedOrganizationDetails selfHostedOrganization,
|
public async Task UpdateLicenseAsync(SelfHostedOrganizationDetails selfHostedOrganization,
|
||||||
@ -59,7 +62,8 @@ public class UpdateOrganizationLicenseCommand : IUpdateOrganizationLicenseComman
|
|||||||
private async Task UpdateOrganizationAsync(SelfHostedOrganizationDetails selfHostedOrganizationDetails, OrganizationLicense license)
|
private async Task UpdateOrganizationAsync(SelfHostedOrganizationDetails selfHostedOrganizationDetails, OrganizationLicense license)
|
||||||
{
|
{
|
||||||
var organization = selfHostedOrganizationDetails.ToOrganization();
|
var organization = selfHostedOrganizationDetails.ToOrganization();
|
||||||
organization.UpdateFromLicense(license);
|
|
||||||
|
organization.UpdateFromLicense(license, _featureService);
|
||||||
|
|
||||||
await _organizationService.ReplaceAndUpdateCacheAsync(organization);
|
await _organizationService.ReplaceAndUpdateCacheAsync(organization);
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ public interface IPushRegistrationService
|
|||||||
{
|
{
|
||||||
Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
|
Task CreateOrUpdateRegistrationAsync(string pushToken, string deviceId, string userId,
|
||||||
string identifier, DeviceType type);
|
string identifier, DeviceType type);
|
||||||
Task DeleteRegistrationAsync(string deviceId, DeviceType type);
|
Task DeleteRegistrationAsync(string deviceId);
|
||||||
Task AddUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId);
|
Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId);
|
||||||
Task DeleteUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId);
|
Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId);
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,8 @@ public interface IStripeAdapter
|
|||||||
Task<Stripe.Customer> CustomerUpdateAsync(string id, Stripe.CustomerUpdateOptions options = null);
|
Task<Stripe.Customer> CustomerUpdateAsync(string id, Stripe.CustomerUpdateOptions options = null);
|
||||||
Task<Stripe.Customer> CustomerDeleteAsync(string id);
|
Task<Stripe.Customer> CustomerDeleteAsync(string id);
|
||||||
Task<List<PaymentMethod>> CustomerListPaymentMethods(string id, CustomerListPaymentMethodsOptions options = null);
|
Task<List<PaymentMethod>> CustomerListPaymentMethods(string id, CustomerListPaymentMethodsOptions options = null);
|
||||||
|
Task<CustomerBalanceTransaction> CustomerBalanceTransactionCreate(string customerId,
|
||||||
|
CustomerBalanceTransactionCreateOptions options);
|
||||||
Task<Stripe.Subscription> SubscriptionCreateAsync(Stripe.SubscriptionCreateOptions subscriptionCreateOptions);
|
Task<Stripe.Subscription> SubscriptionCreateAsync(Stripe.SubscriptionCreateOptions subscriptionCreateOptions);
|
||||||
Task<Stripe.Subscription> SubscriptionGetAsync(string id, Stripe.SubscriptionGetOptions options = null);
|
Task<Stripe.Subscription> SubscriptionGetAsync(string id, Stripe.SubscriptionGetOptions options = null);
|
||||||
Task<List<Stripe.Subscription>> SubscriptionListAsync(StripeSubscriptionListOptions subscriptionSearchOptions);
|
Task<List<Stripe.Subscription>> SubscriptionListAsync(StripeSubscriptionListOptions subscriptionSearchOptions);
|
||||||
|
@ -38,13 +38,13 @@ public class DeviceService : IDeviceService
|
|||||||
public async Task ClearTokenAsync(Device device)
|
public async Task ClearTokenAsync(Device device)
|
||||||
{
|
{
|
||||||
await _deviceRepository.ClearPushTokenAsync(device.Id);
|
await _deviceRepository.ClearPushTokenAsync(device.Id);
|
||||||
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString(), device.Type);
|
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteAsync(Device device)
|
public async Task DeleteAsync(Device device)
|
||||||
{
|
{
|
||||||
await _deviceRepository.DeleteAsync(device);
|
await _deviceRepository.DeleteAsync(device);
|
||||||
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString(), device.Type);
|
await _pushRegistrationService.DeleteRegistrationAsync(device.Id.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task UpdateDevicesTrustAsync(string currentDeviceIdentifier,
|
public async Task UpdateDevicesTrustAsync(string currentDeviceIdentifier,
|
||||||
|
@ -1,61 +1,31 @@
|
|||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Repositories;
|
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Tools.Entities;
|
using Bit.Core.Tools.Entities;
|
||||||
using Bit.Core.Utilities;
|
|
||||||
using Bit.Core.Vault.Entities;
|
using Bit.Core.Vault.Entities;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Bit.Core.Services;
|
namespace Bit.Core.Services;
|
||||||
|
|
||||||
public class MultiServicePushNotificationService : IPushNotificationService
|
public class MultiServicePushNotificationService : IPushNotificationService
|
||||||
{
|
{
|
||||||
private readonly List<IPushNotificationService> _services = new List<IPushNotificationService>();
|
private readonly IEnumerable<IPushNotificationService> _services;
|
||||||
private readonly ILogger<MultiServicePushNotificationService> _logger;
|
private readonly ILogger<MultiServicePushNotificationService> _logger;
|
||||||
|
|
||||||
public MultiServicePushNotificationService(
|
public MultiServicePushNotificationService(
|
||||||
IHttpClientFactory httpFactory,
|
[FromKeyedServices("implementation")] IEnumerable<IPushNotificationService> services,
|
||||||
IDeviceRepository deviceRepository,
|
|
||||||
IInstallationDeviceRepository installationDeviceRepository,
|
|
||||||
GlobalSettings globalSettings,
|
|
||||||
IHttpContextAccessor httpContextAccessor,
|
|
||||||
ILogger<MultiServicePushNotificationService> logger,
|
ILogger<MultiServicePushNotificationService> logger,
|
||||||
ILogger<RelayPushNotificationService> relayLogger,
|
GlobalSettings globalSettings)
|
||||||
ILogger<NotificationsApiPushNotificationService> hubLogger)
|
|
||||||
{
|
{
|
||||||
if (globalSettings.SelfHosted)
|
_services = services;
|
||||||
{
|
|
||||||
if (CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) &&
|
|
||||||
globalSettings.Installation?.Id != null &&
|
|
||||||
CoreHelpers.SettingHasValue(globalSettings.Installation?.Key))
|
|
||||||
{
|
|
||||||
_services.Add(new RelayPushNotificationService(httpFactory, deviceRepository, globalSettings,
|
|
||||||
httpContextAccessor, relayLogger));
|
|
||||||
}
|
|
||||||
if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) &&
|
|
||||||
CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications))
|
|
||||||
{
|
|
||||||
_services.Add(new NotificationsApiPushNotificationService(
|
|
||||||
httpFactory, globalSettings, httpContextAccessor, hubLogger));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var generalHub = globalSettings.NotificationHubs?.FirstOrDefault(h => h.HubType == NotificationHubType.General);
|
|
||||||
if (CoreHelpers.SettingHasValue(generalHub?.ConnectionString))
|
|
||||||
{
|
|
||||||
_services.Add(new NotificationHubPushNotificationService(installationDeviceRepository,
|
|
||||||
globalSettings, httpContextAccessor, hubLogger));
|
|
||||||
}
|
|
||||||
if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString))
|
|
||||||
{
|
|
||||||
_services.Add(new AzureQueuePushNotificationService(globalSettings, httpContextAccessor));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_logger.LogInformation("Hub services: {Services}", _services.Count());
|
||||||
|
globalSettings?.NotificationHubPool?.NotificationHubs?.ForEach(hub =>
|
||||||
|
{
|
||||||
|
_logger.LogInformation("HubName: {HubName}, EnableSendTracing: {EnableSendTracing}, RegistrationStartDate: {RegistrationStartDate}, RegistrationEndDate: {RegistrationEndDate}", hub.HubName, hub.EnableSendTracing, hub.RegistrationStartDate, hub.RegistrationEndDate);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
public Task PushSyncCipherCreateAsync(Cipher cipher, IEnumerable<Guid> collectionIds)
|
||||||
|
@ -38,37 +38,36 @@ public class RelayPushRegistrationService : BaseIdentityClientService, IPushRegi
|
|||||||
await SendAsync(HttpMethod.Post, "push/register", requestModel);
|
await SendAsync(HttpMethod.Post, "push/register", requestModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteRegistrationAsync(string deviceId, DeviceType type)
|
public async Task DeleteRegistrationAsync(string deviceId)
|
||||||
{
|
{
|
||||||
var requestModel = new PushDeviceRequestModel
|
var requestModel = new PushDeviceRequestModel
|
||||||
{
|
{
|
||||||
Id = deviceId,
|
Id = deviceId,
|
||||||
Type = type,
|
|
||||||
};
|
};
|
||||||
await SendAsync(HttpMethod.Post, "push/delete", requestModel);
|
await SendAsync(HttpMethod.Post, "push/delete", requestModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddUserRegistrationOrganizationAsync(
|
public async Task AddUserRegistrationOrganizationAsync(
|
||||||
IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
IEnumerable<string> deviceIds, string organizationId)
|
||||||
{
|
{
|
||||||
if (!devices.Any())
|
if (!deviceIds.Any())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestModel = new PushUpdateRequestModel(devices, organizationId);
|
var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);
|
||||||
await SendAsync(HttpMethod.Put, "push/add-organization", requestModel);
|
await SendAsync(HttpMethod.Put, "push/add-organization", requestModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task DeleteUserRegistrationOrganizationAsync(
|
public async Task DeleteUserRegistrationOrganizationAsync(
|
||||||
IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
IEnumerable<string> deviceIds, string organizationId)
|
||||||
{
|
{
|
||||||
if (!devices.Any())
|
if (!deviceIds.Any())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestModel = new PushUpdateRequestModel(devices, organizationId);
|
var requestModel = new PushUpdateRequestModel(deviceIds, organizationId);
|
||||||
await SendAsync(HttpMethod.Put, "push/delete-organization", requestModel);
|
await SendAsync(HttpMethod.Put, "push/delete-organization", requestModel);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ public class StripeAdapter : IStripeAdapter
|
|||||||
private readonly Stripe.PriceService _priceService;
|
private readonly Stripe.PriceService _priceService;
|
||||||
private readonly Stripe.SetupIntentService _setupIntentService;
|
private readonly Stripe.SetupIntentService _setupIntentService;
|
||||||
private readonly Stripe.TestHelpers.TestClockService _testClockService;
|
private readonly Stripe.TestHelpers.TestClockService _testClockService;
|
||||||
|
private readonly CustomerBalanceTransactionService _customerBalanceTransactionService;
|
||||||
|
|
||||||
public StripeAdapter()
|
public StripeAdapter()
|
||||||
{
|
{
|
||||||
@ -34,6 +35,7 @@ public class StripeAdapter : IStripeAdapter
|
|||||||
_priceService = new Stripe.PriceService();
|
_priceService = new Stripe.PriceService();
|
||||||
_setupIntentService = new SetupIntentService();
|
_setupIntentService = new SetupIntentService();
|
||||||
_testClockService = new Stripe.TestHelpers.TestClockService();
|
_testClockService = new Stripe.TestHelpers.TestClockService();
|
||||||
|
_customerBalanceTransactionService = new CustomerBalanceTransactionService();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task<Stripe.Customer> CustomerCreateAsync(Stripe.CustomerCreateOptions options)
|
public Task<Stripe.Customer> CustomerCreateAsync(Stripe.CustomerCreateOptions options)
|
||||||
@ -63,6 +65,10 @@ public class StripeAdapter : IStripeAdapter
|
|||||||
return paymentMethods.Data;
|
return paymentMethods.Data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<CustomerBalanceTransaction> CustomerBalanceTransactionCreate(string customerId,
|
||||||
|
CustomerBalanceTransactionCreateOptions options)
|
||||||
|
=> await _customerBalanceTransactionService.CreateAsync(customerId, options);
|
||||||
|
|
||||||
public Task<Stripe.Subscription> SubscriptionCreateAsync(Stripe.SubscriptionCreateOptions options)
|
public Task<Stripe.Subscription> SubscriptionCreateAsync(Stripe.SubscriptionCreateOptions options)
|
||||||
{
|
{
|
||||||
return _subscriptionService.CreateAsync(options);
|
return _subscriptionService.CreateAsync(options);
|
||||||
|
@ -4,7 +4,7 @@ namespace Bit.Core.Services;
|
|||||||
|
|
||||||
public class NoopPushRegistrationService : IPushRegistrationService
|
public class NoopPushRegistrationService : IPushRegistrationService
|
||||||
{
|
{
|
||||||
public Task AddUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
public Task AddUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
|
||||||
{
|
{
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
@ -15,12 +15,12 @@ public class NoopPushRegistrationService : IPushRegistrationService
|
|||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task DeleteRegistrationAsync(string deviceId, DeviceType deviceType)
|
public Task DeleteRegistrationAsync(string deviceId)
|
||||||
{
|
{
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task DeleteUserRegistrationOrganizationAsync(IEnumerable<KeyValuePair<string, DeviceType>> devices, string organizationId)
|
public Task DeleteUserRegistrationOrganizationAsync(IEnumerable<string> deviceIds, string organizationId)
|
||||||
{
|
{
|
||||||
return Task.FromResult(0);
|
return Task.FromResult(0);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using Bit.Core.Auth.Settings;
|
using Bit.Core.Auth.Settings;
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Settings.LoggingSettings;
|
using Bit.Core.Settings.LoggingSettings;
|
||||||
|
|
||||||
namespace Bit.Core.Settings;
|
namespace Bit.Core.Settings;
|
||||||
@ -65,7 +64,7 @@ public class GlobalSettings : IGlobalSettings
|
|||||||
public virtual SentrySettings Sentry { get; set; } = new SentrySettings();
|
public virtual SentrySettings Sentry { get; set; } = new SentrySettings();
|
||||||
public virtual SyslogSettings Syslog { get; set; } = new SyslogSettings();
|
public virtual SyslogSettings Syslog { get; set; } = new SyslogSettings();
|
||||||
public virtual ILogLevelSettings MinLogLevel { get; set; } = new LogLevelSettings();
|
public virtual ILogLevelSettings MinLogLevel { get; set; } = new LogLevelSettings();
|
||||||
public virtual List<NotificationHubSettings> NotificationHubs { get; set; } = new();
|
public virtual NotificationHubPoolSettings NotificationHubPool { get; set; } = new();
|
||||||
public virtual YubicoSettings Yubico { get; set; } = new YubicoSettings();
|
public virtual YubicoSettings Yubico { get; set; } = new YubicoSettings();
|
||||||
public virtual DuoSettings Duo { get; set; } = new DuoSettings();
|
public virtual DuoSettings Duo { get; set; } = new DuoSettings();
|
||||||
public virtual BraintreeSettings Braintree { get; set; } = new BraintreeSettings();
|
public virtual BraintreeSettings Braintree { get; set; } = new BraintreeSettings();
|
||||||
@ -424,7 +423,7 @@ public class GlobalSettings : IGlobalSettings
|
|||||||
public string ConnectionString
|
public string ConnectionString
|
||||||
{
|
{
|
||||||
get => _connectionString;
|
get => _connectionString;
|
||||||
set => _connectionString = value.Trim('"');
|
set => _connectionString = value?.Trim('"');
|
||||||
}
|
}
|
||||||
public string HubName { get; set; }
|
public string HubName { get; set; }
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -433,10 +432,32 @@ public class GlobalSettings : IGlobalSettings
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableSendTracing { get; set; } = false;
|
public bool EnableSendTracing { get; set; } = false;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// At least one hub configuration should have registration enabled, preferably the General hub as a safety net.
|
/// The date and time at which registration will be enabled.
|
||||||
|
///
|
||||||
|
/// **This value should not be updated once set, as it is used to determine installation location of devices.**
|
||||||
|
///
|
||||||
|
/// If null, registration is disabled.
|
||||||
|
///
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableRegistration { get; set; }
|
public DateTime? RegistrationStartDate { get; set; }
|
||||||
public NotificationHubType HubType { get; set; }
|
/// <summary>
|
||||||
|
/// The date and time at which registration will be disabled.
|
||||||
|
///
|
||||||
|
/// **This value should not be updated once set, as it is used to determine installation location of devices.**
|
||||||
|
///
|
||||||
|
/// If null, hub registration has no yet known expiry.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? RegistrationEndDate { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class NotificationHubPoolSettings
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// List of Notification Hub settings to use for sending push notifications.
|
||||||
|
///
|
||||||
|
/// Note that hubs on the same namespace share active device limits, so multiple namespaces should be used to increase capacity.
|
||||||
|
/// </summary>
|
||||||
|
public List<NotificationHubSettings> NotificationHubs { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class YubicoSettings
|
public class YubicoSettings
|
||||||
|
@ -76,6 +76,39 @@ public static class CoreHelpers
|
|||||||
return new Guid(guidArray);
|
return new Guid(guidArray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static DateTime DateFromComb(Guid combGuid)
|
||||||
|
{
|
||||||
|
var guidArray = combGuid.ToByteArray();
|
||||||
|
var daysArray = new byte[4];
|
||||||
|
var msecsArray = new byte[4];
|
||||||
|
|
||||||
|
Array.Copy(guidArray, guidArray.Length - 6, daysArray, 2, 2);
|
||||||
|
Array.Copy(guidArray, guidArray.Length - 4, msecsArray, 0, 4);
|
||||||
|
|
||||||
|
Array.Reverse(daysArray);
|
||||||
|
Array.Reverse(msecsArray);
|
||||||
|
|
||||||
|
var days = BitConverter.ToInt32(daysArray, 0);
|
||||||
|
var msecs = BitConverter.ToInt32(msecsArray, 0);
|
||||||
|
|
||||||
|
var time = TimeSpan.FromDays(days) + TimeSpan.FromMilliseconds(msecs * 3.333333);
|
||||||
|
return new DateTime(_baseDateTicks + time.Ticks, DateTimeKind.Utc);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static long BinForComb(Guid combGuid, int binCount)
|
||||||
|
{
|
||||||
|
// From System.Web.Util.HashCodeCombiner
|
||||||
|
uint CombineHashCodes(uint h1, byte h2)
|
||||||
|
{
|
||||||
|
return (uint)(((h1 << 5) + h1) ^ h2);
|
||||||
|
}
|
||||||
|
var guidArray = combGuid.ToByteArray();
|
||||||
|
var randomArray = new byte[10];
|
||||||
|
Array.Copy(guidArray, 0, randomArray, 0, 10);
|
||||||
|
var hash = randomArray.Aggregate((uint)randomArray.Length, CombineHashCodes);
|
||||||
|
return hash % binCount;
|
||||||
|
}
|
||||||
|
|
||||||
public static string CleanCertificateThumbprint(string thumbprint)
|
public static string CleanCertificateThumbprint(string thumbprint)
|
||||||
{
|
{
|
||||||
// Clean possible garbage characters from thumbprint copy/paste
|
// Clean possible garbage characters from thumbprint copy/paste
|
||||||
|
@ -9,10 +9,6 @@ namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
|||||||
|
|
||||||
public class Organization : Core.AdminConsole.Entities.Organization
|
public class Organization : Core.AdminConsole.Entities.Organization
|
||||||
{
|
{
|
||||||
// Shadow properties - to be introduced by https://bitwarden.atlassian.net/browse/PM-10863
|
|
||||||
public bool LimitCollectionCreation { get => LimitCollectionCreationDeletion; set => LimitCollectionCreationDeletion = value; }
|
|
||||||
public bool LimitCollectionDeletion { get => LimitCollectionCreationDeletion; set => LimitCollectionCreationDeletion = value; }
|
|
||||||
|
|
||||||
public virtual ICollection<Cipher> Ciphers { get; set; }
|
public virtual ICollection<Cipher> Ciphers { get; set; }
|
||||||
public virtual ICollection<OrganizationUser> OrganizationUsers { get; set; }
|
public virtual ICollection<OrganizationUser> OrganizationUsers { get; set; }
|
||||||
public virtual ICollection<Group> Groups { get; set; }
|
public virtual ICollection<Group> Groups { get; set; }
|
||||||
@ -42,9 +38,6 @@ public class OrganizationMapperProfile : Profile
|
|||||||
.ForMember(org => org.ApiKeys, opt => opt.Ignore())
|
.ForMember(org => org.ApiKeys, opt => opt.Ignore())
|
||||||
.ForMember(org => org.Connections, opt => opt.Ignore())
|
.ForMember(org => org.Connections, opt => opt.Ignore())
|
||||||
.ForMember(org => org.Domains, opt => opt.Ignore())
|
.ForMember(org => org.Domains, opt => opt.Ignore())
|
||||||
// Shadow properties - to be introduced by https://bitwarden.atlassian.net/browse/PM-10863
|
|
||||||
.ForMember(org => org.LimitCollectionCreation, opt => opt.Ignore())
|
|
||||||
.ForMember(org => org.LimitCollectionDeletion, opt => opt.Ignore())
|
|
||||||
.ReverseMap();
|
.ReverseMap();
|
||||||
|
|
||||||
CreateProjection<Organization, SelfHostedOrganizationDetails>()
|
CreateProjection<Organization, SelfHostedOrganizationDetails>()
|
||||||
|
@ -99,6 +99,9 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
|
|||||||
UseScim = e.UseScim,
|
UseScim = e.UseScim,
|
||||||
UseCustomPermissions = e.UseCustomPermissions,
|
UseCustomPermissions = e.UseCustomPermissions,
|
||||||
UsePolicies = e.UsePolicies,
|
UsePolicies = e.UsePolicies,
|
||||||
|
LimitCollectionCreation = e.LimitCollectionCreation,
|
||||||
|
LimitCollectionDeletion = e.LimitCollectionDeletion,
|
||||||
|
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||||
LimitCollectionCreationDeletion = e.LimitCollectionCreationDeletion,
|
LimitCollectionCreationDeletion = e.LimitCollectionCreationDeletion,
|
||||||
AllowAdminAccessToAllCollectionItems = e.AllowAdminAccessToAllCollectionItems
|
AllowAdminAccessToAllCollectionItems = e.AllowAdminAccessToAllCollectionItems
|
||||||
}).ToListAsync();
|
}).ToListAsync();
|
||||||
|
@ -66,6 +66,9 @@ public class OrganizationUserOrganizationDetailsViewQuery : IQuery<OrganizationU
|
|||||||
UsePasswordManager = o.UsePasswordManager,
|
UsePasswordManager = o.UsePasswordManager,
|
||||||
SmSeats = o.SmSeats,
|
SmSeats = o.SmSeats,
|
||||||
SmServiceAccounts = o.SmServiceAccounts,
|
SmServiceAccounts = o.SmServiceAccounts,
|
||||||
|
LimitCollectionCreation = o.LimitCollectionCreation,
|
||||||
|
LimitCollectionDeletion = o.LimitCollectionDeletion,
|
||||||
|
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||||
LimitCollectionCreationDeletion = o.LimitCollectionCreationDeletion,
|
LimitCollectionCreationDeletion = o.LimitCollectionCreationDeletion,
|
||||||
AllowAdminAccessToAllCollectionItems = o.AllowAdminAccessToAllCollectionItems,
|
AllowAdminAccessToAllCollectionItems = o.AllowAdminAccessToAllCollectionItems,
|
||||||
};
|
};
|
||||||
|
@ -44,6 +44,9 @@ public class ProviderUserOrganizationDetailsViewQuery : IQuery<ProviderUserOrgan
|
|||||||
ProviderId = x.p.Id,
|
ProviderId = x.p.Id,
|
||||||
ProviderName = x.p.Name,
|
ProviderName = x.p.Name,
|
||||||
PlanType = x.o.PlanType,
|
PlanType = x.o.PlanType,
|
||||||
|
LimitCollectionCreation = x.o.LimitCollectionCreation,
|
||||||
|
LimitCollectionDeletion = x.o.LimitCollectionDeletion,
|
||||||
|
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
|
||||||
LimitCollectionCreationDeletion = x.o.LimitCollectionCreationDeletion,
|
LimitCollectionCreationDeletion = x.o.LimitCollectionCreationDeletion,
|
||||||
AllowAdminAccessToAllCollectionItems = x.o.AllowAdminAccessToAllCollectionItems,
|
AllowAdminAccessToAllCollectionItems = x.o.AllowAdminAccessToAllCollectionItems,
|
||||||
});
|
});
|
||||||
|
@ -4,6 +4,7 @@ using System.Security.Claims;
|
|||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using AspNetCoreRateLimit;
|
using AspNetCoreRateLimit;
|
||||||
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
using Bit.Core.AdminConsole.Models.Business.Tokenables;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.AdminConsole.Services.Implementations;
|
using Bit.Core.AdminConsole.Services.Implementations;
|
||||||
using Bit.Core.AdminConsole.Services.NoopImplementations;
|
using Bit.Core.AdminConsole.Services.NoopImplementations;
|
||||||
@ -24,6 +25,7 @@ using Bit.Core.Enums;
|
|||||||
using Bit.Core.HostedServices;
|
using Bit.Core.HostedServices;
|
||||||
using Bit.Core.Identity;
|
using Bit.Core.Identity;
|
||||||
using Bit.Core.IdentityServer;
|
using Bit.Core.IdentityServer;
|
||||||
|
using Bit.Core.NotificationHub;
|
||||||
using Bit.Core.OrganizationFeatures;
|
using Bit.Core.OrganizationFeatures;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
using Bit.Core.Resources;
|
using Bit.Core.Resources;
|
||||||
@ -102,9 +104,9 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddUserServices(globalSettings);
|
services.AddUserServices(globalSettings);
|
||||||
services.AddTrialInitiationServices();
|
services.AddTrialInitiationServices();
|
||||||
services.AddOrganizationServices(globalSettings);
|
services.AddOrganizationServices(globalSettings);
|
||||||
|
services.AddPolicyServices();
|
||||||
services.AddScoped<ICollectionService, CollectionService>();
|
services.AddScoped<ICollectionService, CollectionService>();
|
||||||
services.AddScoped<IGroupService, GroupService>();
|
services.AddScoped<IGroupService, GroupService>();
|
||||||
services.AddScoped<IPolicyService, PolicyService>();
|
|
||||||
services.AddScoped<IEventService, EventService>();
|
services.AddScoped<IEventService, EventService>();
|
||||||
services.AddScoped<IEmergencyAccessService, EmergencyAccessService>();
|
services.AddScoped<IEmergencyAccessService, EmergencyAccessService>();
|
||||||
services.AddSingleton<IDeviceService, DeviceService>();
|
services.AddSingleton<IDeviceService, DeviceService>();
|
||||||
@ -263,16 +265,30 @@ public static class ServiceCollectionExtensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
services.AddSingleton<IPushNotificationService, MultiServicePushNotificationService>();
|
services.AddSingleton<IPushNotificationService, MultiServicePushNotificationService>();
|
||||||
if (globalSettings.SelfHosted &&
|
if (globalSettings.SelfHosted)
|
||||||
CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) &&
|
|
||||||
globalSettings.Installation?.Id != null &&
|
|
||||||
CoreHelpers.SettingHasValue(globalSettings.Installation?.Key))
|
|
||||||
{
|
{
|
||||||
services.AddSingleton<IPushRegistrationService, RelayPushRegistrationService>();
|
if (CoreHelpers.SettingHasValue(globalSettings.PushRelayBaseUri) &&
|
||||||
|
globalSettings.Installation?.Id != null &&
|
||||||
|
CoreHelpers.SettingHasValue(globalSettings.Installation?.Key))
|
||||||
|
{
|
||||||
|
services.AddKeyedSingleton<IPushNotificationService, RelayPushNotificationService>("implementation");
|
||||||
|
services.AddSingleton<IPushRegistrationService, RelayPushRegistrationService>();
|
||||||
|
}
|
||||||
|
if (CoreHelpers.SettingHasValue(globalSettings.InternalIdentityKey) &&
|
||||||
|
CoreHelpers.SettingHasValue(globalSettings.BaseServiceUri.InternalNotifications))
|
||||||
|
{
|
||||||
|
services.AddKeyedSingleton<IPushNotificationService, NotificationsApiPushNotificationService>("implementation");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else if (!globalSettings.SelfHosted)
|
else if (!globalSettings.SelfHosted)
|
||||||
{
|
{
|
||||||
|
services.AddSingleton<INotificationHubPool, NotificationHubPool>();
|
||||||
services.AddSingleton<IPushRegistrationService, NotificationHubPushRegistrationService>();
|
services.AddSingleton<IPushRegistrationService, NotificationHubPushRegistrationService>();
|
||||||
|
services.AddKeyedSingleton<IPushNotificationService, NotificationHubPushNotificationService>("implementation");
|
||||||
|
if (CoreHelpers.SettingHasValue(globalSettings.Notifications?.ConnectionString))
|
||||||
|
{
|
||||||
|
services.AddKeyedSingleton<IPushNotificationService, AzureQueuePushNotificationService>("implementation");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
@ -12,18 +12,28 @@ BEGIN
|
|||||||
[dbo].[Collection] S ON S.[Id] = CC.[CollectionId]
|
[dbo].[Collection] S ON S.[Id] = CC.[CollectionId]
|
||||||
INNER JOIN
|
INNER JOIN
|
||||||
[dbo].[OrganizationUser] OU ON OU.[OrganizationId] = S.[OrganizationId] AND OU.[UserId] = @UserId
|
[dbo].[OrganizationUser] OU ON OU.[OrganizationId] = S.[OrganizationId] AND OU.[UserId] = @UserId
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[CollectionUser] CU ON CU.[CollectionId] = S.[Id] AND CU.[OrganizationUserId] = OU.[Id]
|
||||||
|
WHERE
|
||||||
|
OU.[Status] = 2
|
||||||
|
|
||||||
|
UNION ALL
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
CC.*
|
||||||
|
FROM
|
||||||
|
[dbo].[CollectionCipher] CC
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[Collection] S ON S.[Id] = CC.[CollectionId]
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[OrganizationUser] OU ON OU.[OrganizationId] = S.[OrganizationId] AND OU.[UserId] = @UserId
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[GroupUser] GU ON GU.[OrganizationUserId] = OU.[Id]
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[CollectionGroup] CG ON CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId]
|
||||||
LEFT JOIN
|
LEFT JOIN
|
||||||
[dbo].[CollectionUser] CU ON CU.[CollectionId] = S.[Id] AND CU.[OrganizationUserId] = OU.[Id]
|
[dbo].[CollectionUser] CU ON CU.[CollectionId] = S.[Id] AND CU.[OrganizationUserId] = OU.[Id]
|
||||||
LEFT JOIN
|
|
||||||
[dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id]
|
|
||||||
LEFT JOIN
|
|
||||||
[dbo].[Group] G ON G.[Id] = GU.[GroupId]
|
|
||||||
LEFT JOIN
|
|
||||||
[dbo].[CollectionGroup] CG ON CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId]
|
|
||||||
WHERE
|
WHERE
|
||||||
OU.[Status] = 2 -- Confirmed
|
OU.[Status] = 2
|
||||||
AND (
|
AND CU.[CollectionId] IS NULL
|
||||||
CU.[CollectionId] IS NOT NULL
|
|
||||||
OR CG.[CollectionId] IS NOT NULL
|
|
||||||
)
|
|
||||||
END
|
END
|
||||||
|
@ -229,13 +229,13 @@ public class OrganizationDomainControllerTests
|
|||||||
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
sutProvider.GetDependency<IOrganizationDomainRepository>()
|
||||||
.GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId)
|
.GetDomainByIdOrganizationIdAsync(organizationDomain.Id, organizationDomain.OrganizationId)
|
||||||
.Returns(organizationDomain);
|
.Returns(organizationDomain);
|
||||||
sutProvider.GetDependency<IVerifyOrganizationDomainCommand>().VerifyOrganizationDomainAsync(organizationDomain)
|
sutProvider.GetDependency<IVerifyOrganizationDomainCommand>().UserVerifyOrganizationDomainAsync(organizationDomain)
|
||||||
.Returns(new OrganizationDomain());
|
.Returns(new OrganizationDomain());
|
||||||
|
|
||||||
var result = await sutProvider.Sut.Verify(organizationDomain.OrganizationId, organizationDomain.Id);
|
var result = await sutProvider.Sut.Verify(organizationDomain.OrganizationId, organizationDomain.Id);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IVerifyOrganizationDomainCommand>().Received(1)
|
await sutProvider.GetDependency<IVerifyOrganizationDomainCommand>().Received(1)
|
||||||
.VerifyOrganizationDomainAsync(organizationDomain);
|
.UserVerifyOrganizationDomainAsync(organizationDomain);
|
||||||
Assert.IsType<OrganizationDomainResponseModel>(result);
|
Assert.IsType<OrganizationDomainResponseModel>(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ public class OrganizationBillingControllerTests
|
|||||||
{
|
{
|
||||||
sutProvider.GetDependency<ICurrentContext>().AccessMembersTab(organizationId).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>().AccessMembersTab(organizationId).Returns(true);
|
||||||
sutProvider.GetDependency<IOrganizationBillingService>().GetMetadata(organizationId)
|
sutProvider.GetDependency<IOrganizationBillingService>().GetMetadata(organizationId)
|
||||||
.Returns(new OrganizationMetadata(true));
|
.Returns(new OrganizationMetadata(true, true));
|
||||||
|
|
||||||
var result = await sutProvider.Sut.GetMetadataAsync(organizationId);
|
var result = await sutProvider.Sut.GetMetadataAsync(organizationId);
|
||||||
|
|
||||||
@ -60,6 +60,7 @@ public class OrganizationBillingControllerTests
|
|||||||
|
|
||||||
var organizationMetadataResponse = ((Ok<OrganizationMetadataResponse>)result).Value;
|
var organizationMetadataResponse = ((Ok<OrganizationMetadataResponse>)result).Value;
|
||||||
|
|
||||||
|
Assert.True(organizationMetadataResponse.IsEligibleForSelfHost);
|
||||||
Assert.True(organizationMetadataResponse.IsOnSecretsManagerStandalone);
|
Assert.True(organizationMetadataResponse.IsOnSecretsManagerStandalone);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
using Bit.Api.Vault.AuthorizationHandlers.Collections;
|
||||||
|
using Bit.Core;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -32,7 +33,10 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
organization.Type = userType;
|
organization.Type = userType;
|
||||||
organization.Permissions = new Permissions();
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
ArrangeOrganizationAbility(sutProvider, organization, true);
|
// `LimitCollectonCreationDeletionSplit` feature flag state isn't
|
||||||
|
// relevant for this test. The flag is never checked for in this
|
||||||
|
// test. This is asserted below.
|
||||||
|
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { BulkCollectionOperations.Create },
|
new[] { BulkCollectionOperations.Create },
|
||||||
@ -44,11 +48,12 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
Assert.True(context.HasSucceeded);
|
Assert.True(context.HasSucceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData, CollectionCustomization]
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
public async Task CanCreateAsync_WhenUser_WithLimitCollectionCreationDeletionFalse_Success(
|
public async Task CanCreateAsync_WhenUser_WithLimitCollectionCreationFalse_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
|
||||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
ICollection<Collection> collections,
|
ICollection<Collection> collections,
|
||||||
CurrentContextOrganization organization)
|
CurrentContextOrganization organization)
|
||||||
@ -57,7 +62,7 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
|
|
||||||
organization.Type = OrganizationUserType.User;
|
organization.Type = OrganizationUserType.User;
|
||||||
|
|
||||||
ArrangeOrganizationAbility(sutProvider, organization, false);
|
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, false, false);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { BulkCollectionOperations.Create },
|
new[] { BulkCollectionOperations.Create },
|
||||||
@ -66,16 +71,49 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)
|
||||||
|
.Returns(false);
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
|
Assert.True(context.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
|
public async Task CanCreateAsync_WhenUser_WithLimitCollectionCreationFalse_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success(
|
||||||
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
|
ICollection<Collection> collections,
|
||||||
|
CurrentContextOrganization organization)
|
||||||
|
{
|
||||||
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
organization.Type = OrganizationUserType.User;
|
||||||
|
|
||||||
|
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, false, false);
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(
|
||||||
|
new[] { BulkCollectionOperations.Create },
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
collections);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
Assert.True(context.HasSucceeded);
|
Assert.True(context.HasSucceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, CollectionCustomization]
|
[Theory, CollectionCustomization]
|
||||||
[BitAutoData(OrganizationUserType.User)]
|
[BitAutoData(OrganizationUserType.User)]
|
||||||
[BitAutoData(OrganizationUserType.Custom)]
|
[BitAutoData(OrganizationUserType.Custom)]
|
||||||
public async Task CanCreateAsync_WhenMissingPermissions_NoSuccess(
|
public async Task CanCreateAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureDisabled_NoSuccess(
|
||||||
OrganizationUserType userType,
|
OrganizationUserType userType,
|
||||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
ICollection<Collection> collections,
|
ICollection<Collection> collections,
|
||||||
@ -92,7 +130,7 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
ManageUsers = false
|
ManageUsers = false
|
||||||
};
|
};
|
||||||
|
|
||||||
ArrangeOrganizationAbility(sutProvider, organization, true);
|
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { BulkCollectionOperations.Create },
|
new[] { BulkCollectionOperations.Create },
|
||||||
@ -102,21 +140,61 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
|
Assert.False(context.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, CollectionCustomization]
|
||||||
|
[BitAutoData(OrganizationUserType.User)]
|
||||||
|
[BitAutoData(OrganizationUserType.Custom)]
|
||||||
|
public async Task CanCreateAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureEnabled_NoSuccess(
|
||||||
|
OrganizationUserType userType,
|
||||||
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
|
ICollection<Collection> collections,
|
||||||
|
CurrentContextOrganization organization)
|
||||||
|
{
|
||||||
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
organization.Type = userType;
|
||||||
|
organization.Permissions = new Permissions
|
||||||
|
{
|
||||||
|
EditAnyCollection = false,
|
||||||
|
DeleteAnyCollection = false,
|
||||||
|
ManageGroups = false,
|
||||||
|
ManageUsers = false
|
||||||
|
};
|
||||||
|
|
||||||
|
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(
|
||||||
|
new[] { BulkCollectionOperations.Create },
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
collections);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
Assert.False(context.HasSucceeded);
|
Assert.False(context.HasSucceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData, CollectionCustomization]
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
public async Task CanCreateAsync_WhenMissingOrgAccess_NoSuccess(
|
public async Task CanCreateAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitDisabled_NoSuccess(
|
||||||
Guid userId,
|
Guid userId,
|
||||||
CurrentContextOrganization organization,
|
CurrentContextOrganization organization,
|
||||||
List<Collection> collections,
|
List<Collection> collections,
|
||||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider)
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider)
|
||||||
{
|
{
|
||||||
collections.ForEach(c => c.OrganizationId = organization.Id);
|
collections.ForEach(c => c.OrganizationId = organization.Id);
|
||||||
ArrangeOrganizationAbility(sutProvider, organization, true);
|
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { BulkCollectionOperations.Create },
|
new[] { BulkCollectionOperations.Create },
|
||||||
@ -127,8 +205,38 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
|
||||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
|
Assert.False(context.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
|
public async Task CanCreateAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitEnabled_NoSuccess(
|
||||||
|
Guid userId,
|
||||||
|
CurrentContextOrganization organization,
|
||||||
|
List<Collection> collections,
|
||||||
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider)
|
||||||
|
{
|
||||||
|
collections.ForEach(c => c.OrganizationId = organization.Id);
|
||||||
|
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(
|
||||||
|
new[] { BulkCollectionOperations.Create },
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
collections
|
||||||
|
);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
Assert.False(context.HasSucceeded);
|
Assert.False(context.HasSucceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -904,7 +1012,10 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
DeleteAnyCollection = true
|
DeleteAnyCollection = true
|
||||||
};
|
};
|
||||||
|
|
||||||
ArrangeOrganizationAbility(sutProvider, organization, true);
|
// `LimitCollectonCreationDeletionSplit` feature flag state isn't
|
||||||
|
// relevant for this test. The flag is never checked for in this
|
||||||
|
// test. This is asserted below.
|
||||||
|
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { BulkCollectionOperations.Delete },
|
new[] { BulkCollectionOperations.Delete },
|
||||||
@ -916,6 +1027,7 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
Assert.True(context.HasSucceeded);
|
Assert.True(context.HasSucceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -931,7 +1043,10 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
organization.Type = userType;
|
organization.Type = userType;
|
||||||
organization.Permissions = new Permissions();
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
ArrangeOrganizationAbility(sutProvider, organization, true);
|
// `LimitCollectonCreationDeletionSplit` feature flag state isn't
|
||||||
|
// relevant for this test. The flag is never checked for in this
|
||||||
|
// test. This is asserted below.
|
||||||
|
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { BulkCollectionOperations.Delete },
|
new[] { BulkCollectionOperations.Delete },
|
||||||
@ -943,11 +1058,12 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
Assert.True(context.HasSucceeded);
|
Assert.True(context.HasSucceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData, CollectionCustomization]
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
public async Task CanDeleteAsync_WhenUser_LimitCollectionCreationDeletionFalse_WithCanManagePermission_Success(
|
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
|
||||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
ICollection<CollectionDetails> collections,
|
ICollection<CollectionDetails> collections,
|
||||||
CurrentContextOrganization organization)
|
CurrentContextOrganization organization)
|
||||||
@ -957,11 +1073,12 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
organization.Type = OrganizationUserType.User;
|
organization.Type = OrganizationUserType.User;
|
||||||
organization.Permissions = new Permissions();
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
ArrangeOrganizationAbility(sutProvider, organization, false);
|
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, false, false);
|
||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
|
||||||
|
|
||||||
foreach (var c in collections)
|
foreach (var c in collections)
|
||||||
{
|
{
|
||||||
@ -975,6 +1092,41 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
|
Assert.True(context.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
|
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success(
|
||||||
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
|
ICollection<CollectionDetails> collections,
|
||||||
|
CurrentContextOrganization organization)
|
||||||
|
{
|
||||||
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
organization.Type = OrganizationUserType.User;
|
||||||
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
|
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, false, false);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
|
||||||
|
|
||||||
|
foreach (var c in collections)
|
||||||
|
{
|
||||||
|
c.Manage = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(
|
||||||
|
new[] { BulkCollectionOperations.Delete },
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
collections);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
Assert.True(context.HasSucceeded);
|
Assert.True(context.HasSucceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -982,7 +1134,7 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
[BitAutoData(OrganizationUserType.Admin)]
|
[BitAutoData(OrganizationUserType.Admin)]
|
||||||
[BitAutoData(OrganizationUserType.Owner)]
|
[BitAutoData(OrganizationUserType.Owner)]
|
||||||
[BitAutoData(OrganizationUserType.User)]
|
[BitAutoData(OrganizationUserType.User)]
|
||||||
public async Task CanDeleteAsync_LimitCollectionCreationDeletionFalse_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_Success(
|
public async Task CanDeleteAsync_LimitCollectionDeletionFalse_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
|
||||||
OrganizationUserType userType,
|
OrganizationUserType userType,
|
||||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
ICollection<CollectionDetails> collections,
|
ICollection<CollectionDetails> collections,
|
||||||
@ -993,11 +1145,12 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
organization.Type = userType;
|
organization.Type = userType;
|
||||||
organization.Permissions = new Permissions();
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
ArrangeOrganizationAbility(sutProvider, organization, false, false);
|
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, false, false, false);
|
||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
|
||||||
|
|
||||||
foreach (var c in collections)
|
foreach (var c in collections)
|
||||||
{
|
{
|
||||||
@ -1011,13 +1164,15 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
Assert.True(context.HasSucceeded);
|
Assert.True(context.HasSucceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, CollectionCustomization]
|
[Theory, CollectionCustomization]
|
||||||
[BitAutoData(OrganizationUserType.Admin)]
|
[BitAutoData(OrganizationUserType.Admin)]
|
||||||
[BitAutoData(OrganizationUserType.Owner)]
|
[BitAutoData(OrganizationUserType.Owner)]
|
||||||
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_Success(
|
[BitAutoData(OrganizationUserType.User)]
|
||||||
|
public async Task CanDeleteAsync_LimitCollectionDeletionFalse_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success(
|
||||||
OrganizationUserType userType,
|
OrganizationUserType userType,
|
||||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
ICollection<CollectionDetails> collections,
|
ICollection<CollectionDetails> collections,
|
||||||
@ -1028,11 +1183,12 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
organization.Type = userType;
|
organization.Type = userType;
|
||||||
organization.Permissions = new Permissions();
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
ArrangeOrganizationAbility(sutProvider, organization, true, false);
|
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, false, false, false);
|
||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
|
||||||
|
|
||||||
foreach (var c in collections)
|
foreach (var c in collections)
|
||||||
{
|
{
|
||||||
@ -1046,13 +1202,14 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
Assert.True(context.HasSucceeded);
|
Assert.True(context.HasSucceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, CollectionCustomization]
|
[Theory, CollectionCustomization]
|
||||||
[BitAutoData(OrganizationUserType.Admin)]
|
[BitAutoData(OrganizationUserType.Admin)]
|
||||||
[BitAutoData(OrganizationUserType.Owner)]
|
[BitAutoData(OrganizationUserType.Owner)]
|
||||||
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithoutCanManagePermission_Failure(
|
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
|
||||||
OrganizationUserType userType,
|
OrganizationUserType userType,
|
||||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
ICollection<CollectionDetails> collections,
|
ICollection<CollectionDetails> collections,
|
||||||
@ -1063,12 +1220,87 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
organization.Type = userType;
|
organization.Type = userType;
|
||||||
organization.Permissions = new Permissions();
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
ArrangeOrganizationAbility(sutProvider, organization, true, false);
|
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true, false);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
|
||||||
|
|
||||||
|
foreach (var c in collections)
|
||||||
|
{
|
||||||
|
c.Manage = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(
|
||||||
|
new[] { BulkCollectionOperations.Delete },
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
collections);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
|
Assert.True(context.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, CollectionCustomization]
|
||||||
|
[BitAutoData(OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(OrganizationUserType.Owner)]
|
||||||
|
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success(
|
||||||
|
OrganizationUserType userType,
|
||||||
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
|
ICollection<CollectionDetails> collections,
|
||||||
|
CurrentContextOrganization organization)
|
||||||
|
{
|
||||||
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
organization.Type = userType;
|
||||||
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
|
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true, false);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
|
||||||
|
|
||||||
|
foreach (var c in collections)
|
||||||
|
{
|
||||||
|
c.Manage = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(
|
||||||
|
new[] { BulkCollectionOperations.Delete },
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
collections);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
|
Assert.True(context.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, CollectionCustomization]
|
||||||
|
[BitAutoData(OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(OrganizationUserType.Owner)]
|
||||||
|
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithoutCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Failure(
|
||||||
|
OrganizationUserType userType,
|
||||||
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
|
ICollection<CollectionDetails> collections,
|
||||||
|
CurrentContextOrganization organization)
|
||||||
|
{
|
||||||
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
organization.Type = userType;
|
||||||
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
|
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true, false);
|
||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
|
||||||
|
|
||||||
foreach (var c in collections)
|
foreach (var c in collections)
|
||||||
{
|
{
|
||||||
@ -1082,11 +1314,50 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
|
Assert.False(context.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, CollectionCustomization]
|
||||||
|
[BitAutoData(OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(OrganizationUserType.Owner)]
|
||||||
|
public async Task CanDeleteAsync_WhenAdminOrOwner_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithoutCanManagePermission_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Failure(
|
||||||
|
OrganizationUserType userType,
|
||||||
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
|
ICollection<CollectionDetails> collections,
|
||||||
|
CurrentContextOrganization organization)
|
||||||
|
{
|
||||||
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
organization.Type = userType;
|
||||||
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
|
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true, false);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
|
||||||
|
|
||||||
|
foreach (var c in collections)
|
||||||
|
{
|
||||||
|
c.Manage = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(
|
||||||
|
new[] { BulkCollectionOperations.Delete },
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
collections);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
Assert.False(context.HasSucceeded);
|
Assert.False(context.HasSucceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData, CollectionCustomization]
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
public async Task CanDeleteAsync_WhenUser_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsTrue_Failure(
|
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsTrue_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Failure(
|
||||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
ICollection<CollectionDetails> collections,
|
ICollection<CollectionDetails> collections,
|
||||||
CurrentContextOrganization organization)
|
CurrentContextOrganization organization)
|
||||||
@ -1096,12 +1367,13 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
organization.Type = OrganizationUserType.User;
|
organization.Type = OrganizationUserType.User;
|
||||||
organization.Permissions = new Permissions();
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
ArrangeOrganizationAbility(sutProvider, organization, true);
|
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true);
|
||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
|
||||||
|
|
||||||
foreach (var c in collections)
|
foreach (var c in collections)
|
||||||
{
|
{
|
||||||
@ -1115,11 +1387,12 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
Assert.False(context.HasSucceeded);
|
Assert.False(context.HasSucceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData, CollectionCustomization]
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
public async Task CanDeleteAsync_WhenUser_LimitCollectionCreationDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_Failure(
|
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsTrue_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Failure(
|
||||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
ICollection<CollectionDetails> collections,
|
ICollection<CollectionDetails> collections,
|
||||||
CurrentContextOrganization organization)
|
CurrentContextOrganization organization)
|
||||||
@ -1129,12 +1402,13 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
organization.Type = OrganizationUserType.User;
|
organization.Type = OrganizationUserType.User;
|
||||||
organization.Permissions = new Permissions();
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
ArrangeOrganizationAbility(sutProvider, organization, true, false);
|
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
|
||||||
|
|
||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
|
||||||
|
|
||||||
foreach (var c in collections)
|
foreach (var c in collections)
|
||||||
{
|
{
|
||||||
@ -1148,13 +1422,88 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
|
Assert.False(context.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
|
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Failure(
|
||||||
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
|
ICollection<CollectionDetails> collections,
|
||||||
|
CurrentContextOrganization organization)
|
||||||
|
{
|
||||||
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
organization.Type = OrganizationUserType.User;
|
||||||
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
|
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true, false);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)
|
||||||
|
.Returns(false);
|
||||||
|
|
||||||
|
foreach (var c in collections)
|
||||||
|
{
|
||||||
|
c.Manage = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(
|
||||||
|
new[] { BulkCollectionOperations.Delete },
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
collections);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
|
Assert.False(context.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
|
public async Task CanDeleteAsync_WhenUser_LimitCollectionDeletionTrue_AllowAdminAccessToAllCollectionItemsFalse_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Failure(
|
||||||
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
|
ICollection<CollectionDetails> collections,
|
||||||
|
CurrentContextOrganization organization)
|
||||||
|
{
|
||||||
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
organization.Type = OrganizationUserType.User;
|
||||||
|
organization.Permissions = new Permissions();
|
||||||
|
|
||||||
|
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true, false);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<ICollectionRepository>().GetManyByUserIdAsync(actingUserId).Returns(collections);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
foreach (var c in collections)
|
||||||
|
{
|
||||||
|
c.Manage = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(
|
||||||
|
new[] { BulkCollectionOperations.Delete },
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
collections);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
Assert.False(context.HasSucceeded);
|
Assert.False(context.HasSucceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, CollectionCustomization]
|
[Theory, CollectionCustomization]
|
||||||
[BitAutoData(OrganizationUserType.User)]
|
[BitAutoData(OrganizationUserType.User)]
|
||||||
[BitAutoData(OrganizationUserType.Custom)]
|
[BitAutoData(OrganizationUserType.Custom)]
|
||||||
public async Task CanDeleteAsync_WhenMissingPermissions_NoSuccess(
|
public async Task CanDeleteAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureDisabled_NoSuccess(
|
||||||
OrganizationUserType userType,
|
OrganizationUserType userType,
|
||||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
ICollection<Collection> collections,
|
ICollection<Collection> collections,
|
||||||
@ -1171,7 +1520,7 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
ManageUsers = false
|
ManageUsers = false
|
||||||
};
|
};
|
||||||
|
|
||||||
ArrangeOrganizationAbility(sutProvider, organization, true);
|
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(sutProvider, organization, true, true);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { BulkCollectionOperations.Delete },
|
new[] { BulkCollectionOperations.Delete },
|
||||||
@ -1181,14 +1530,54 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
|
Assert.False(context.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, CollectionCustomization]
|
||||||
|
[BitAutoData(OrganizationUserType.User)]
|
||||||
|
[BitAutoData(OrganizationUserType.Custom)]
|
||||||
|
public async Task CanDeleteAsync_WhenMissingPermissions_WithLimitCollectionCreationDeletionSplitFeatureEnabled_NoSuccess(
|
||||||
|
OrganizationUserType userType,
|
||||||
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
|
ICollection<Collection> collections,
|
||||||
|
CurrentContextOrganization organization)
|
||||||
|
{
|
||||||
|
var actingUserId = Guid.NewGuid();
|
||||||
|
|
||||||
|
organization.Type = userType;
|
||||||
|
organization.Permissions = new Permissions
|
||||||
|
{
|
||||||
|
EditAnyCollection = false,
|
||||||
|
DeleteAnyCollection = false,
|
||||||
|
ManageGroups = false,
|
||||||
|
ManageUsers = false
|
||||||
|
};
|
||||||
|
|
||||||
|
ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(sutProvider, organization, true, true);
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(
|
||||||
|
new[] { BulkCollectionOperations.Delete },
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
collections);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
Assert.False(context.HasSucceeded);
|
Assert.False(context.HasSucceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData, CollectionCustomization]
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
public async Task CanDeleteAsync_WhenMissingOrgAccess_NoSuccess(
|
public async Task CanDeleteAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitFeatureDisabled_NoSuccess(
|
||||||
Guid userId,
|
Guid userId,
|
||||||
ICollection<Collection> collections,
|
ICollection<Collection> collections,
|
||||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider)
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider)
|
||||||
@ -1202,8 +1591,34 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||||
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
|
||||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
|
||||||
|
|
||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
|
Assert.False(context.HasSucceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
|
public async Task CanDeleteAsync_WhenMissingOrgAccess_WithLimitCollectionCreationDeletionSplitFeatureEnabled_NoSuccess(
|
||||||
|
Guid userId,
|
||||||
|
ICollection<Collection> collections,
|
||||||
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider)
|
||||||
|
{
|
||||||
|
var context = new AuthorizationHandlerContext(
|
||||||
|
new[] { BulkCollectionOperations.Delete },
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
collections
|
||||||
|
);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(userId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(Arg.Any<Guid>()).Returns((CurrentContextOrganization)null);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(false);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>().Received(1).IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
Assert.False(context.HasSucceeded);
|
Assert.False(context.HasSucceeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1224,6 +1639,7 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
await sutProvider.Sut.HandleAsync(context);
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
Assert.True(context.HasFailed);
|
Assert.True(context.HasFailed);
|
||||||
sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs();
|
sutProvider.GetDependency<ICollectionRepository>().DidNotReceiveWithAnyArgs();
|
||||||
|
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData, CollectionCustomization]
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
@ -1247,10 +1663,11 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.HandleAsync(context));
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() => sutProvider.Sut.HandleAsync(context));
|
||||||
Assert.Equal("Requested collections must belong to the same organization.", exception.Message);
|
Assert.Equal("Requested collections must belong to the same organization.", exception.Message);
|
||||||
sutProvider.GetDependency<ICurrentContext>().DidNotReceiveWithAnyArgs().GetOrganization(default);
|
sutProvider.GetDependency<ICurrentContext>().DidNotReceiveWithAnyArgs().GetOrganization(default);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().DidNotReceiveWithAnyArgs().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData, CollectionCustomization]
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
public async Task HandleRequirementAsync_Provider_Success(
|
public async Task HandleRequirementAsync_Provider_WithLimitCollectionCreationDeletionSplitFeatureDisabled_Success(
|
||||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
ICollection<Collection> collections)
|
ICollection<Collection> collections)
|
||||||
{
|
{
|
||||||
@ -1286,6 +1703,63 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync()
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync()
|
||||||
.Returns(organizationAbilities);
|
.Returns(organizationAbilities);
|
||||||
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(true);
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(true);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(false);
|
||||||
|
|
||||||
|
var context = new AuthorizationHandlerContext(
|
||||||
|
new[] { op },
|
||||||
|
new ClaimsPrincipal(),
|
||||||
|
collections
|
||||||
|
);
|
||||||
|
|
||||||
|
await sutProvider.Sut.HandleAsync(context);
|
||||||
|
|
||||||
|
Assert.True(context.HasSucceeded);
|
||||||
|
await sutProvider.GetDependency<ICurrentContext>().Received().ProviderUserForOrgAsync(orgId);
|
||||||
|
|
||||||
|
// Recreate the SUT to reset the mocks/dependencies between tests
|
||||||
|
sutProvider.Recreate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData, CollectionCustomization]
|
||||||
|
public async Task HandleRequirementAsync_Provider_WithLimitCollectionCreationDeletionSplitFeatureEnabled_Success(
|
||||||
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
|
ICollection<Collection> collections)
|
||||||
|
{
|
||||||
|
var actingUserId = Guid.NewGuid();
|
||||||
|
var orgId = collections.First().OrganizationId;
|
||||||
|
|
||||||
|
var organizationAbilities = new Dictionary<Guid, OrganizationAbility>
|
||||||
|
{
|
||||||
|
{ collections.First().OrganizationId,
|
||||||
|
new OrganizationAbility
|
||||||
|
{
|
||||||
|
LimitCollectionCreation = true,
|
||||||
|
LimitCollectionDeletion = true,
|
||||||
|
AllowAdminAccessToAllCollectionItems = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var operationsToTest = new[]
|
||||||
|
{
|
||||||
|
BulkCollectionOperations.Create,
|
||||||
|
BulkCollectionOperations.Read,
|
||||||
|
BulkCollectionOperations.ReadAccess,
|
||||||
|
BulkCollectionOperations.Update,
|
||||||
|
BulkCollectionOperations.ModifyUserAccess,
|
||||||
|
BulkCollectionOperations.ModifyGroupAccess,
|
||||||
|
BulkCollectionOperations.Delete,
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var op in operationsToTest)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().UserId.Returns(actingUserId);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().GetOrganization(orgId).Returns((CurrentContextOrganization)null);
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilitiesAsync()
|
||||||
|
.Returns(organizationAbilities);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ProviderUserForOrgAsync(Arg.Any<Guid>()).Returns(true);
|
||||||
|
sutProvider.GetDependency<IFeatureService>().IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit).Returns(true);
|
||||||
|
|
||||||
var context = new AuthorizationHandlerContext(
|
var context = new AuthorizationHandlerContext(
|
||||||
new[] { op },
|
new[] { op },
|
||||||
@ -1336,14 +1810,37 @@ public class BulkCollectionAuthorizationHandlerTests
|
|||||||
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdAsync(Arg.Any<Guid>());
|
await sutProvider.GetDependency<ICollectionRepository>().Received(1).GetManyByUserIdAsync(Arg.Any<Guid>());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ArrangeOrganizationAbility(
|
private static void ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureDisabled(
|
||||||
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
CurrentContextOrganization organization, bool limitCollectionCreationDeletion,
|
CurrentContextOrganization organization,
|
||||||
|
bool limitCollectionCreation,
|
||||||
|
bool limitCollectionDeletion,
|
||||||
bool allowAdminAccessToAllCollectionItems = true)
|
bool allowAdminAccessToAllCollectionItems = true)
|
||||||
{
|
{
|
||||||
var organizationAbility = new OrganizationAbility();
|
var organizationAbility = new OrganizationAbility();
|
||||||
organizationAbility.Id = organization.Id;
|
organizationAbility.Id = organization.Id;
|
||||||
organizationAbility.LimitCollectionCreationDeletion = limitCollectionCreationDeletion;
|
|
||||||
|
organizationAbility.LimitCollectionCreationDeletion = limitCollectionCreation || limitCollectionDeletion;
|
||||||
|
|
||||||
|
organizationAbility.AllowAdminAccessToAllCollectionItems = allowAdminAccessToAllCollectionItems;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)
|
||||||
|
.Returns(organizationAbility);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ArrangeOrganizationAbility_WithLimitCollectionCreationDeletionSplitFeatureEnabled(
|
||||||
|
SutProvider<BulkCollectionAuthorizationHandler> sutProvider,
|
||||||
|
CurrentContextOrganization organization,
|
||||||
|
bool limitCollectionCreation,
|
||||||
|
bool limitCollectionDeletion,
|
||||||
|
bool allowAdminAccessToAllCollectionItems = true)
|
||||||
|
{
|
||||||
|
var organizationAbility = new OrganizationAbility();
|
||||||
|
organizationAbility.Id = organization.Id;
|
||||||
|
|
||||||
|
organizationAbility.LimitCollectionCreation = limitCollectionCreation;
|
||||||
|
organizationAbility.LimitCollectionDeletion = limitCollectionDeletion;
|
||||||
|
|
||||||
organizationAbility.AllowAdminAccessToAllCollectionItems = allowAdminAccessToAllCollectionItems;
|
organizationAbility.AllowAdminAccessToAllCollectionItems = allowAdminAccessToAllCollectionItems;
|
||||||
|
|
||||||
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)
|
sutProvider.GetDependency<IApplicationCacheService>().GetOrganizationAbilityAsync(organizationAbility.Id)
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
using AutoFixture;
|
using AutoFixture;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Org.BouncyCastle.Security;
|
|
||||||
|
|
||||||
namespace Bit.Test.Common.AutoFixture;
|
namespace Bit.Test.Common.AutoFixture;
|
||||||
|
|
||||||
@ -15,7 +14,7 @@ public class ControllerCustomization : ICustomization
|
|||||||
{
|
{
|
||||||
if (!controllerType.IsAssignableTo(typeof(Controller)))
|
if (!controllerType.IsAssignableTo(typeof(Controller)))
|
||||||
{
|
{
|
||||||
throw new InvalidParameterException($"{nameof(controllerType)} must derive from {typeof(Controller).Name}");
|
throw new Exception($"{nameof(controllerType)} must derive from {typeof(Controller).Name}");
|
||||||
}
|
}
|
||||||
|
|
||||||
_controllerType = controllerType;
|
_controllerType = controllerType;
|
||||||
|
@ -127,7 +127,6 @@ public class SutProvider<TSut> : ISutProvider
|
|||||||
return _sutProvider.GetDependency(parameterInfo.ParameterType, "");
|
return _sutProvider.GetDependency(parameterInfo.ParameterType, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// This is the equivalent of _fixture.Create<parameterInfo.ParameterType>, but no overload for
|
// This is the equivalent of _fixture.Create<parameterInfo.ParameterType>, but no overload for
|
||||||
// Create(Type type) exists.
|
// Create(Type type) exists.
|
||||||
var dependency = new SpecimenContext(_fixture).Resolve(new SeededRequest(parameterInfo.ParameterType,
|
var dependency = new SpecimenContext(_fixture).Resolve(new SeededRequest(parameterInfo.ParameterType,
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
using AutoFixture;
|
using AutoFixture;
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
|
using Microsoft.Extensions.Time.Testing;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using RichardSzalay.MockHttp;
|
using RichardSzalay.MockHttp;
|
||||||
|
|
||||||
@ -47,4 +48,19 @@ public static class SutProviderExtensions
|
|||||||
.SetDependency(mockHttpClientFactory)
|
.SetDependency(mockHttpClientFactory)
|
||||||
.Create();
|
.Create();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures SutProvider to use FakeTimeProvider.
|
||||||
|
/// It is registered under both the TimeProvider type and the FakeTimeProvider type
|
||||||
|
/// so that it can be retrieved in a type-safe manner with GetDependency.
|
||||||
|
/// This can be chained with other builder methods; make sure to call
|
||||||
|
/// <see cref="ISutProvider.Create"/> before use.
|
||||||
|
/// </summary>
|
||||||
|
public static SutProvider<T> WithFakeTimeProvider<T>(this SutProvider<T> sutProvider)
|
||||||
|
{
|
||||||
|
var fakeTimeProvider = new FakeTimeProvider();
|
||||||
|
return sutProvider
|
||||||
|
.SetDependency((TimeProvider)fakeTimeProvider)
|
||||||
|
.SetDependency(fakeTimeProvider);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.TimeProvider.Testing" Version="8.10.0" />
|
||||||
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
|
<PackageReference Include="NSubstitute" Version="$(NSubstituteVersion)" />
|
||||||
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
<PackageReference Include="xunit" Version="$(XUnitVersion)" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)">
|
<PackageReference Include="xunit.runner.visualstudio" Version="$(XUnitRunnerVisualStudioVersion)">
|
||||||
|
@ -9,10 +9,12 @@ namespace Bit.Core.Test.AdminConsole.AutoFixture;
|
|||||||
internal class PolicyCustomization : ICustomization
|
internal class PolicyCustomization : ICustomization
|
||||||
{
|
{
|
||||||
public PolicyType Type { get; set; }
|
public PolicyType Type { get; set; }
|
||||||
|
public bool Enabled { get; set; }
|
||||||
|
|
||||||
public PolicyCustomization(PolicyType type)
|
public PolicyCustomization(PolicyType type, bool enabled)
|
||||||
{
|
{
|
||||||
Type = type;
|
Type = type;
|
||||||
|
Enabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Customize(IFixture fixture)
|
public void Customize(IFixture fixture)
|
||||||
@ -20,21 +22,23 @@ internal class PolicyCustomization : ICustomization
|
|||||||
fixture.Customize<Policy>(composer => composer
|
fixture.Customize<Policy>(composer => composer
|
||||||
.With(o => o.OrganizationId, Guid.NewGuid())
|
.With(o => o.OrganizationId, Guid.NewGuid())
|
||||||
.With(o => o.Type, Type)
|
.With(o => o.Type, Type)
|
||||||
.With(o => o.Enabled, true));
|
.With(o => o.Enabled, Enabled));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PolicyAttribute : CustomizeAttribute
|
public class PolicyAttribute : CustomizeAttribute
|
||||||
{
|
{
|
||||||
private readonly PolicyType _type;
|
private readonly PolicyType _type;
|
||||||
|
private readonly bool _enabled;
|
||||||
|
|
||||||
public PolicyAttribute(PolicyType type)
|
public PolicyAttribute(PolicyType type, bool enabled = true)
|
||||||
{
|
{
|
||||||
_type = type;
|
_type = type;
|
||||||
|
_enabled = enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override ICustomization GetCustomization(ParameterInfo parameter)
|
public override ICustomization GetCustomization(ParameterInfo parameter)
|
||||||
{
|
{
|
||||||
return new PolicyCustomization(_type);
|
return new PolicyCustomization(_type, _enabled);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
public class VerifyOrganizationDomainCommandTests
|
||||||
{
|
{
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task VerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimed(Guid id,
|
public async Task UserVerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimed(Guid id,
|
||||||
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||||
{
|
{
|
||||||
var expected = new OrganizationDomain
|
var expected = new OrganizationDomain
|
||||||
@ -30,14 +30,14 @@ public class VerifyOrganizationDomainCommandTests
|
|||||||
.GetByIdAsync(id)
|
.GetByIdAsync(id)
|
||||||
.Returns(expected);
|
.Returns(expected);
|
||||||
|
|
||||||
var requestAction = async () => await sutProvider.Sut.VerifyOrganizationDomainAsync(expected);
|
var requestAction = async () => await sutProvider.Sut.UserVerifyOrganizationDomainAsync(expected);
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<ConflictException>(requestAction);
|
var exception = await Assert.ThrowsAsync<ConflictException>(requestAction);
|
||||||
Assert.Contains("Domain has already been verified.", exception.Message);
|
Assert.Contains("Domain has already been verified.", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task VerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimedByAnotherOrganization(Guid id,
|
public async Task UserVerifyOrganizationDomain_ShouldThrowConflict_WhenDomainHasBeenClaimedByAnotherOrganization(Guid id,
|
||||||
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||||
{
|
{
|
||||||
var expected = new OrganizationDomain
|
var expected = new OrganizationDomain
|
||||||
@ -54,14 +54,14 @@ public class VerifyOrganizationDomainCommandTests
|
|||||||
.GetClaimedDomainsByDomainNameAsync(expected.DomainName)
|
.GetClaimedDomainsByDomainNameAsync(expected.DomainName)
|
||||||
.Returns(new List<OrganizationDomain> { expected });
|
.Returns(new List<OrganizationDomain> { expected });
|
||||||
|
|
||||||
var requestAction = async () => await sutProvider.Sut.VerifyOrganizationDomainAsync(expected);
|
var requestAction = async () => await sutProvider.Sut.UserVerifyOrganizationDomainAsync(expected);
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<ConflictException>(requestAction);
|
var exception = await Assert.ThrowsAsync<ConflictException>(requestAction);
|
||||||
Assert.Contains("The domain is not available to be claimed.", exception.Message);
|
Assert.Contains("The domain is not available to be claimed.", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task VerifyOrganizationDomain_ShouldVerifyDomainUpdateAndLogEvent_WhenTxtRecordExists(Guid id,
|
public async Task UserVerifyOrganizationDomain_ShouldVerifyDomainUpdateAndLogEvent_WhenTxtRecordExists(Guid id,
|
||||||
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||||
{
|
{
|
||||||
var expected = new OrganizationDomain
|
var expected = new OrganizationDomain
|
||||||
@ -81,7 +81,7 @@ public class VerifyOrganizationDomainCommandTests
|
|||||||
.ResolveAsync(expected.DomainName, Arg.Any<string>())
|
.ResolveAsync(expected.DomainName, Arg.Any<string>())
|
||||||
.Returns(true);
|
.Returns(true);
|
||||||
|
|
||||||
var result = await sutProvider.Sut.VerifyOrganizationDomainAsync(expected);
|
var result = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(expected);
|
||||||
|
|
||||||
Assert.NotNull(result.VerifiedDate);
|
Assert.NotNull(result.VerifiedDate);
|
||||||
await sutProvider.GetDependency<IOrganizationDomainRepository>().Received(1)
|
await sutProvider.GetDependency<IOrganizationDomainRepository>().Received(1)
|
||||||
@ -91,7 +91,7 @@ public class VerifyOrganizationDomainCommandTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task VerifyOrganizationDomain_ShouldNotSetVerifiedDate_WhenTxtRecordDoesNotExist(Guid id,
|
public async Task UserVerifyOrganizationDomain_ShouldNotSetVerifiedDate_WhenTxtRecordDoesNotExist(Guid id,
|
||||||
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||||
{
|
{
|
||||||
var expected = new OrganizationDomain
|
var expected = new OrganizationDomain
|
||||||
@ -111,10 +111,30 @@ public class VerifyOrganizationDomainCommandTests
|
|||||||
.ResolveAsync(expected.DomainName, Arg.Any<string>())
|
.ResolveAsync(expected.DomainName, Arg.Any<string>())
|
||||||
.Returns(false);
|
.Returns(false);
|
||||||
|
|
||||||
var result = await sutProvider.Sut.VerifyOrganizationDomainAsync(expected);
|
var result = await sutProvider.Sut.UserVerifyOrganizationDomainAsync(expected);
|
||||||
|
|
||||||
Assert.Null(result.VerifiedDate);
|
Assert.Null(result.VerifiedDate);
|
||||||
await sutProvider.GetDependency<IEventService>().Received(1)
|
await sutProvider.GetDependency<IEventService>().Received(1)
|
||||||
.LogOrganizationDomainEventAsync(Arg.Any<OrganizationDomain>(), EventType.OrganizationDomain_NotVerified);
|
.LogOrganizationDomainEventAsync(Arg.Any<OrganizationDomain>(), EventType.OrganizationDomain_NotVerified);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task SystemVerifyOrganizationDomain_CallsEventServiceWithUpdatedJobRunCount(SutProvider<VerifyOrganizationDomainCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var domain = new OrganizationDomain()
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
OrganizationId = Guid.NewGuid(),
|
||||||
|
CreationDate = DateTime.UtcNow,
|
||||||
|
DomainName = "test.com",
|
||||||
|
Txt = "btw+12345",
|
||||||
|
};
|
||||||
|
|
||||||
|
_ = await sutProvider.Sut.SystemVerifyOrganizationDomainAsync(domain);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IEventService>().ReceivedWithAnyArgs(1)
|
||||||
|
.LogOrganizationDomainEventAsync(default, EventType.OrganizationDomain_NotVerified,
|
||||||
|
EventSystemUser.DomainVerification);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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