mirror of
https://github.com/bitwarden/server.git
synced 2025-04-05 21:18:13 -05:00
Merge branch 'main' into km/pm-17987
This commit is contained in:
commit
8f85144c70
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
@ -66,6 +66,7 @@ src/Admin/Views/Tools @bitwarden/team-billing-dev
|
|||||||
|
|
||||||
# Platform team
|
# Platform team
|
||||||
.github/workflows/build.yml @bitwarden/team-platform-dev
|
.github/workflows/build.yml @bitwarden/team-platform-dev
|
||||||
|
.github/workflows/build_target.yml @bitwarden/team-platform-dev
|
||||||
.github/workflows/cleanup-after-pr.yml @bitwarden/team-platform-dev
|
.github/workflows/cleanup-after-pr.yml @bitwarden/team-platform-dev
|
||||||
.github/workflows/cleanup-rc-branch.yml @bitwarden/team-platform-dev
|
.github/workflows/cleanup-rc-branch.yml @bitwarden/team-platform-dev
|
||||||
.github/workflows/repository-management.yml @bitwarden/team-platform-dev
|
.github/workflows/repository-management.yml @bitwarden/team-platform-dev
|
||||||
|
52
.github/workflows/build.yml
vendored
52
.github/workflows/build.yml
vendored
@ -7,22 +7,18 @@ on:
|
|||||||
- "main"
|
- "main"
|
||||||
- "rc"
|
- "rc"
|
||||||
- "hotfix-rc"
|
- "hotfix-rc"
|
||||||
pull_request_target:
|
pull_request:
|
||||||
types: [opened, synchronize]
|
types: [opened, synchronize]
|
||||||
|
workflow_call:
|
||||||
|
inputs: {}
|
||||||
|
|
||||||
env:
|
env:
|
||||||
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
|
_AZ_REGISTRY: "bitwardenprod.azurecr.io"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check-run:
|
|
||||||
name: Check PR run
|
|
||||||
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
|
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
name: Lint
|
name: Lint
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
|
||||||
- check-run
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
@ -40,6 +36,8 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs:
|
||||||
- lint
|
- lint
|
||||||
|
outputs:
|
||||||
|
has_secrets: ${{ steps.check-secrets.outputs.has_secrets }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@ -75,6 +73,14 @@ jobs:
|
|||||||
base_path: ./bitwarden_license/src
|
base_path: ./bitwarden_license/src
|
||||||
node: true
|
node: true
|
||||||
steps:
|
steps:
|
||||||
|
- name: Check secrets
|
||||||
|
id: check-secrets
|
||||||
|
env:
|
||||||
|
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
|
||||||
|
run: |
|
||||||
|
has_secrets=${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL != '' }}
|
||||||
|
echo "has_secrets=$has_secrets" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Check out repo
|
- name: Check out repo
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
with:
|
with:
|
||||||
@ -134,6 +140,7 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
needs:
|
needs:
|
||||||
- build-artifacts
|
- build-artifacts
|
||||||
|
if: ${{ needs.build-artifacts.outputs.has_secrets == 'true' }}
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@ -227,7 +234,7 @@ jobs:
|
|||||||
- name: Generate Docker image tag
|
- name: Generate Docker image tag
|
||||||
id: tag
|
id: tag
|
||||||
run: |
|
run: |
|
||||||
if [[ "${GITHUB_EVENT_NAME}" == "pull_request_target" ]]; then
|
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
|
||||||
IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s#/#-#g")
|
IMAGE_TAG=$(echo "${GITHUB_HEAD_REF}" | sed "s#/#-#g")
|
||||||
else
|
else
|
||||||
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
|
IMAGE_TAG=$(echo "${GITHUB_REF:11}" | sed "s#/#-#g")
|
||||||
@ -289,11 +296,11 @@ jobs:
|
|||||||
"GH_PAT=${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}"
|
"GH_PAT=${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}"
|
||||||
|
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
|
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||||
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0
|
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0
|
||||||
|
|
||||||
- name: Sign image with Cosign
|
- name: Sign image with Cosign
|
||||||
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
|
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||||
env:
|
env:
|
||||||
DIGEST: ${{ steps.build-docker.outputs.digest }}
|
DIGEST: ${{ steps.build-docker.outputs.digest }}
|
||||||
TAGS: ${{ steps.image-tags.outputs.tags }}
|
TAGS: ${{ steps.image-tags.outputs.tags }}
|
||||||
@ -343,7 +350,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Make Docker stubs
|
- name: Make Docker stubs
|
||||||
if: |
|
if: |
|
||||||
github.event_name != 'pull_request_target'
|
github.event_name != 'pull_request'
|
||||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||||
run: |
|
run: |
|
||||||
# Set proper setup image based on branch
|
# Set proper setup image based on branch
|
||||||
@ -385,7 +392,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Make Docker stub checksums
|
- name: Make Docker stub checksums
|
||||||
if: |
|
if: |
|
||||||
github.event_name != 'pull_request_target'
|
github.event_name != 'pull_request'
|
||||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||||
run: |
|
run: |
|
||||||
sha256sum docker-stub-US.zip > docker-stub-US-sha256.txt
|
sha256sum docker-stub-US.zip > docker-stub-US-sha256.txt
|
||||||
@ -393,7 +400,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Docker stub US artifact
|
- name: Upload Docker stub US artifact
|
||||||
if: |
|
if: |
|
||||||
github.event_name != 'pull_request_target'
|
github.event_name != 'pull_request'
|
||||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
@ -403,7 +410,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Docker stub EU artifact
|
- name: Upload Docker stub EU artifact
|
||||||
if: |
|
if: |
|
||||||
github.event_name != 'pull_request_target'
|
github.event_name != 'pull_request'
|
||||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
@ -413,7 +420,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Docker stub US checksum artifact
|
- name: Upload Docker stub US checksum artifact
|
||||||
if: |
|
if: |
|
||||||
github.event_name != 'pull_request_target'
|
github.event_name != 'pull_request'
|
||||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
@ -423,7 +430,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Docker stub EU checksum artifact
|
- name: Upload Docker stub EU checksum artifact
|
||||||
if: |
|
if: |
|
||||||
github.event_name != 'pull_request_target'
|
github.event_name != 'pull_request'
|
||||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||||
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
uses: actions/upload-artifact@65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0
|
||||||
with:
|
with:
|
||||||
@ -552,7 +559,7 @@ jobs:
|
|||||||
self-host-build:
|
self-host-build:
|
||||||
name: Trigger self-host build
|
name: Trigger self-host build
|
||||||
if: |
|
if: |
|
||||||
github.event_name != 'pull_request_target'
|
github.event_name != 'pull_request'
|
||||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs:
|
||||||
@ -587,7 +594,7 @@ jobs:
|
|||||||
|
|
||||||
trigger-k8s-deploy:
|
trigger-k8s-deploy:
|
||||||
name: Trigger k8s deploy
|
name: Trigger k8s deploy
|
||||||
if: github.event_name != 'pull_request_target' && github.ref == 'refs/heads/main'
|
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
needs:
|
needs:
|
||||||
- build-docker
|
- build-docker
|
||||||
@ -623,7 +630,8 @@ jobs:
|
|||||||
trigger-ee-updates:
|
trigger-ee-updates:
|
||||||
name: Trigger Ephemeral Environment updates
|
name: Trigger Ephemeral Environment updates
|
||||||
if: |
|
if: |
|
||||||
github.event_name == 'pull_request_target'
|
needs.build-artifacts.outputs.has_secrets == 'true'
|
||||||
|
&& github.event_name == 'pull_request'
|
||||||
&& contains(github.event.pull_request.labels.*.name, 'ephemeral-environment')
|
&& contains(github.event.pull_request.labels.*.name, 'ephemeral-environment')
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
needs:
|
needs:
|
||||||
@ -660,7 +668,8 @@ jobs:
|
|||||||
name: Trigger Ephemeral Environment Sync
|
name: Trigger Ephemeral Environment Sync
|
||||||
needs: trigger-ee-updates
|
needs: trigger-ee-updates
|
||||||
if: |
|
if: |
|
||||||
github.event_name == 'pull_request_target'
|
needs.build-artifacts.outputs.has_secrets == 'true'
|
||||||
|
&& github.event_name == 'pull_request'
|
||||||
&& contains(github.event.pull_request.labels.*.name, 'ephemeral-environment')
|
&& contains(github.event.pull_request.labels.*.name, 'ephemeral-environment')
|
||||||
uses: bitwarden/gh-actions/.github/workflows/_ephemeral_environment_manager.yml@main
|
uses: bitwarden/gh-actions/.github/workflows/_ephemeral_environment_manager.yml@main
|
||||||
with:
|
with:
|
||||||
@ -670,7 +679,6 @@ jobs:
|
|||||||
pull_request_number: ${{ github.event.number }}
|
pull_request_number: ${{ github.event.number }}
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
|
||||||
check-failures:
|
check-failures:
|
||||||
name: Check for failures
|
name: Check for failures
|
||||||
if: always()
|
if: always()
|
||||||
@ -686,7 +694,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check if any job failed
|
- name: Check if any job failed
|
||||||
if: |
|
if: |
|
||||||
github.event_name != 'pull_request_target'
|
github.event_name != 'pull_request'
|
||||||
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
&& (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc')
|
||||||
&& contains(needs.*.result, 'failure')
|
&& contains(needs.*.result, 'failure')
|
||||||
run: exit 1
|
run: exit 1
|
||||||
|
21
.github/workflows/build_target.yml
vendored
Normal file
21
.github/workflows/build_target.yml
vendored
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
name: Build on PR Target
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, synchronize]
|
||||||
|
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-run:
|
||||||
|
name: Check PR run
|
||||||
|
uses: bitwarden/gh-actions/.github/workflows/check-run.yml@main
|
||||||
|
|
||||||
|
run-workflow:
|
||||||
|
name: Run Build on PR Target
|
||||||
|
needs: check-run
|
||||||
|
if: ${{ github.event.pull_request.head.repo.full_name != github.repository }}
|
||||||
|
uses: ./.github/workflows/build.yml
|
||||||
|
secrets: inherit
|
@ -1,10 +1,8 @@
|
|||||||
using Bit.Core;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.Enums;
|
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.Scim.Groups.Interfaces;
|
using Bit.Scim.Groups.Interfaces;
|
||||||
using Bit.Scim.Models;
|
using Bit.Scim.Models;
|
||||||
using Bit.Scim.Utilities;
|
using Bit.Scim.Utilities;
|
||||||
@ -24,10 +22,8 @@ public class GroupsController : Controller
|
|||||||
private readonly IGetGroupsListQuery _getGroupsListQuery;
|
private readonly IGetGroupsListQuery _getGroupsListQuery;
|
||||||
private readonly IDeleteGroupCommand _deleteGroupCommand;
|
private readonly IDeleteGroupCommand _deleteGroupCommand;
|
||||||
private readonly IPatchGroupCommand _patchGroupCommand;
|
private readonly IPatchGroupCommand _patchGroupCommand;
|
||||||
private readonly IPatchGroupCommandvNext _patchGroupCommandvNext;
|
|
||||||
private readonly IPostGroupCommand _postGroupCommand;
|
private readonly IPostGroupCommand _postGroupCommand;
|
||||||
private readonly IPutGroupCommand _putGroupCommand;
|
private readonly IPutGroupCommand _putGroupCommand;
|
||||||
private readonly IFeatureService _featureService;
|
|
||||||
|
|
||||||
public GroupsController(
|
public GroupsController(
|
||||||
IGroupRepository groupRepository,
|
IGroupRepository groupRepository,
|
||||||
@ -35,10 +31,8 @@ public class GroupsController : Controller
|
|||||||
IGetGroupsListQuery getGroupsListQuery,
|
IGetGroupsListQuery getGroupsListQuery,
|
||||||
IDeleteGroupCommand deleteGroupCommand,
|
IDeleteGroupCommand deleteGroupCommand,
|
||||||
IPatchGroupCommand patchGroupCommand,
|
IPatchGroupCommand patchGroupCommand,
|
||||||
IPatchGroupCommandvNext patchGroupCommandvNext,
|
|
||||||
IPostGroupCommand postGroupCommand,
|
IPostGroupCommand postGroupCommand,
|
||||||
IPutGroupCommand putGroupCommand,
|
IPutGroupCommand putGroupCommand
|
||||||
IFeatureService featureService
|
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
_groupRepository = groupRepository;
|
_groupRepository = groupRepository;
|
||||||
@ -46,10 +40,8 @@ public class GroupsController : Controller
|
|||||||
_getGroupsListQuery = getGroupsListQuery;
|
_getGroupsListQuery = getGroupsListQuery;
|
||||||
_deleteGroupCommand = deleteGroupCommand;
|
_deleteGroupCommand = deleteGroupCommand;
|
||||||
_patchGroupCommand = patchGroupCommand;
|
_patchGroupCommand = patchGroupCommand;
|
||||||
_patchGroupCommandvNext = patchGroupCommandvNext;
|
|
||||||
_postGroupCommand = postGroupCommand;
|
_postGroupCommand = postGroupCommand;
|
||||||
_putGroupCommand = putGroupCommand;
|
_putGroupCommand = putGroupCommand;
|
||||||
_featureService = featureService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
@ -103,21 +95,13 @@ public class GroupsController : Controller
|
|||||||
[HttpPatch("{id}")]
|
[HttpPatch("{id}")]
|
||||||
public async Task<IActionResult> Patch(Guid organizationId, Guid id, [FromBody] ScimPatchModel model)
|
public async Task<IActionResult> Patch(Guid organizationId, Guid id, [FromBody] ScimPatchModel model)
|
||||||
{
|
{
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.ShortcutDuplicatePatchRequests))
|
var group = await _groupRepository.GetByIdAsync(id);
|
||||||
|
if (group == null || group.OrganizationId != organizationId)
|
||||||
{
|
{
|
||||||
var group = await _groupRepository.GetByIdAsync(id);
|
throw new NotFoundException("Group not found.");
|
||||||
if (group == null || group.OrganizationId != organizationId)
|
|
||||||
{
|
|
||||||
throw new NotFoundException("Group not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
await _patchGroupCommandvNext.PatchGroupAsync(group, model);
|
|
||||||
return new NoContentResult();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
await _patchGroupCommand.PatchGroupAsync(group, model);
|
||||||
await _patchGroupCommand.PatchGroupAsync(organization, id, model);
|
|
||||||
|
|
||||||
return new NoContentResult();
|
return new NoContentResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
@ -23,7 +24,7 @@ public class UsersController : Controller
|
|||||||
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
|
||||||
private readonly IPatchUserCommand _patchUserCommand;
|
private readonly IPatchUserCommand _patchUserCommand;
|
||||||
private readonly IPostUserCommand _postUserCommand;
|
private readonly IPostUserCommand _postUserCommand;
|
||||||
private readonly ILogger<UsersController> _logger;
|
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
|
||||||
|
|
||||||
public UsersController(
|
public UsersController(
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
@ -32,7 +33,7 @@ public class UsersController : Controller
|
|||||||
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
|
||||||
IPatchUserCommand patchUserCommand,
|
IPatchUserCommand patchUserCommand,
|
||||||
IPostUserCommand postUserCommand,
|
IPostUserCommand postUserCommand,
|
||||||
ILogger<UsersController> logger)
|
IRestoreOrganizationUserCommand restoreOrganizationUserCommand)
|
||||||
{
|
{
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_organizationService = organizationService;
|
_organizationService = organizationService;
|
||||||
@ -40,7 +41,7 @@ public class UsersController : Controller
|
|||||||
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
_removeOrganizationUserCommand = removeOrganizationUserCommand;
|
||||||
_patchUserCommand = patchUserCommand;
|
_patchUserCommand = patchUserCommand;
|
||||||
_postUserCommand = postUserCommand;
|
_postUserCommand = postUserCommand;
|
||||||
_logger = logger;
|
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
@ -93,7 +94,7 @@ public class UsersController : Controller
|
|||||||
|
|
||||||
if (model.Active && orgUser.Status == OrganizationUserStatusType.Revoked)
|
if (model.Active && orgUser.Status == OrganizationUserStatusType.Revoked)
|
||||||
{
|
{
|
||||||
await _organizationService.RestoreUserAsync(orgUser, EventSystemUser.SCIM);
|
await _restoreOrganizationUserCommand.RestoreUserAsync(orgUser, EventSystemUser.SCIM);
|
||||||
}
|
}
|
||||||
else if (!model.Active && orgUser.Status != OrganizationUserStatusType.Revoked)
|
else if (!model.Active && orgUser.Status != OrganizationUserStatusType.Revoked)
|
||||||
{
|
{
|
||||||
|
@ -5,5 +5,5 @@ namespace Bit.Scim.Groups.Interfaces;
|
|||||||
|
|
||||||
public interface IPatchGroupCommand
|
public interface IPatchGroupCommand
|
||||||
{
|
{
|
||||||
Task PatchGroupAsync(Organization organization, Guid id, ScimPatchModel model);
|
Task PatchGroupAsync(Group group, ScimPatchModel model);
|
||||||
}
|
}
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
|
||||||
using Bit.Scim.Models;
|
|
||||||
|
|
||||||
namespace Bit.Scim.Groups.Interfaces;
|
|
||||||
|
|
||||||
public interface IPatchGroupCommandvNext
|
|
||||||
{
|
|
||||||
Task PatchGroupAsync(Group group, ScimPatchModel model);
|
|
||||||
}
|
|
@ -5,8 +5,10 @@ using Bit.Core.AdminConsole.Repositories;
|
|||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
using Bit.Scim.Groups.Interfaces;
|
using Bit.Scim.Groups.Interfaces;
|
||||||
using Bit.Scim.Models;
|
using Bit.Scim.Models;
|
||||||
|
using Bit.Scim.Utilities;
|
||||||
|
|
||||||
namespace Bit.Scim.Groups;
|
namespace Bit.Scim.Groups;
|
||||||
|
|
||||||
@ -16,118 +18,137 @@ public class PatchGroupCommand : IPatchGroupCommand
|
|||||||
private readonly IGroupService _groupService;
|
private readonly IGroupService _groupService;
|
||||||
private readonly IUpdateGroupCommand _updateGroupCommand;
|
private readonly IUpdateGroupCommand _updateGroupCommand;
|
||||||
private readonly ILogger<PatchGroupCommand> _logger;
|
private readonly ILogger<PatchGroupCommand> _logger;
|
||||||
|
private readonly IOrganizationRepository _organizationRepository;
|
||||||
|
|
||||||
public PatchGroupCommand(
|
public PatchGroupCommand(
|
||||||
IGroupRepository groupRepository,
|
IGroupRepository groupRepository,
|
||||||
IGroupService groupService,
|
IGroupService groupService,
|
||||||
IUpdateGroupCommand updateGroupCommand,
|
IUpdateGroupCommand updateGroupCommand,
|
||||||
ILogger<PatchGroupCommand> logger)
|
ILogger<PatchGroupCommand> logger,
|
||||||
|
IOrganizationRepository organizationRepository)
|
||||||
{
|
{
|
||||||
_groupRepository = groupRepository;
|
_groupRepository = groupRepository;
|
||||||
_groupService = groupService;
|
_groupService = groupService;
|
||||||
_updateGroupCommand = updateGroupCommand;
|
_updateGroupCommand = updateGroupCommand;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_organizationRepository = organizationRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task PatchGroupAsync(Organization organization, Guid id, ScimPatchModel model)
|
public async Task PatchGroupAsync(Group group, ScimPatchModel model)
|
||||||
{
|
{
|
||||||
var group = await _groupRepository.GetByIdAsync(id);
|
|
||||||
if (group == null || group.OrganizationId != organization.Id)
|
|
||||||
{
|
|
||||||
throw new NotFoundException("Group not found.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var operationHandled = false;
|
|
||||||
foreach (var operation in model.Operations)
|
foreach (var operation in model.Operations)
|
||||||
{
|
{
|
||||||
// Replace operations
|
await HandleOperationAsync(group, operation);
|
||||||
if (operation.Op?.ToLowerInvariant() == "replace")
|
}
|
||||||
{
|
}
|
||||||
// Replace a list of members
|
|
||||||
if (operation.Path?.ToLowerInvariant() == "members")
|
private async Task HandleOperationAsync(Group group, ScimPatchModel.OperationModel operation)
|
||||||
|
{
|
||||||
|
switch (operation.Op?.ToLowerInvariant())
|
||||||
|
{
|
||||||
|
// Replace a list of members
|
||||||
|
case PatchOps.Replace when operation.Path?.ToLowerInvariant() == PatchPaths.Members:
|
||||||
{
|
{
|
||||||
var ids = GetOperationValueIds(operation.Value);
|
var ids = GetOperationValueIds(operation.Value);
|
||||||
await _groupRepository.UpdateUsersAsync(group.Id, ids);
|
await _groupRepository.UpdateUsersAsync(group.Id, ids);
|
||||||
operationHandled = true;
|
break;
|
||||||
}
|
}
|
||||||
// Replace group name from path
|
|
||||||
else if (operation.Path?.ToLowerInvariant() == "displayname")
|
// Replace group name from path
|
||||||
|
case PatchOps.Replace when operation.Path?.ToLowerInvariant() == PatchPaths.DisplayName:
|
||||||
{
|
{
|
||||||
group.Name = operation.Value.GetString();
|
group.Name = operation.Value.GetString();
|
||||||
|
var organization = await _organizationRepository.GetByIdAsync(group.OrganizationId);
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
|
await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
|
||||||
operationHandled = true;
|
break;
|
||||||
}
|
}
|
||||||
// Replace group name from value object
|
|
||||||
else if (string.IsNullOrWhiteSpace(operation.Path) &&
|
// Replace group name from value object
|
||||||
operation.Value.TryGetProperty("displayName", out var displayNameProperty))
|
case PatchOps.Replace when
|
||||||
|
string.IsNullOrWhiteSpace(operation.Path) &&
|
||||||
|
operation.Value.TryGetProperty("displayName", out var displayNameProperty):
|
||||||
{
|
{
|
||||||
group.Name = displayNameProperty.GetString();
|
group.Name = displayNameProperty.GetString();
|
||||||
|
var organization = await _organizationRepository.GetByIdAsync(group.OrganizationId);
|
||||||
|
if (organization == null)
|
||||||
|
{
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
|
await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
|
||||||
operationHandled = true;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Add a single member
|
// Add a single member
|
||||||
else if (operation.Op?.ToLowerInvariant() == "add" &&
|
case PatchOps.Add when
|
||||||
!string.IsNullOrWhiteSpace(operation.Path) &&
|
!string.IsNullOrWhiteSpace(operation.Path) &&
|
||||||
operation.Path.ToLowerInvariant().StartsWith("members[value eq "))
|
operation.Path.StartsWith("members[value eq ", StringComparison.OrdinalIgnoreCase) &&
|
||||||
{
|
TryGetOperationPathId(operation.Path, out var addId):
|
||||||
var addId = GetOperationPathId(operation.Path);
|
{
|
||||||
if (addId.HasValue)
|
await AddMembersAsync(group, [addId]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a list of members
|
||||||
|
case PatchOps.Add when
|
||||||
|
operation.Path?.ToLowerInvariant() == PatchPaths.Members:
|
||||||
|
{
|
||||||
|
await AddMembersAsync(group, GetOperationValueIds(operation.Value));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a single member
|
||||||
|
case PatchOps.Remove when
|
||||||
|
!string.IsNullOrWhiteSpace(operation.Path) &&
|
||||||
|
operation.Path.StartsWith("members[value eq ", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
TryGetOperationPathId(operation.Path, out var removeId):
|
||||||
|
{
|
||||||
|
await _groupService.DeleteUserAsync(group, removeId, EventSystemUser.SCIM);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove a list of members
|
||||||
|
case PatchOps.Remove when
|
||||||
|
operation.Path?.ToLowerInvariant() == PatchPaths.Members:
|
||||||
{
|
{
|
||||||
var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();
|
var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();
|
||||||
orgUserIds.Add(addId.Value);
|
foreach (var v in GetOperationValueIds(operation.Value))
|
||||||
|
{
|
||||||
|
orgUserIds.Remove(v);
|
||||||
|
}
|
||||||
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
|
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
|
||||||
operationHandled = true;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
// Add a list of members
|
|
||||||
else if (operation.Op?.ToLowerInvariant() == "add" &&
|
|
||||||
operation.Path?.ToLowerInvariant() == "members")
|
|
||||||
{
|
|
||||||
var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();
|
|
||||||
foreach (var v in GetOperationValueIds(operation.Value))
|
|
||||||
{
|
|
||||||
orgUserIds.Add(v);
|
|
||||||
}
|
|
||||||
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
|
|
||||||
operationHandled = true;
|
|
||||||
}
|
|
||||||
// Remove a single member
|
|
||||||
else if (operation.Op?.ToLowerInvariant() == "remove" &&
|
|
||||||
!string.IsNullOrWhiteSpace(operation.Path) &&
|
|
||||||
operation.Path.ToLowerInvariant().StartsWith("members[value eq "))
|
|
||||||
{
|
|
||||||
var removeId = GetOperationPathId(operation.Path);
|
|
||||||
if (removeId.HasValue)
|
|
||||||
{
|
|
||||||
await _groupService.DeleteUserAsync(group, removeId.Value, EventSystemUser.SCIM);
|
|
||||||
operationHandled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Remove a list of members
|
|
||||||
else if (operation.Op?.ToLowerInvariant() == "remove" &&
|
|
||||||
operation.Path?.ToLowerInvariant() == "members")
|
|
||||||
{
|
|
||||||
var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();
|
|
||||||
foreach (var v in GetOperationValueIds(operation.Value))
|
|
||||||
{
|
|
||||||
orgUserIds.Remove(v);
|
|
||||||
}
|
|
||||||
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
|
|
||||||
operationHandled = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!operationHandled)
|
default:
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Group patch operation not handled: {0} : ",
|
_logger.LogWarning("Group patch operation not handled: {OperationOp}:{OperationPath}", operation.Op, operation.Path);
|
||||||
string.Join(", ", model.Operations.Select(o => $"{o.Op}:{o.Path}")));
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Guid> GetOperationValueIds(JsonElement objArray)
|
private async Task AddMembersAsync(Group group, HashSet<Guid> usersToAdd)
|
||||||
{
|
{
|
||||||
var ids = new List<Guid>();
|
// Azure Entra ID is known to send redundant "add" requests for each existing member every time any member
|
||||||
|
// is removed. To avoid excessive load on the database, we check against the high availability replica and
|
||||||
|
// return early if they already exist.
|
||||||
|
var groupMembers = await _groupRepository.GetManyUserIdsByIdAsync(group.Id, useReadOnlyReplica: true);
|
||||||
|
if (usersToAdd.IsSubsetOf(groupMembers))
|
||||||
|
{
|
||||||
|
_logger.LogDebug("Ignoring duplicate SCIM request to add members {Members} to group {Group}", usersToAdd, group.Id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _groupRepository.AddGroupUsersByIdAsync(group.Id, usersToAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HashSet<Guid> GetOperationValueIds(JsonElement objArray)
|
||||||
|
{
|
||||||
|
var ids = new HashSet<Guid>();
|
||||||
foreach (var obj in objArray.EnumerateArray())
|
foreach (var obj in objArray.EnumerateArray())
|
||||||
{
|
{
|
||||||
if (obj.TryGetProperty("value", out var valueProperty))
|
if (obj.TryGetProperty("value", out var valueProperty))
|
||||||
@ -141,13 +162,9 @@ public class PatchGroupCommand : IPatchGroupCommand
|
|||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Guid? GetOperationPathId(string path)
|
private static bool TryGetOperationPathId(string path, out Guid pathId)
|
||||||
{
|
{
|
||||||
// Parse Guid from string like: members[value eq "{GUID}"}]
|
// Parse Guid from string like: members[value eq "{GUID}"}]
|
||||||
if (Guid.TryParse(path.Substring(18).Replace("\"]", string.Empty), out var id))
|
return Guid.TryParse(path.Substring(18).Replace("\"]", string.Empty), out pathId);
|
||||||
{
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,170 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
|
||||||
using Bit.Core.AdminConsole.Services;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Exceptions;
|
|
||||||
using Bit.Core.Repositories;
|
|
||||||
using Bit.Scim.Groups.Interfaces;
|
|
||||||
using Bit.Scim.Models;
|
|
||||||
using Bit.Scim.Utilities;
|
|
||||||
|
|
||||||
namespace Bit.Scim.Groups;
|
|
||||||
|
|
||||||
public class PatchGroupCommandvNext : IPatchGroupCommandvNext
|
|
||||||
{
|
|
||||||
private readonly IGroupRepository _groupRepository;
|
|
||||||
private readonly IGroupService _groupService;
|
|
||||||
private readonly IUpdateGroupCommand _updateGroupCommand;
|
|
||||||
private readonly ILogger<PatchGroupCommandvNext> _logger;
|
|
||||||
private readonly IOrganizationRepository _organizationRepository;
|
|
||||||
|
|
||||||
public PatchGroupCommandvNext(
|
|
||||||
IGroupRepository groupRepository,
|
|
||||||
IGroupService groupService,
|
|
||||||
IUpdateGroupCommand updateGroupCommand,
|
|
||||||
ILogger<PatchGroupCommandvNext> logger,
|
|
||||||
IOrganizationRepository organizationRepository)
|
|
||||||
{
|
|
||||||
_groupRepository = groupRepository;
|
|
||||||
_groupService = groupService;
|
|
||||||
_updateGroupCommand = updateGroupCommand;
|
|
||||||
_logger = logger;
|
|
||||||
_organizationRepository = organizationRepository;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task PatchGroupAsync(Group group, ScimPatchModel model)
|
|
||||||
{
|
|
||||||
foreach (var operation in model.Operations)
|
|
||||||
{
|
|
||||||
await HandleOperationAsync(group, operation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task HandleOperationAsync(Group group, ScimPatchModel.OperationModel operation)
|
|
||||||
{
|
|
||||||
switch (operation.Op?.ToLowerInvariant())
|
|
||||||
{
|
|
||||||
// Replace a list of members
|
|
||||||
case PatchOps.Replace when operation.Path?.ToLowerInvariant() == PatchPaths.Members:
|
|
||||||
{
|
|
||||||
var ids = GetOperationValueIds(operation.Value);
|
|
||||||
await _groupRepository.UpdateUsersAsync(group.Id, ids);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace group name from path
|
|
||||||
case PatchOps.Replace when operation.Path?.ToLowerInvariant() == PatchPaths.DisplayName:
|
|
||||||
{
|
|
||||||
group.Name = operation.Value.GetString();
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(group.OrganizationId);
|
|
||||||
if (organization == null)
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace group name from value object
|
|
||||||
case PatchOps.Replace when
|
|
||||||
string.IsNullOrWhiteSpace(operation.Path) &&
|
|
||||||
operation.Value.TryGetProperty("displayName", out var displayNameProperty):
|
|
||||||
{
|
|
||||||
group.Name = displayNameProperty.GetString();
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(group.OrganizationId);
|
|
||||||
if (organization == null)
|
|
||||||
{
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
await _updateGroupCommand.UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a single member
|
|
||||||
case PatchOps.Add when
|
|
||||||
!string.IsNullOrWhiteSpace(operation.Path) &&
|
|
||||||
operation.Path.StartsWith("members[value eq ", StringComparison.OrdinalIgnoreCase) &&
|
|
||||||
TryGetOperationPathId(operation.Path, out var addId):
|
|
||||||
{
|
|
||||||
await AddMembersAsync(group, [addId]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a list of members
|
|
||||||
case PatchOps.Add when
|
|
||||||
operation.Path?.ToLowerInvariant() == PatchPaths.Members:
|
|
||||||
{
|
|
||||||
await AddMembersAsync(group, GetOperationValueIds(operation.Value));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove a single member
|
|
||||||
case PatchOps.Remove when
|
|
||||||
!string.IsNullOrWhiteSpace(operation.Path) &&
|
|
||||||
operation.Path.StartsWith("members[value eq ", StringComparison.OrdinalIgnoreCase) &&
|
|
||||||
TryGetOperationPathId(operation.Path, out var removeId):
|
|
||||||
{
|
|
||||||
await _groupService.DeleteUserAsync(group, removeId, EventSystemUser.SCIM);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove a list of members
|
|
||||||
case PatchOps.Remove when
|
|
||||||
operation.Path?.ToLowerInvariant() == PatchPaths.Members:
|
|
||||||
{
|
|
||||||
var orgUserIds = (await _groupRepository.GetManyUserIdsByIdAsync(group.Id)).ToHashSet();
|
|
||||||
foreach (var v in GetOperationValueIds(operation.Value))
|
|
||||||
{
|
|
||||||
orgUserIds.Remove(v);
|
|
||||||
}
|
|
||||||
await _groupRepository.UpdateUsersAsync(group.Id, orgUserIds);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Group patch operation not handled: {OperationOp}:{OperationPath}", operation.Op, operation.Path);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AddMembersAsync(Group group, HashSet<Guid> usersToAdd)
|
|
||||||
{
|
|
||||||
// Azure Entra ID is known to send redundant "add" requests for each existing member every time any member
|
|
||||||
// is removed. To avoid excessive load on the database, we check against the high availability replica and
|
|
||||||
// return early if they already exist.
|
|
||||||
var groupMembers = await _groupRepository.GetManyUserIdsByIdAsync(group.Id, useReadOnlyReplica: true);
|
|
||||||
if (usersToAdd.IsSubsetOf(groupMembers))
|
|
||||||
{
|
|
||||||
_logger.LogDebug("Ignoring duplicate SCIM request to add members {Members} to group {Group}", usersToAdd, group.Id);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _groupRepository.AddGroupUsersByIdAsync(group.Id, usersToAdd);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static HashSet<Guid> GetOperationValueIds(JsonElement objArray)
|
|
||||||
{
|
|
||||||
var ids = new HashSet<Guid>();
|
|
||||||
foreach (var obj in objArray.EnumerateArray())
|
|
||||||
{
|
|
||||||
if (obj.TryGetProperty("value", out var valueProperty))
|
|
||||||
{
|
|
||||||
if (valueProperty.TryGetGuid(out var guid))
|
|
||||||
{
|
|
||||||
ids.Add(guid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ids;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryGetOperationPathId(string path, out Guid pathId)
|
|
||||||
{
|
|
||||||
// Parse Guid from string like: members[value eq "{GUID}"}]
|
|
||||||
return Guid.TryParse(path.Substring(18).Replace("\"]", string.Empty), out pathId);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,5 @@
|
|||||||
using Bit.Core.Enums;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||||
|
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;
|
||||||
@ -11,15 +12,18 @@ public class PatchUserCommand : IPatchUserCommand
|
|||||||
{
|
{
|
||||||
private readonly IOrganizationUserRepository _organizationUserRepository;
|
private readonly IOrganizationUserRepository _organizationUserRepository;
|
||||||
private readonly IOrganizationService _organizationService;
|
private readonly IOrganizationService _organizationService;
|
||||||
|
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
|
||||||
private readonly ILogger<PatchUserCommand> _logger;
|
private readonly ILogger<PatchUserCommand> _logger;
|
||||||
|
|
||||||
public PatchUserCommand(
|
public PatchUserCommand(
|
||||||
IOrganizationUserRepository organizationUserRepository,
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
IOrganizationService organizationService,
|
IOrganizationService organizationService,
|
||||||
|
IRestoreOrganizationUserCommand restoreOrganizationUserCommand,
|
||||||
ILogger<PatchUserCommand> logger)
|
ILogger<PatchUserCommand> logger)
|
||||||
{
|
{
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
_organizationService = organizationService;
|
_organizationService = organizationService;
|
||||||
|
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,7 +75,7 @@ public class PatchUserCommand : IPatchUserCommand
|
|||||||
{
|
{
|
||||||
if (active && orgUser.Status == OrganizationUserStatusType.Revoked)
|
if (active && orgUser.Status == OrganizationUserStatusType.Revoked)
|
||||||
{
|
{
|
||||||
await _organizationService.RestoreUserAsync(orgUser, EventSystemUser.SCIM);
|
await _restoreOrganizationUserCommand.RestoreUserAsync(orgUser, EventSystemUser.SCIM);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
else if (!active && orgUser.Status != OrganizationUserStatusType.Revoked)
|
else if (!active && orgUser.Status != OrganizationUserStatusType.Revoked)
|
||||||
|
@ -10,7 +10,6 @@ public static class ScimServiceCollectionExtensions
|
|||||||
public static void AddScimGroupCommands(this IServiceCollection services)
|
public static void AddScimGroupCommands(this IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddScoped<IPatchGroupCommand, PatchGroupCommand>();
|
services.AddScoped<IPatchGroupCommand, PatchGroupCommand>();
|
||||||
services.AddScoped<IPatchGroupCommandvNext, PatchGroupCommandvNext>();
|
|
||||||
services.AddScoped<IPostGroupCommand, PostGroupCommand>();
|
services.AddScoped<IPostGroupCommand, PostGroupCommand>();
|
||||||
services.AddScoped<IPutGroupCommand, PutGroupCommand>();
|
services.AddScoped<IPutGroupCommand, PutGroupCommand>();
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ public class GroupsControllerPatchTests : IClassFixture<ScimApplicationFactory>,
|
|||||||
{
|
{
|
||||||
var databaseContext = _factory.GetDatabaseContext();
|
var databaseContext = _factory.GetDatabaseContext();
|
||||||
_factory.ReinitializeDbForTests(databaseContext);
|
_factory.ReinitializeDbForTests(databaseContext);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,251 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
|
||||||
using Bit.Core.Services;
|
|
||||||
using Bit.Scim.Groups.Interfaces;
|
|
||||||
using Bit.Scim.IntegrationTest.Factories;
|
|
||||||
using Bit.Scim.Models;
|
|
||||||
using Bit.Scim.Utilities;
|
|
||||||
using Bit.Test.Common.Helpers;
|
|
||||||
using NSubstitute;
|
|
||||||
using NSubstitute.ExceptionExtensions;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Bit.Scim.IntegrationTest.Controllers.v2;
|
|
||||||
|
|
||||||
public class GroupsControllerPatchTestsvNext : IClassFixture<ScimApplicationFactory>, IAsyncLifetime
|
|
||||||
{
|
|
||||||
private readonly ScimApplicationFactory _factory;
|
|
||||||
|
|
||||||
public GroupsControllerPatchTestsvNext(ScimApplicationFactory factory)
|
|
||||||
{
|
|
||||||
_factory = factory;
|
|
||||||
|
|
||||||
// Enable the feature flag for new PatchGroupsCommand and stub out the old command to be safe
|
|
||||||
_factory.SubstituteService((IFeatureService featureService)
|
|
||||||
=> featureService.IsEnabled(FeatureFlagKeys.ShortcutDuplicatePatchRequests).Returns(true));
|
|
||||||
_factory.SubstituteService((IPatchGroupCommand patchGroupCommand)
|
|
||||||
=> patchGroupCommand.PatchGroupAsync(Arg.Any<Organization>(), Arg.Any<Guid>(), Arg.Any<ScimPatchModel>())
|
|
||||||
.ThrowsAsync(new Exception("This test suite should be testing the vNext command, but the existing command was called.")));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task InitializeAsync()
|
|
||||||
{
|
|
||||||
var databaseContext = _factory.GetDatabaseContext();
|
|
||||||
_factory.ReinitializeDbForTests(databaseContext);
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
Task IAsyncLifetime.DisposeAsync() => Task.CompletedTask;
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Patch_ReplaceDisplayName_Success()
|
|
||||||
{
|
|
||||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
|
||||||
var groupId = ScimApplicationFactory.TestGroupId1;
|
|
||||||
var newDisplayName = "Patch Display Name";
|
|
||||||
var inputModel = new ScimPatchModel
|
|
||||||
{
|
|
||||||
Operations = new List<ScimPatchModel.OperationModel>()
|
|
||||||
{
|
|
||||||
new ScimPatchModel.OperationModel
|
|
||||||
{
|
|
||||||
Op = "replace",
|
|
||||||
Value = JsonDocument.Parse($"{{\"displayName\":\"{newDisplayName}\"}}").RootElement
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
|
||||||
};
|
|
||||||
|
|
||||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
|
||||||
|
|
||||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
|
||||||
|
|
||||||
var databaseContext = _factory.GetDatabaseContext();
|
|
||||||
var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId);
|
|
||||||
Assert.Equal(newDisplayName, group.Name);
|
|
||||||
|
|
||||||
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount, databaseContext.GroupUsers.Count());
|
|
||||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1));
|
|
||||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Patch_ReplaceMembers_Success()
|
|
||||||
{
|
|
||||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
|
||||||
var groupId = ScimApplicationFactory.TestGroupId1;
|
|
||||||
var inputModel = new ScimPatchModel
|
|
||||||
{
|
|
||||||
Operations = new List<ScimPatchModel.OperationModel>()
|
|
||||||
{
|
|
||||||
new ScimPatchModel.OperationModel
|
|
||||||
{
|
|
||||||
Op = "replace",
|
|
||||||
Path = "members",
|
|
||||||
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId2}\"}}]").RootElement
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
|
||||||
};
|
|
||||||
|
|
||||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
|
||||||
|
|
||||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
|
||||||
|
|
||||||
var databaseContext = _factory.GetDatabaseContext();
|
|
||||||
Assert.Single(databaseContext.GroupUsers);
|
|
||||||
|
|
||||||
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount - 1, databaseContext.GroupUsers.Count());
|
|
||||||
var groupUser = databaseContext.GroupUsers.FirstOrDefault();
|
|
||||||
Assert.Equal(ScimApplicationFactory.TestOrganizationUserId2, groupUser.OrganizationUserId);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Patch_AddSingleMember_Success()
|
|
||||||
{
|
|
||||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
|
||||||
var groupId = ScimApplicationFactory.TestGroupId1;
|
|
||||||
var inputModel = new ScimPatchModel
|
|
||||||
{
|
|
||||||
Operations = new List<ScimPatchModel.OperationModel>()
|
|
||||||
{
|
|
||||||
new ScimPatchModel.OperationModel
|
|
||||||
{
|
|
||||||
Op = "add",
|
|
||||||
Path = $"members[value eq \"{ScimApplicationFactory.TestOrganizationUserId2}\"]",
|
|
||||||
Value = JsonDocument.Parse("{}").RootElement
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
|
||||||
};
|
|
||||||
|
|
||||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
|
||||||
|
|
||||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
|
||||||
|
|
||||||
var databaseContext = _factory.GetDatabaseContext();
|
|
||||||
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount + 1, databaseContext.GroupUsers.Count());
|
|
||||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId1));
|
|
||||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2));
|
|
||||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId4));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Patch_AddListMembers_Success()
|
|
||||||
{
|
|
||||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
|
||||||
var groupId = ScimApplicationFactory.TestGroupId2;
|
|
||||||
var inputModel = new ScimPatchModel
|
|
||||||
{
|
|
||||||
Operations = new List<ScimPatchModel.OperationModel>()
|
|
||||||
{
|
|
||||||
new ScimPatchModel.OperationModel
|
|
||||||
{
|
|
||||||
Op = "add",
|
|
||||||
Path = "members",
|
|
||||||
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId2}\"}},{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId3}\"}}]").RootElement
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
|
||||||
};
|
|
||||||
|
|
||||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
|
||||||
|
|
||||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
|
||||||
|
|
||||||
var databaseContext = _factory.GetDatabaseContext();
|
|
||||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId2));
|
|
||||||
Assert.True(databaseContext.GroupUsers.Any(gu => gu.GroupId == groupId && gu.OrganizationUserId == ScimApplicationFactory.TestOrganizationUserId3));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Patch_RemoveSingleMember_ReplaceDisplayName_Success()
|
|
||||||
{
|
|
||||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
|
||||||
var groupId = ScimApplicationFactory.TestGroupId1;
|
|
||||||
var newDisplayName = "Patch Display Name";
|
|
||||||
var inputModel = new ScimPatchModel
|
|
||||||
{
|
|
||||||
Operations = new List<ScimPatchModel.OperationModel>()
|
|
||||||
{
|
|
||||||
new ScimPatchModel.OperationModel
|
|
||||||
{
|
|
||||||
Op = "remove",
|
|
||||||
Path = $"members[value eq \"{ScimApplicationFactory.TestOrganizationUserId1}\"]",
|
|
||||||
Value = JsonDocument.Parse("{}").RootElement
|
|
||||||
},
|
|
||||||
new ScimPatchModel.OperationModel
|
|
||||||
{
|
|
||||||
Op = "replace",
|
|
||||||
Value = JsonDocument.Parse($"{{\"displayName\":\"{newDisplayName}\"}}").RootElement
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
|
||||||
};
|
|
||||||
|
|
||||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
|
||||||
|
|
||||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
|
||||||
|
|
||||||
var databaseContext = _factory.GetDatabaseContext();
|
|
||||||
Assert.Equal(ScimApplicationFactory.InitialGroupUsersCount - 1, databaseContext.GroupUsers.Count());
|
|
||||||
Assert.Equal(ScimApplicationFactory.InitialGroupCount, databaseContext.Groups.Count());
|
|
||||||
|
|
||||||
var group = databaseContext.Groups.FirstOrDefault(g => g.Id == groupId);
|
|
||||||
Assert.Equal(newDisplayName, group.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Patch_RemoveListMembers_Success()
|
|
||||||
{
|
|
||||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
|
||||||
var groupId = ScimApplicationFactory.TestGroupId1;
|
|
||||||
var inputModel = new ScimPatchModel
|
|
||||||
{
|
|
||||||
Operations = new List<ScimPatchModel.OperationModel>()
|
|
||||||
{
|
|
||||||
new ScimPatchModel.OperationModel
|
|
||||||
{
|
|
||||||
Op = "remove",
|
|
||||||
Path = "members",
|
|
||||||
Value = JsonDocument.Parse($"[{{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId1}\"}}, {{\"value\":\"{ScimApplicationFactory.TestOrganizationUserId4}\"}}]").RootElement
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
|
||||||
};
|
|
||||||
|
|
||||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
|
||||||
|
|
||||||
Assert.Equal(StatusCodes.Status204NoContent, context.Response.StatusCode);
|
|
||||||
|
|
||||||
var databaseContext = _factory.GetDatabaseContext();
|
|
||||||
Assert.Empty(databaseContext.GroupUsers);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Fact]
|
|
||||||
public async Task Patch_NotFound()
|
|
||||||
{
|
|
||||||
var organizationId = ScimApplicationFactory.TestOrganizationId1;
|
|
||||||
var groupId = Guid.NewGuid();
|
|
||||||
var inputModel = new Models.ScimPatchModel
|
|
||||||
{
|
|
||||||
Operations = new List<ScimPatchModel.OperationModel>(),
|
|
||||||
Schemas = new List<string>() { ScimConstants.Scim2SchemaGroup }
|
|
||||||
};
|
|
||||||
var expectedResponse = new ScimErrorResponseModel
|
|
||||||
{
|
|
||||||
Status = StatusCodes.Status404NotFound,
|
|
||||||
Detail = "Group not found.",
|
|
||||||
Schemas = new List<string> { ScimConstants.Scim2SchemaError }
|
|
||||||
};
|
|
||||||
|
|
||||||
var context = await _factory.GroupsPatchAsync(organizationId, groupId, inputModel);
|
|
||||||
|
|
||||||
Assert.Equal(StatusCodes.Status404NotFound, context.Response.StatusCode);
|
|
||||||
|
|
||||||
var responseModel = JsonSerializer.Deserialize<ScimErrorResponseModel>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
|
|
||||||
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,15 +1,18 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using AutoFixture;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
using Bit.Scim.Groups;
|
using Bit.Scim.Groups;
|
||||||
using Bit.Scim.Models;
|
using Bit.Scim.Models;
|
||||||
using Bit.Scim.Utilities;
|
using Bit.Scim.Utilities;
|
||||||
using Bit.Test.Common.AutoFixture;
|
using Bit.Test.Common.AutoFixture;
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
@ -20,19 +23,16 @@ public class PatchGroupCommandTests
|
|||||||
{
|
{
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task PatchGroup_ReplaceListMembers_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, IEnumerable<Guid> userIds)
|
public async Task PatchGroup_ReplaceListMembers_Success(SutProvider<PatchGroupCommand> sutProvider,
|
||||||
|
Organization organization, Group group, IEnumerable<Guid> userIds)
|
||||||
{
|
{
|
||||||
group.OrganizationId = organization.Id;
|
group.OrganizationId = organization.Id;
|
||||||
|
|
||||||
sutProvider.GetDependency<IGroupRepository>()
|
var scimPatchModel = new ScimPatchModel
|
||||||
.GetByIdAsync(group.Id)
|
|
||||||
.Returns(group);
|
|
||||||
|
|
||||||
var scimPatchModel = new Models.ScimPatchModel
|
|
||||||
{
|
{
|
||||||
Operations = new List<ScimPatchModel.OperationModel>
|
Operations = new List<ScimPatchModel.OperationModel>
|
||||||
{
|
{
|
||||||
new ScimPatchModel.OperationModel
|
new()
|
||||||
{
|
{
|
||||||
Op = "replace",
|
Op = "replace",
|
||||||
Path = "members",
|
Path = "members",
|
||||||
@ -42,26 +42,31 @@ public class PatchGroupCommandTests
|
|||||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||||
};
|
};
|
||||||
|
|
||||||
await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel);
|
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(group.Id, Arg.Is<IEnumerable<Guid>>(arg => arg.All(id => userIds.Contains(id))));
|
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(
|
||||||
|
group.Id,
|
||||||
|
Arg.Is<IEnumerable<Guid>>(arg =>
|
||||||
|
arg.Count() == userIds.Count() &&
|
||||||
|
arg.ToHashSet().SetEquals(userIds)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task PatchGroup_ReplaceDisplayNameFromPath_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, string displayName)
|
public async Task PatchGroup_ReplaceDisplayNameFromPath_Success(
|
||||||
|
SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, string displayName)
|
||||||
{
|
{
|
||||||
group.OrganizationId = organization.Id;
|
group.OrganizationId = organization.Id;
|
||||||
|
|
||||||
sutProvider.GetDependency<IGroupRepository>()
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
.GetByIdAsync(group.Id)
|
.GetByIdAsync(organization.Id)
|
||||||
.Returns(group);
|
.Returns(organization);
|
||||||
|
|
||||||
var scimPatchModel = new Models.ScimPatchModel
|
var scimPatchModel = new ScimPatchModel
|
||||||
{
|
{
|
||||||
Operations = new List<ScimPatchModel.OperationModel>
|
Operations = new List<ScimPatchModel.OperationModel>
|
||||||
{
|
{
|
||||||
new ScimPatchModel.OperationModel
|
new()
|
||||||
{
|
{
|
||||||
Op = "replace",
|
Op = "replace",
|
||||||
Path = "displayname",
|
Path = "displayname",
|
||||||
@ -71,27 +76,55 @@ public class PatchGroupCommandTests
|
|||||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||||
};
|
};
|
||||||
|
|
||||||
await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel);
|
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
|
await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
|
||||||
Assert.Equal(displayName, group.Name);
|
Assert.Equal(displayName, group.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PatchGroup_ReplaceDisplayNameFromPath_MissingOrganization_Throws(
|
||||||
|
SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, string displayName)
|
||||||
|
{
|
||||||
|
group.OrganizationId = organization.Id;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organization.Id)
|
||||||
|
.Returns((Organization)null);
|
||||||
|
|
||||||
|
var scimPatchModel = new ScimPatchModel
|
||||||
|
{
|
||||||
|
Operations = new List<ScimPatchModel.OperationModel>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Op = "replace",
|
||||||
|
Path = "displayname",
|
||||||
|
Value = JsonDocument.Parse($"\"{displayName}\"").RootElement
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||||
|
};
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PatchGroupAsync(group, scimPatchModel));
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task PatchGroup_ReplaceDisplayNameFromValueObject_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, string displayName)
|
public async Task PatchGroup_ReplaceDisplayNameFromValueObject_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, string displayName)
|
||||||
{
|
{
|
||||||
group.OrganizationId = organization.Id;
|
group.OrganizationId = organization.Id;
|
||||||
|
|
||||||
sutProvider.GetDependency<IGroupRepository>()
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
.GetByIdAsync(group.Id)
|
.GetByIdAsync(organization.Id)
|
||||||
.Returns(group);
|
.Returns(organization);
|
||||||
|
|
||||||
var scimPatchModel = new Models.ScimPatchModel
|
var scimPatchModel = new ScimPatchModel
|
||||||
{
|
{
|
||||||
Operations = new List<ScimPatchModel.OperationModel>
|
Operations = new List<ScimPatchModel.OperationModel>
|
||||||
{
|
{
|
||||||
new ScimPatchModel.OperationModel
|
new()
|
||||||
{
|
{
|
||||||
Op = "replace",
|
Op = "replace",
|
||||||
Value = JsonDocument.Parse($"{{\"displayName\":\"{displayName}\"}}").RootElement
|
Value = JsonDocument.Parse($"{{\"displayName\":\"{displayName}\"}}").RootElement
|
||||||
@ -100,12 +133,39 @@ public class PatchGroupCommandTests
|
|||||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||||
};
|
};
|
||||||
|
|
||||||
await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel);
|
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
|
await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
|
||||||
Assert.Equal(displayName, group.Name);
|
Assert.Equal(displayName, group.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PatchGroup_ReplaceDisplayNameFromValueObject_MissingOrganization_Throws(
|
||||||
|
SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, string displayName)
|
||||||
|
{
|
||||||
|
group.OrganizationId = organization.Id;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetByIdAsync(organization.Id)
|
||||||
|
.Returns((Organization)null);
|
||||||
|
|
||||||
|
var scimPatchModel = new ScimPatchModel
|
||||||
|
{
|
||||||
|
Operations = new List<ScimPatchModel.OperationModel>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Op = "replace",
|
||||||
|
Value = JsonDocument.Parse($"{{\"displayName\":\"{displayName}\"}}").RootElement
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||||
|
};
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.PatchGroupAsync(group, scimPatchModel));
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task PatchGroup_AddSingleMember_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, ICollection<Guid> existingMembers, Guid userId)
|
public async Task PatchGroup_AddSingleMember_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, ICollection<Guid> existingMembers, Guid userId)
|
||||||
@ -113,18 +173,14 @@ public class PatchGroupCommandTests
|
|||||||
group.OrganizationId = organization.Id;
|
group.OrganizationId = organization.Id;
|
||||||
|
|
||||||
sutProvider.GetDependency<IGroupRepository>()
|
sutProvider.GetDependency<IGroupRepository>()
|
||||||
.GetByIdAsync(group.Id)
|
.GetManyUserIdsByIdAsync(group.Id, true)
|
||||||
.Returns(group);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IGroupRepository>()
|
|
||||||
.GetManyUserIdsByIdAsync(group.Id)
|
|
||||||
.Returns(existingMembers);
|
.Returns(existingMembers);
|
||||||
|
|
||||||
var scimPatchModel = new Models.ScimPatchModel
|
var scimPatchModel = new ScimPatchModel
|
||||||
{
|
{
|
||||||
Operations = new List<ScimPatchModel.OperationModel>
|
Operations = new List<ScimPatchModel.OperationModel>
|
||||||
{
|
{
|
||||||
new ScimPatchModel.OperationModel
|
new()
|
||||||
{
|
{
|
||||||
Op = "add",
|
Op = "add",
|
||||||
Path = $"members[value eq \"{userId}\"]",
|
Path = $"members[value eq \"{userId}\"]",
|
||||||
@ -133,9 +189,47 @@ public class PatchGroupCommandTests
|
|||||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||||
};
|
};
|
||||||
|
|
||||||
await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel);
|
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(group.Id, Arg.Is<IEnumerable<Guid>>(arg => arg.All(id => existingMembers.Append(userId).Contains(id))));
|
await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByIdAsync(
|
||||||
|
group.Id,
|
||||||
|
Arg.Is<IEnumerable<Guid>>(arg => arg.Single() == userId));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PatchGroup_AddSingleMember_ReturnsEarlyIfAlreadyInGroup(
|
||||||
|
SutProvider<PatchGroupCommand> sutProvider,
|
||||||
|
Organization organization,
|
||||||
|
Group group,
|
||||||
|
ICollection<Guid> existingMembers)
|
||||||
|
{
|
||||||
|
// User being added is already in group
|
||||||
|
var userId = existingMembers.First();
|
||||||
|
group.OrganizationId = organization.Id;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IGroupRepository>()
|
||||||
|
.GetManyUserIdsByIdAsync(group.Id, true)
|
||||||
|
.Returns(existingMembers);
|
||||||
|
|
||||||
|
var scimPatchModel = new ScimPatchModel
|
||||||
|
{
|
||||||
|
Operations = new List<ScimPatchModel.OperationModel>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Op = "add",
|
||||||
|
Path = $"members[value eq \"{userId}\"]",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||||
|
};
|
||||||
|
|
||||||
|
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IGroupRepository>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.AddGroupUsersByIdAsync(default, default);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@ -145,18 +239,14 @@ public class PatchGroupCommandTests
|
|||||||
group.OrganizationId = organization.Id;
|
group.OrganizationId = organization.Id;
|
||||||
|
|
||||||
sutProvider.GetDependency<IGroupRepository>()
|
sutProvider.GetDependency<IGroupRepository>()
|
||||||
.GetByIdAsync(group.Id)
|
.GetManyUserIdsByIdAsync(group.Id, true)
|
||||||
.Returns(group);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IGroupRepository>()
|
|
||||||
.GetManyUserIdsByIdAsync(group.Id)
|
|
||||||
.Returns(existingMembers);
|
.Returns(existingMembers);
|
||||||
|
|
||||||
var scimPatchModel = new Models.ScimPatchModel
|
var scimPatchModel = new ScimPatchModel
|
||||||
{
|
{
|
||||||
Operations = new List<ScimPatchModel.OperationModel>
|
Operations = new List<ScimPatchModel.OperationModel>
|
||||||
{
|
{
|
||||||
new ScimPatchModel.OperationModel
|
new()
|
||||||
{
|
{
|
||||||
Op = "add",
|
Op = "add",
|
||||||
Path = $"members",
|
Path = $"members",
|
||||||
@ -166,9 +256,101 @@ public class PatchGroupCommandTests
|
|||||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||||
};
|
};
|
||||||
|
|
||||||
await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel);
|
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(group.Id, Arg.Is<IEnumerable<Guid>>(arg => arg.All(id => existingMembers.Concat(userIds).Contains(id))));
|
await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByIdAsync(
|
||||||
|
group.Id,
|
||||||
|
Arg.Is<IEnumerable<Guid>>(arg =>
|
||||||
|
arg.Count() == userIds.Count &&
|
||||||
|
arg.ToHashSet().SetEquals(userIds)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PatchGroup_AddListMembers_IgnoresDuplicatesInRequest(
|
||||||
|
SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group,
|
||||||
|
ICollection<Guid> existingMembers)
|
||||||
|
{
|
||||||
|
// Create 3 userIds
|
||||||
|
var fixture = new Fixture { RepeatCount = 3 };
|
||||||
|
var userIds = fixture.CreateMany<Guid>().ToList();
|
||||||
|
|
||||||
|
// Copy the list and add a duplicate
|
||||||
|
var userIdsWithDuplicate = userIds.Append(userIds.First()).ToList();
|
||||||
|
Assert.Equal(4, userIdsWithDuplicate.Count);
|
||||||
|
|
||||||
|
group.OrganizationId = organization.Id;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IGroupRepository>()
|
||||||
|
.GetManyUserIdsByIdAsync(group.Id, true)
|
||||||
|
.Returns(existingMembers);
|
||||||
|
|
||||||
|
var scimPatchModel = new ScimPatchModel
|
||||||
|
{
|
||||||
|
Operations = new List<ScimPatchModel.OperationModel>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Op = "add",
|
||||||
|
Path = $"members",
|
||||||
|
Value = JsonDocument.Parse(JsonSerializer
|
||||||
|
.Serialize(userIdsWithDuplicate
|
||||||
|
.Select(uid => new { value = uid })
|
||||||
|
.ToArray())).RootElement
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||||
|
};
|
||||||
|
|
||||||
|
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByIdAsync(
|
||||||
|
group.Id,
|
||||||
|
Arg.Is<IEnumerable<Guid>>(arg =>
|
||||||
|
arg.Count() == 3 &&
|
||||||
|
arg.ToHashSet().SetEquals(userIds)));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PatchGroup_AddListMembers_SuccessIfOnlySomeUsersAreInGroup(
|
||||||
|
SutProvider<PatchGroupCommand> sutProvider,
|
||||||
|
Organization organization, Group group,
|
||||||
|
ICollection<Guid> existingMembers,
|
||||||
|
ICollection<Guid> userIds)
|
||||||
|
{
|
||||||
|
// A user is already in the group, but some still need to be added
|
||||||
|
userIds.Add(existingMembers.First());
|
||||||
|
|
||||||
|
group.OrganizationId = organization.Id;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IGroupRepository>()
|
||||||
|
.GetManyUserIdsByIdAsync(group.Id, true)
|
||||||
|
.Returns(existingMembers);
|
||||||
|
|
||||||
|
var scimPatchModel = new ScimPatchModel
|
||||||
|
{
|
||||||
|
Operations = new List<ScimPatchModel.OperationModel>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
Op = "add",
|
||||||
|
Path = $"members",
|
||||||
|
Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||||
|
};
|
||||||
|
|
||||||
|
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IGroupRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.AddGroupUsersByIdAsync(
|
||||||
|
group.Id,
|
||||||
|
Arg.Is<IEnumerable<Guid>>(arg =>
|
||||||
|
arg.Count() == userIds.Count &&
|
||||||
|
arg.ToHashSet().SetEquals(userIds)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@ -177,10 +359,6 @@ public class PatchGroupCommandTests
|
|||||||
{
|
{
|
||||||
group.OrganizationId = organization.Id;
|
group.OrganizationId = organization.Id;
|
||||||
|
|
||||||
sutProvider.GetDependency<IGroupRepository>()
|
|
||||||
.GetByIdAsync(group.Id)
|
|
||||||
.Returns(group);
|
|
||||||
|
|
||||||
var scimPatchModel = new Models.ScimPatchModel
|
var scimPatchModel = new Models.ScimPatchModel
|
||||||
{
|
{
|
||||||
Operations = new List<ScimPatchModel.OperationModel>
|
Operations = new List<ScimPatchModel.OperationModel>
|
||||||
@ -194,21 +372,19 @@ public class PatchGroupCommandTests
|
|||||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||||
};
|
};
|
||||||
|
|
||||||
await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel);
|
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IGroupService>().Received(1).DeleteUserAsync(group, userId, EventSystemUser.SCIM);
|
await sutProvider.GetDependency<IGroupService>().Received(1).DeleteUserAsync(group, userId, EventSystemUser.SCIM);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task PatchGroup_RemoveListMembers_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group, ICollection<Guid> existingMembers)
|
public async Task PatchGroup_RemoveListMembers_Success(SutProvider<PatchGroupCommand> sutProvider,
|
||||||
|
Organization organization, Group group, ICollection<Guid> existingMembers)
|
||||||
{
|
{
|
||||||
|
List<Guid> usersToRemove = [existingMembers.First(), existingMembers.Skip(1).First()];
|
||||||
group.OrganizationId = organization.Id;
|
group.OrganizationId = organization.Id;
|
||||||
|
|
||||||
sutProvider.GetDependency<IGroupRepository>()
|
|
||||||
.GetByIdAsync(group.Id)
|
|
||||||
.Returns(group);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IGroupRepository>()
|
sutProvider.GetDependency<IGroupRepository>()
|
||||||
.GetManyUserIdsByIdAsync(group.Id)
|
.GetManyUserIdsByIdAsync(group.Id)
|
||||||
.Returns(existingMembers);
|
.Returns(existingMembers);
|
||||||
@ -217,30 +393,58 @@ public class PatchGroupCommandTests
|
|||||||
{
|
{
|
||||||
Operations = new List<ScimPatchModel.OperationModel>
|
Operations = new List<ScimPatchModel.OperationModel>
|
||||||
{
|
{
|
||||||
new ScimPatchModel.OperationModel
|
new()
|
||||||
{
|
{
|
||||||
Op = "remove",
|
Op = "remove",
|
||||||
Path = $"members",
|
Path = $"members",
|
||||||
Value = JsonDocument.Parse(JsonSerializer.Serialize(existingMembers.Select(uid => new { value = uid }).ToArray())).RootElement
|
Value = JsonDocument.Parse(JsonSerializer.Serialize(usersToRemove.Select(uid => new { value = uid }).ToArray())).RootElement
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||||
};
|
};
|
||||||
|
|
||||||
await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel);
|
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(group.Id, Arg.Is<IEnumerable<Guid>>(arg => arg.All(id => existingMembers.Contains(id))));
|
var expectedRemainingUsers = existingMembers.Skip(2).ToList();
|
||||||
|
await sutProvider.GetDependency<IGroupRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.UpdateUsersAsync(
|
||||||
|
group.Id,
|
||||||
|
Arg.Is<IEnumerable<Guid>>(arg =>
|
||||||
|
arg.Count() == expectedRemainingUsers.Count &&
|
||||||
|
arg.ToHashSet().SetEquals(expectedRemainingUsers)));
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData]
|
[BitAutoData]
|
||||||
public async Task PatchGroup_NoAction_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group)
|
public async Task PatchGroup_InvalidOperation_Success(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group)
|
||||||
{
|
{
|
||||||
group.OrganizationId = organization.Id;
|
group.OrganizationId = organization.Id;
|
||||||
|
|
||||||
sutProvider.GetDependency<IGroupRepository>()
|
var scimPatchModel = new Models.ScimPatchModel
|
||||||
.GetByIdAsync(group.Id)
|
{
|
||||||
.Returns(group);
|
Operations = [new ScimPatchModel.OperationModel { Op = "invalid operation" }],
|
||||||
|
Schemas = [ScimConstants.Scim2SchemaUser]
|
||||||
|
};
|
||||||
|
|
||||||
|
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
||||||
|
|
||||||
|
// Assert: no operation performed
|
||||||
|
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default);
|
||||||
|
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyUserIdsByIdAsync(default);
|
||||||
|
await sutProvider.GetDependency<IUpdateGroupCommand>().DidNotReceiveWithAnyArgs().UpdateGroupAsync(default, default);
|
||||||
|
await sutProvider.GetDependency<IGroupService>().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default);
|
||||||
|
|
||||||
|
// Assert: logging
|
||||||
|
sutProvider.GetDependency<ILogger<PatchGroupCommand>>().ReceivedWithAnyArgs().LogWarning(default);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task PatchGroup_NoOperation_Success(
|
||||||
|
SutProvider<PatchGroupCommand> sutProvider, Organization organization, Group group)
|
||||||
|
{
|
||||||
|
group.OrganizationId = organization.Id;
|
||||||
|
|
||||||
var scimPatchModel = new Models.ScimPatchModel
|
var scimPatchModel = new Models.ScimPatchModel
|
||||||
{
|
{
|
||||||
@ -248,45 +452,11 @@ public class PatchGroupCommandTests
|
|||||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
||||||
};
|
};
|
||||||
|
|
||||||
await sutProvider.Sut.PatchGroupAsync(organization, group.Id, scimPatchModel);
|
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default);
|
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default);
|
||||||
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyUserIdsByIdAsync(default);
|
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyUserIdsByIdAsync(default);
|
||||||
await sutProvider.GetDependency<IUpdateGroupCommand>().DidNotReceiveWithAnyArgs().UpdateGroupAsync(default, default);
|
await sutProvider.GetDependency<IUpdateGroupCommand>().DidNotReceiveWithAnyArgs().UpdateGroupAsync(default, default);
|
||||||
await sutProvider.GetDependency<IGroupService>().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default);
|
await sutProvider.GetDependency<IGroupService>().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData]
|
|
||||||
public async Task PatchGroup_NotFound_Throws(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Guid groupId)
|
|
||||||
{
|
|
||||||
var scimPatchModel = new Models.ScimPatchModel
|
|
||||||
{
|
|
||||||
Operations = new List<ScimPatchModel.OperationModel>(),
|
|
||||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
|
||||||
};
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.PatchGroupAsync(organization, groupId, scimPatchModel));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData]
|
|
||||||
public async Task PatchGroup_MismatchingOrganizationId_Throws(SutProvider<PatchGroupCommand> sutProvider, Organization organization, Guid groupId)
|
|
||||||
{
|
|
||||||
var scimPatchModel = new Models.ScimPatchModel
|
|
||||||
{
|
|
||||||
Operations = new List<ScimPatchModel.OperationModel>(),
|
|
||||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
|
||||||
};
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IGroupRepository>()
|
|
||||||
.GetByIdAsync(groupId)
|
|
||||||
.Returns(new Group
|
|
||||||
{
|
|
||||||
Id = groupId,
|
|
||||||
OrganizationId = Guid.NewGuid()
|
|
||||||
});
|
|
||||||
|
|
||||||
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.PatchGroupAsync(organization, groupId, scimPatchModel));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,381 +0,0 @@
|
|||||||
using System.Text.Json;
|
|
||||||
using AutoFixture;
|
|
||||||
using Bit.Core.AdminConsole.Entities;
|
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Groups.Interfaces;
|
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
|
||||||
using Bit.Core.AdminConsole.Services;
|
|
||||||
using Bit.Core.Enums;
|
|
||||||
using Bit.Core.Repositories;
|
|
||||||
using Bit.Scim.Groups;
|
|
||||||
using Bit.Scim.Models;
|
|
||||||
using Bit.Scim.Utilities;
|
|
||||||
using Bit.Test.Common.AutoFixture;
|
|
||||||
using Bit.Test.Common.AutoFixture.Attributes;
|
|
||||||
using NSubstitute;
|
|
||||||
using Xunit;
|
|
||||||
|
|
||||||
namespace Bit.Scim.Test.Groups;
|
|
||||||
|
|
||||||
[SutProviderCustomize]
|
|
||||||
public class PatchGroupCommandvNextTests
|
|
||||||
{
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData]
|
|
||||||
public async Task PatchGroup_ReplaceListMembers_Success(SutProvider<PatchGroupCommandvNext> sutProvider,
|
|
||||||
Organization organization, Group group, IEnumerable<Guid> userIds)
|
|
||||||
{
|
|
||||||
group.OrganizationId = organization.Id;
|
|
||||||
|
|
||||||
var scimPatchModel = new ScimPatchModel
|
|
||||||
{
|
|
||||||
Operations = new List<ScimPatchModel.OperationModel>
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Op = "replace",
|
|
||||||
Path = "members",
|
|
||||||
Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
|
||||||
};
|
|
||||||
|
|
||||||
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IGroupRepository>().Received(1).UpdateUsersAsync(
|
|
||||||
group.Id,
|
|
||||||
Arg.Is<IEnumerable<Guid>>(arg =>
|
|
||||||
arg.Count() == userIds.Count() &&
|
|
||||||
arg.ToHashSet().SetEquals(userIds)));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData]
|
|
||||||
public async Task PatchGroup_ReplaceDisplayNameFromPath_Success(
|
|
||||||
SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group, string displayName)
|
|
||||||
{
|
|
||||||
group.OrganizationId = organization.Id;
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>()
|
|
||||||
.GetByIdAsync(organization.Id)
|
|
||||||
.Returns(organization);
|
|
||||||
|
|
||||||
var scimPatchModel = new ScimPatchModel
|
|
||||||
{
|
|
||||||
Operations = new List<ScimPatchModel.OperationModel>
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Op = "replace",
|
|
||||||
Path = "displayname",
|
|
||||||
Value = JsonDocument.Parse($"\"{displayName}\"").RootElement
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
|
||||||
};
|
|
||||||
|
|
||||||
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
|
|
||||||
Assert.Equal(displayName, group.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData]
|
|
||||||
public async Task PatchGroup_ReplaceDisplayNameFromValueObject_Success(SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group, string displayName)
|
|
||||||
{
|
|
||||||
group.OrganizationId = organization.Id;
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationRepository>()
|
|
||||||
.GetByIdAsync(organization.Id)
|
|
||||||
.Returns(organization);
|
|
||||||
|
|
||||||
var scimPatchModel = new ScimPatchModel
|
|
||||||
{
|
|
||||||
Operations = new List<ScimPatchModel.OperationModel>
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Op = "replace",
|
|
||||||
Value = JsonDocument.Parse($"{{\"displayName\":\"{displayName}\"}}").RootElement
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
|
||||||
};
|
|
||||||
|
|
||||||
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IUpdateGroupCommand>().Received(1).UpdateGroupAsync(group, organization, EventSystemUser.SCIM);
|
|
||||||
Assert.Equal(displayName, group.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData]
|
|
||||||
public async Task PatchGroup_AddSingleMember_Success(SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group, ICollection<Guid> existingMembers, Guid userId)
|
|
||||||
{
|
|
||||||
group.OrganizationId = organization.Id;
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IGroupRepository>()
|
|
||||||
.GetManyUserIdsByIdAsync(group.Id, true)
|
|
||||||
.Returns(existingMembers);
|
|
||||||
|
|
||||||
var scimPatchModel = new ScimPatchModel
|
|
||||||
{
|
|
||||||
Operations = new List<ScimPatchModel.OperationModel>
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Op = "add",
|
|
||||||
Path = $"members[value eq \"{userId}\"]",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
|
||||||
};
|
|
||||||
|
|
||||||
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByIdAsync(
|
|
||||||
group.Id,
|
|
||||||
Arg.Is<IEnumerable<Guid>>(arg => arg.Single() == userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData]
|
|
||||||
public async Task PatchGroup_AddSingleMember_ReturnsEarlyIfAlreadyInGroup(
|
|
||||||
SutProvider<PatchGroupCommandvNext> sutProvider,
|
|
||||||
Organization organization,
|
|
||||||
Group group,
|
|
||||||
ICollection<Guid> existingMembers)
|
|
||||||
{
|
|
||||||
// User being added is already in group
|
|
||||||
var userId = existingMembers.First();
|
|
||||||
group.OrganizationId = organization.Id;
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IGroupRepository>()
|
|
||||||
.GetManyUserIdsByIdAsync(group.Id, true)
|
|
||||||
.Returns(existingMembers);
|
|
||||||
|
|
||||||
var scimPatchModel = new ScimPatchModel
|
|
||||||
{
|
|
||||||
Operations = new List<ScimPatchModel.OperationModel>
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Op = "add",
|
|
||||||
Path = $"members[value eq \"{userId}\"]",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
|
||||||
};
|
|
||||||
|
|
||||||
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IGroupRepository>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.AddGroupUsersByIdAsync(default, default);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData]
|
|
||||||
public async Task PatchGroup_AddListMembers_Success(SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group, ICollection<Guid> existingMembers, ICollection<Guid> userIds)
|
|
||||||
{
|
|
||||||
group.OrganizationId = organization.Id;
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IGroupRepository>()
|
|
||||||
.GetManyUserIdsByIdAsync(group.Id, true)
|
|
||||||
.Returns(existingMembers);
|
|
||||||
|
|
||||||
var scimPatchModel = new ScimPatchModel
|
|
||||||
{
|
|
||||||
Operations = new List<ScimPatchModel.OperationModel>
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Op = "add",
|
|
||||||
Path = $"members",
|
|
||||||
Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
|
||||||
};
|
|
||||||
|
|
||||||
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByIdAsync(
|
|
||||||
group.Id,
|
|
||||||
Arg.Is<IEnumerable<Guid>>(arg =>
|
|
||||||
arg.Count() == userIds.Count &&
|
|
||||||
arg.ToHashSet().SetEquals(userIds)));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData]
|
|
||||||
public async Task PatchGroup_AddListMembers_IgnoresDuplicatesInRequest(
|
|
||||||
SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group,
|
|
||||||
ICollection<Guid> existingMembers)
|
|
||||||
{
|
|
||||||
// Create 3 userIds
|
|
||||||
var fixture = new Fixture { RepeatCount = 3 };
|
|
||||||
var userIds = fixture.CreateMany<Guid>().ToList();
|
|
||||||
|
|
||||||
// Copy the list and add a duplicate
|
|
||||||
var userIdsWithDuplicate = userIds.Append(userIds.First()).ToList();
|
|
||||||
Assert.Equal(4, userIdsWithDuplicate.Count);
|
|
||||||
|
|
||||||
group.OrganizationId = organization.Id;
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IGroupRepository>()
|
|
||||||
.GetManyUserIdsByIdAsync(group.Id, true)
|
|
||||||
.Returns(existingMembers);
|
|
||||||
|
|
||||||
var scimPatchModel = new ScimPatchModel
|
|
||||||
{
|
|
||||||
Operations = new List<ScimPatchModel.OperationModel>
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Op = "add",
|
|
||||||
Path = $"members",
|
|
||||||
Value = JsonDocument.Parse(JsonSerializer
|
|
||||||
.Serialize(userIdsWithDuplicate
|
|
||||||
.Select(uid => new { value = uid })
|
|
||||||
.ToArray())).RootElement
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
|
||||||
};
|
|
||||||
|
|
||||||
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IGroupRepository>().Received(1).AddGroupUsersByIdAsync(
|
|
||||||
group.Id,
|
|
||||||
Arg.Is<IEnumerable<Guid>>(arg =>
|
|
||||||
arg.Count() == 3 &&
|
|
||||||
arg.ToHashSet().SetEquals(userIds)));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData]
|
|
||||||
public async Task PatchGroup_AddListMembers_SuccessIfOnlySomeUsersAreInGroup(
|
|
||||||
SutProvider<PatchGroupCommandvNext> sutProvider,
|
|
||||||
Organization organization, Group group,
|
|
||||||
ICollection<Guid> existingMembers,
|
|
||||||
ICollection<Guid> userIds)
|
|
||||||
{
|
|
||||||
// A user is already in the group, but some still need to be added
|
|
||||||
userIds.Add(existingMembers.First());
|
|
||||||
|
|
||||||
group.OrganizationId = organization.Id;
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IGroupRepository>()
|
|
||||||
.GetManyUserIdsByIdAsync(group.Id, true)
|
|
||||||
.Returns(existingMembers);
|
|
||||||
|
|
||||||
var scimPatchModel = new ScimPatchModel
|
|
||||||
{
|
|
||||||
Operations = new List<ScimPatchModel.OperationModel>
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Op = "add",
|
|
||||||
Path = $"members",
|
|
||||||
Value = JsonDocument.Parse(JsonSerializer.Serialize(userIds.Select(uid => new { value = uid }).ToArray())).RootElement
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
|
||||||
};
|
|
||||||
|
|
||||||
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IGroupRepository>()
|
|
||||||
.Received(1)
|
|
||||||
.AddGroupUsersByIdAsync(
|
|
||||||
group.Id,
|
|
||||||
Arg.Is<IEnumerable<Guid>>(arg =>
|
|
||||||
arg.Count() == userIds.Count &&
|
|
||||||
arg.ToHashSet().SetEquals(userIds)));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData]
|
|
||||||
public async Task PatchGroup_RemoveSingleMember_Success(SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group, Guid userId)
|
|
||||||
{
|
|
||||||
group.OrganizationId = organization.Id;
|
|
||||||
|
|
||||||
var scimPatchModel = new Models.ScimPatchModel
|
|
||||||
{
|
|
||||||
Operations = new List<ScimPatchModel.OperationModel>
|
|
||||||
{
|
|
||||||
new ScimPatchModel.OperationModel
|
|
||||||
{
|
|
||||||
Op = "remove",
|
|
||||||
Path = $"members[value eq \"{userId}\"]",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
|
||||||
};
|
|
||||||
|
|
||||||
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IGroupService>().Received(1).DeleteUserAsync(group, userId, EventSystemUser.SCIM);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData]
|
|
||||||
public async Task PatchGroup_RemoveListMembers_Success(SutProvider<PatchGroupCommandvNext> sutProvider,
|
|
||||||
Organization organization, Group group, ICollection<Guid> existingMembers)
|
|
||||||
{
|
|
||||||
List<Guid> usersToRemove = [existingMembers.First(), existingMembers.Skip(1).First()];
|
|
||||||
group.OrganizationId = organization.Id;
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IGroupRepository>()
|
|
||||||
.GetManyUserIdsByIdAsync(group.Id)
|
|
||||||
.Returns(existingMembers);
|
|
||||||
|
|
||||||
var scimPatchModel = new Models.ScimPatchModel
|
|
||||||
{
|
|
||||||
Operations = new List<ScimPatchModel.OperationModel>
|
|
||||||
{
|
|
||||||
new()
|
|
||||||
{
|
|
||||||
Op = "remove",
|
|
||||||
Path = $"members",
|
|
||||||
Value = JsonDocument.Parse(JsonSerializer.Serialize(usersToRemove.Select(uid => new { value = uid }).ToArray())).RootElement
|
|
||||||
}
|
|
||||||
},
|
|
||||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
|
||||||
};
|
|
||||||
|
|
||||||
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
|
||||||
|
|
||||||
var expectedRemainingUsers = existingMembers.Skip(2).ToList();
|
|
||||||
await sutProvider.GetDependency<IGroupRepository>()
|
|
||||||
.Received(1)
|
|
||||||
.UpdateUsersAsync(
|
|
||||||
group.Id,
|
|
||||||
Arg.Is<IEnumerable<Guid>>(arg =>
|
|
||||||
arg.Count() == expectedRemainingUsers.Count &&
|
|
||||||
arg.ToHashSet().SetEquals(expectedRemainingUsers)));
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData]
|
|
||||||
public async Task PatchGroup_NoAction_Success(
|
|
||||||
SutProvider<PatchGroupCommandvNext> sutProvider, Organization organization, Group group)
|
|
||||||
{
|
|
||||||
group.OrganizationId = organization.Id;
|
|
||||||
|
|
||||||
var scimPatchModel = new Models.ScimPatchModel
|
|
||||||
{
|
|
||||||
Operations = new List<ScimPatchModel.OperationModel>(),
|
|
||||||
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
|
|
||||||
};
|
|
||||||
|
|
||||||
await sutProvider.Sut.PatchGroupAsync(group, scimPatchModel);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().UpdateUsersAsync(default, default);
|
|
||||||
await sutProvider.GetDependency<IGroupRepository>().DidNotReceiveWithAnyArgs().GetManyUserIdsByIdAsync(default);
|
|
||||||
await sutProvider.GetDependency<IUpdateGroupCommand>().DidNotReceiveWithAnyArgs().UpdateGroupAsync(default, default);
|
|
||||||
await sutProvider.GetDependency<IGroupService>().DidNotReceiveWithAnyArgs().DeleteUserAsync(default, default);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
@ -43,7 +44,7 @@ public class PatchUserCommandTests
|
|||||||
|
|
||||||
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
|
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationService>().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM);
|
await sutProvider.GetDependency<IRestoreOrganizationUserCommand>().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@ -71,7 +72,7 @@ public class PatchUserCommandTests
|
|||||||
|
|
||||||
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
|
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationService>().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM);
|
await sutProvider.GetDependency<IRestoreOrganizationUserCommand>().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
@ -147,7 +148,7 @@ public class PatchUserCommandTests
|
|||||||
|
|
||||||
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
|
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs().RestoreUserAsync(default, EventSystemUser.SCIM);
|
await sutProvider.GetDependency<IRestoreOrganizationUserCommand>().DidNotReceiveWithAnyArgs().RestoreUserAsync(default, EventSystemUser.SCIM);
|
||||||
await sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs().RevokeUserAsync(default, EventSystemUser.SCIM);
|
await sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs().RevokeUserAsync(default, EventSystemUser.SCIM);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
90
perf/load/sync.js
Normal file
90
perf/load/sync.js
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
import http from "k6/http";
|
||||||
|
import { check, fail } from "k6";
|
||||||
|
import { authenticate } from "./helpers/auth.js";
|
||||||
|
|
||||||
|
const IDENTITY_URL = __ENV.IDENTITY_URL;
|
||||||
|
const API_URL = __ENV.API_URL;
|
||||||
|
const CLIENT_ID = __ENV.CLIENT_ID;
|
||||||
|
const AUTH_USERNAME = __ENV.AUTH_USER_EMAIL;
|
||||||
|
const AUTH_PASSWORD = __ENV.AUTH_USER_PASSWORD_HASH;
|
||||||
|
|
||||||
|
export const options = {
|
||||||
|
ext: {
|
||||||
|
loadimpact: {
|
||||||
|
projectID: 3639465,
|
||||||
|
name: "Sync",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scenarios: {
|
||||||
|
constant_load: {
|
||||||
|
executor: "constant-arrival-rate",
|
||||||
|
rate: 30,
|
||||||
|
timeUnit: "1m", // 0.5 requests / second
|
||||||
|
duration: "10m",
|
||||||
|
preAllocatedVUs: 5,
|
||||||
|
},
|
||||||
|
ramping_load: {
|
||||||
|
executor: "ramping-arrival-rate",
|
||||||
|
startRate: 30,
|
||||||
|
timeUnit: "1m", // 0.5 requests / second to start
|
||||||
|
stages: [
|
||||||
|
{ duration: "30s", target: 30 },
|
||||||
|
{ duration: "2m", target: 75 },
|
||||||
|
{ duration: "1m", target: 60 },
|
||||||
|
{ duration: "2m", target: 100 },
|
||||||
|
{ duration: "2m", target: 90 },
|
||||||
|
{ duration: "1m", target: 120 },
|
||||||
|
{ duration: "30s", target: 150 },
|
||||||
|
{ duration: "30s", target: 60 },
|
||||||
|
{ duration: "30s", target: 0 },
|
||||||
|
],
|
||||||
|
preAllocatedVUs: 20,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
thresholds: {
|
||||||
|
http_req_failed: ["rate<0.01"],
|
||||||
|
http_req_duration: ["p(95)<1200"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function setup() {
|
||||||
|
return authenticate(IDENTITY_URL, CLIENT_ID, AUTH_USERNAME, AUTH_PASSWORD);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function (data) {
|
||||||
|
const params = {
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${data.access_token}`,
|
||||||
|
"X-ClientId": CLIENT_ID,
|
||||||
|
},
|
||||||
|
tags: { name: "Sync" },
|
||||||
|
};
|
||||||
|
|
||||||
|
const excludeDomains = Math.random() > 0.5;
|
||||||
|
|
||||||
|
const syncRes = http.get(`${API_URL}/sync?excludeDomains=${excludeDomains}`, params);
|
||||||
|
if (
|
||||||
|
!check(syncRes, {
|
||||||
|
"sync status is 200": (r) => r.status === 200,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
console.error(`Sync failed with status ${syncRes.status}: ${syncRes.body}`);
|
||||||
|
fail("sync status code was *not* 200");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (syncRes.status === 200) {
|
||||||
|
const syncJson = syncRes.json();
|
||||||
|
|
||||||
|
check(syncJson, {
|
||||||
|
"sync response has profile": (j) => j.profile !== undefined,
|
||||||
|
"sync response has folders": (j) => Array.isArray(j.folders),
|
||||||
|
"sync response has collections": (j) => Array.isArray(j.collections),
|
||||||
|
"sync response has ciphers": (j) => Array.isArray(j.ciphers),
|
||||||
|
"sync response has policies": (j) => Array.isArray(j.policies),
|
||||||
|
"sync response has sends": (j) => Array.isArray(j.sends),
|
||||||
|
"sync response has correct object type": (j) => j.object === "sync"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
@ -8,6 +8,7 @@ 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.Authorization;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
|
using Bit.Core.AdminConsole.OrganizationFeatures.Shared.Authorization;
|
||||||
@ -61,6 +62,7 @@ public class OrganizationUsersController : Controller
|
|||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly IPricingClient _pricingClient;
|
private readonly IPricingClient _pricingClient;
|
||||||
private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand;
|
private readonly IConfirmOrganizationUserCommand _confirmOrganizationUserCommand;
|
||||||
|
private readonly IRestoreOrganizationUserCommand _restoreOrganizationUserCommand;
|
||||||
|
|
||||||
public OrganizationUsersController(
|
public OrganizationUsersController(
|
||||||
IOrganizationRepository organizationRepository,
|
IOrganizationRepository organizationRepository,
|
||||||
@ -86,7 +88,8 @@ public class OrganizationUsersController : Controller
|
|||||||
IPolicyRequirementQuery policyRequirementQuery,
|
IPolicyRequirementQuery policyRequirementQuery,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
IPricingClient pricingClient,
|
IPricingClient pricingClient,
|
||||||
IConfirmOrganizationUserCommand confirmOrganizationUserCommand)
|
IConfirmOrganizationUserCommand confirmOrganizationUserCommand,
|
||||||
|
IRestoreOrganizationUserCommand restoreOrganizationUserCommand)
|
||||||
{
|
{
|
||||||
_organizationRepository = organizationRepository;
|
_organizationRepository = organizationRepository;
|
||||||
_organizationUserRepository = organizationUserRepository;
|
_organizationUserRepository = organizationUserRepository;
|
||||||
@ -112,6 +115,7 @@ public class OrganizationUsersController : Controller
|
|||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
_pricingClient = pricingClient;
|
_pricingClient = pricingClient;
|
||||||
_confirmOrganizationUserCommand = confirmOrganizationUserCommand;
|
_confirmOrganizationUserCommand = confirmOrganizationUserCommand;
|
||||||
|
_restoreOrganizationUserCommand = restoreOrganizationUserCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet("{id}")]
|
[HttpGet("{id}")]
|
||||||
@ -630,14 +634,14 @@ public class OrganizationUsersController : Controller
|
|||||||
[HttpPut("{id}/restore")]
|
[HttpPut("{id}/restore")]
|
||||||
public async Task RestoreAsync(Guid orgId, Guid id)
|
public async Task RestoreAsync(Guid orgId, Guid id)
|
||||||
{
|
{
|
||||||
await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _organizationService.RestoreUserAsync(orgUser, userId));
|
await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _restoreOrganizationUserCommand.RestoreUserAsync(orgUser, userId));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPatch("restore")]
|
[HttpPatch("restore")]
|
||||||
[HttpPut("restore")]
|
[HttpPut("restore")]
|
||||||
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRestoreAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRestoreAsync(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
|
||||||
{
|
{
|
||||||
return await RestoreOrRevokeUsersAsync(orgId, model, (orgId, orgUserIds, restoringUserId) => _organizationService.RestoreUsersAsync(orgId, orgUserIds, restoringUserId, _userService));
|
return await RestoreOrRevokeUsersAsync(orgId, model, (orgId, orgUserIds, restoringUserId) => _restoreOrganizationUserCommand.RestoreUsersAsync(orgId, orgUserIds, restoringUserId, _userService));
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPatch("enable-secrets-manager")]
|
[HttpPatch("enable-secrets-manager")]
|
||||||
|
@ -13,7 +13,17 @@ public static class PolicyDetailResponses
|
|||||||
{
|
{
|
||||||
throw new ArgumentException($"'{nameof(policy)}' must be of type '{nameof(PolicyType.SingleOrg)}'.", nameof(policy));
|
throw new ArgumentException($"'{nameof(policy)}' must be of type '{nameof(PolicyType.SingleOrg)}'.", nameof(policy));
|
||||||
}
|
}
|
||||||
|
return new PolicyDetailResponseModel(policy, await CanToggleState());
|
||||||
|
|
||||||
return new PolicyDetailResponseModel(policy, !await hasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policy.OrganizationId));
|
async Task<bool> CanToggleState()
|
||||||
|
{
|
||||||
|
if (!await hasVerifiedDomainsQuery.HasVerifiedDomainsAsync(policy.OrganizationId))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !policy.Enabled;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -76,6 +76,13 @@ public class OrganizationSponsorshipsController : Controller
|
|||||||
public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] OrganizationSponsorshipCreateRequestModel model)
|
public async Task CreateSponsorship(Guid sponsoringOrgId, [FromBody] OrganizationSponsorshipCreateRequestModel model)
|
||||||
{
|
{
|
||||||
var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgId);
|
var sponsoringOrg = await _organizationRepository.GetByIdAsync(sponsoringOrgId);
|
||||||
|
var freeFamiliesSponsorshipPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(sponsoringOrgId,
|
||||||
|
PolicyType.FreeFamiliesSponsorshipPolicy);
|
||||||
|
|
||||||
|
if (freeFamiliesSponsorshipPolicy?.Enabled == true)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Free Bitwarden Families sponsorship has been disabled by your organization administrator.");
|
||||||
|
}
|
||||||
|
|
||||||
var sponsorship = await _createSponsorshipCommand.CreateSponsorshipAsync(
|
var sponsorship = await _createSponsorshipCommand.CreateSponsorshipAsync(
|
||||||
sponsoringOrg,
|
sponsoringOrg,
|
||||||
@ -89,6 +96,14 @@ public class OrganizationSponsorshipsController : Controller
|
|||||||
[SelfHosted(NotSelfHostedOnly = true)]
|
[SelfHosted(NotSelfHostedOnly = true)]
|
||||||
public async Task ResendSponsorshipOffer(Guid sponsoringOrgId)
|
public async Task ResendSponsorshipOffer(Guid sponsoringOrgId)
|
||||||
{
|
{
|
||||||
|
var freeFamiliesSponsorshipPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(sponsoringOrgId,
|
||||||
|
PolicyType.FreeFamiliesSponsorshipPolicy);
|
||||||
|
|
||||||
|
if (freeFamiliesSponsorshipPolicy?.Enabled == true)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Free Bitwarden Families sponsorship has been disabled by your organization administrator.");
|
||||||
|
}
|
||||||
|
|
||||||
var sponsoringOrgUser = await _organizationUserRepository
|
var sponsoringOrgUser = await _organizationUserRepository
|
||||||
.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default);
|
.GetByOrganizationAsync(sponsoringOrgId, _currentContext.UserId ?? default);
|
||||||
|
|
||||||
@ -135,6 +150,14 @@ public class OrganizationSponsorshipsController : Controller
|
|||||||
throw new BadRequestException("Can only redeem sponsorship for an organization you own.");
|
throw new BadRequestException("Can only redeem sponsorship for an organization you own.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var freeFamiliesSponsorshipPolicy = await _policyRepository.GetByOrganizationIdTypeAsync(
|
||||||
|
model.SponsoredOrganizationId, PolicyType.FreeFamiliesSponsorshipPolicy);
|
||||||
|
|
||||||
|
if (freeFamiliesSponsorshipPolicy?.Enabled == true)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Free Bitwarden Families sponsorship has been disabled by your organization administrator.");
|
||||||
|
}
|
||||||
|
|
||||||
await _setUpSponsorshipCommand.SetUpSponsorshipAsync(
|
await _setUpSponsorshipCommand.SetUpSponsorshipAsync(
|
||||||
sponsorship,
|
sponsorship,
|
||||||
await _organizationRepository.GetByIdAsync(model.SponsoredOrganizationId));
|
await _organizationRepository.GetByIdAsync(model.SponsoredOrganizationId));
|
||||||
|
@ -5,6 +5,7 @@ using Bit.Core;
|
|||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
using Bit.Core.Vault.Commands.Interfaces;
|
using Bit.Core.Vault.Commands.Interfaces;
|
||||||
|
using Bit.Core.Vault.Entities;
|
||||||
using Bit.Core.Vault.Enums;
|
using Bit.Core.Vault.Enums;
|
||||||
using Bit.Core.Vault.Queries;
|
using Bit.Core.Vault.Queries;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
@ -89,11 +90,28 @@ public class SecurityTaskController : Controller
|
|||||||
public async Task<ListResponseModel<SecurityTasksResponseModel>> BulkCreateTasks(Guid orgId,
|
public async Task<ListResponseModel<SecurityTasksResponseModel>> BulkCreateTasks(Guid orgId,
|
||||||
[FromBody] BulkCreateSecurityTasksRequestModel model)
|
[FromBody] BulkCreateSecurityTasksRequestModel model)
|
||||||
{
|
{
|
||||||
var securityTasks = await _createManyTasksCommand.CreateAsync(orgId, model.Tasks);
|
// Retrieve existing pending security tasks for the organization
|
||||||
|
var pendingSecurityTasks = await _getTasksForOrganizationQuery.GetTasksAsync(orgId, SecurityTaskStatus.Pending);
|
||||||
|
|
||||||
await _createManyTaskNotificationsCommand.CreateAsync(orgId, securityTasks);
|
// Get the security tasks that are already associated with a cipher within the submitted model
|
||||||
|
var existingTasks = pendingSecurityTasks.Where(x => model.Tasks.Any(y => y.CipherId == x.CipherId)).ToList();
|
||||||
|
|
||||||
var response = securityTasks.Select(x => new SecurityTasksResponseModel(x)).ToList();
|
// Get tasks that need to be created
|
||||||
|
var tasksToCreateFromModel = model.Tasks.Where(x => !existingTasks.Any(y => y.CipherId == x.CipherId)).ToList();
|
||||||
|
|
||||||
|
ICollection<SecurityTask> newSecurityTasks = new List<SecurityTask>();
|
||||||
|
|
||||||
|
if (tasksToCreateFromModel.Count != 0)
|
||||||
|
{
|
||||||
|
newSecurityTasks = await _createManyTasksCommand.CreateAsync(orgId, tasksToCreateFromModel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine existing tasks and newly created tasks
|
||||||
|
var allTasks = existingTasks.Concat(newSecurityTasks);
|
||||||
|
|
||||||
|
await _createManyTaskNotificationsCommand.CreateAsync(orgId, allTasks);
|
||||||
|
|
||||||
|
var response = allTasks.Select(x => new SecurityTasksResponseModel(x)).ToList();
|
||||||
return new ListResponseModel<SecurityTasksResponseModel>(response);
|
return new ListResponseModel<SecurityTasksResponseModel>(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
18
src/Core/AdminConsole/Entities/OrganizationIntegration.cs
Normal file
18
src/Core/AdminConsole/Entities/OrganizationIntegration.cs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.Entities;
|
||||||
|
|
||||||
|
public class OrganizationIntegration : ITableObject<Guid>
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid OrganizationId { get; set; }
|
||||||
|
public IntegrationType Type { get; set; }
|
||||||
|
public string? Configuration { get; set; }
|
||||||
|
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
|
||||||
|
public void SetNewId() => Id = CoreHelpers.GenerateComb();
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Utilities;
|
||||||
|
|
||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.Entities;
|
||||||
|
|
||||||
|
public class OrganizationIntegrationConfiguration : ITableObject<Guid>
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
public Guid OrganizationIntegrationId { get; set; }
|
||||||
|
public EventType EventType { get; set; }
|
||||||
|
public string? Configuration { get; set; }
|
||||||
|
public string? Template { get; set; }
|
||||||
|
public DateTime CreationDate { get; set; } = DateTime.UtcNow;
|
||||||
|
public DateTime RevisionDate { get; set; } = DateTime.UtcNow;
|
||||||
|
public void SetNewId() => Id = CoreHelpers.GenerateComb();
|
||||||
|
}
|
7
src/Core/AdminConsole/Enums/IntegrationType.cs
Normal file
7
src/Core/AdminConsole/Enums/IntegrationType.cs
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
namespace Bit.Core.Enums;
|
||||||
|
|
||||||
|
public enum IntegrationType : int
|
||||||
|
{
|
||||||
|
Slack = 1,
|
||||||
|
Webhook = 2,
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Restores a user back to their previous status.
|
||||||
|
/// </summary>
|
||||||
|
public interface IRestoreOrganizationUserCommand
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validates that the requesting user can perform the action. There is also a check done to ensure the organization
|
||||||
|
/// can re-add this user based on their current occupied seats.
|
||||||
|
///
|
||||||
|
/// Checks are performed to make sure the user is conforming to all policies enforced by the organization as well as
|
||||||
|
/// other organizations the user may belong to.
|
||||||
|
///
|
||||||
|
/// Reference Events and Push Notifications are fired off for this as well.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organizationUser">Revoked user to be restored.</param>
|
||||||
|
/// <param name="restoringUserId">UserId of the user performing the action.</param>
|
||||||
|
Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates that the requesting user can perform the action. There is also a check done to ensure the organization
|
||||||
|
/// can re-add this user based on their current occupied seats.
|
||||||
|
///
|
||||||
|
/// Checks are performed to make sure the user is conforming to all policies enforced by the organization as well as
|
||||||
|
/// other organizations the user may belong to.
|
||||||
|
///
|
||||||
|
/// Reference Events and Push Notifications are fired off for this as well.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organizationUser">Revoked user to be restored.</param>
|
||||||
|
/// <param name="systemUser">System that is performing the action on behalf of the organization (Public API, SCIM, etc.)</param>
|
||||||
|
Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates that the requesting user can perform the action. There is also a check done to ensure the organization
|
||||||
|
/// can re-add this user based on their current occupied seats.
|
||||||
|
///
|
||||||
|
/// Checks are performed to make sure the user is conforming to all policies enforced by the organization as well as
|
||||||
|
/// other organizations the user may belong to.
|
||||||
|
///
|
||||||
|
/// Reference Events and Push Notifications are fired off for this as well.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="organizationId">Organization the users should be restored to.</param>
|
||||||
|
/// <param name="organizationUserIds">List of organization user ids to restore to previous status.</param>
|
||||||
|
/// <param name="restoringUserId">UserId of the user performing the action.</param>
|
||||||
|
/// <param name="userService">Passed in from caller to avoid circular dependency</param>
|
||||||
|
/// <returns>List of organization user Ids and strings. A successful restoration will have an empty string.
|
||||||
|
/// If an error occurs, the error message will be provided.</returns>
|
||||||
|
Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId, IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService);
|
||||||
|
}
|
@ -0,0 +1,295 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Services;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Platform.Push;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||||
|
|
||||||
|
public class RestoreOrganizationUserCommand(
|
||||||
|
ICurrentContext currentContext,
|
||||||
|
IEventService eventService,
|
||||||
|
IFeatureService featureService,
|
||||||
|
IPushNotificationService pushNotificationService,
|
||||||
|
IOrganizationUserRepository organizationUserRepository,
|
||||||
|
IOrganizationRepository organizationRepository,
|
||||||
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
|
IPolicyService policyService,
|
||||||
|
IUserRepository userRepository,
|
||||||
|
IOrganizationService organizationService) : IRestoreOrganizationUserCommand
|
||||||
|
{
|
||||||
|
public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId)
|
||||||
|
{
|
||||||
|
if (restoringUserId.HasValue && organizationUser.UserId == restoringUserId.Value)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You cannot restore yourself.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organizationUser.Type == OrganizationUserType.Owner && restoringUserId.HasValue &&
|
||||||
|
!await currentContext.OrganizationOwner(organizationUser.OrganizationId))
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Only owners can restore other owners.");
|
||||||
|
}
|
||||||
|
|
||||||
|
await RepositoryRestoreUserAsync(organizationUser);
|
||||||
|
await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||||
|
|
||||||
|
if (featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) &&
|
||||||
|
organizationUser.UserId.HasValue)
|
||||||
|
{
|
||||||
|
await pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser)
|
||||||
|
{
|
||||||
|
await RepositoryRestoreUserAsync(organizationUser);
|
||||||
|
await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored,
|
||||||
|
systemUser);
|
||||||
|
|
||||||
|
if (featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) &&
|
||||||
|
organizationUser.UserId.HasValue)
|
||||||
|
{
|
||||||
|
await pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RepositoryRestoreUserAsync(OrganizationUser organizationUser)
|
||||||
|
{
|
||||||
|
if (organizationUser.Status != OrganizationUserStatusType.Revoked)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Already active.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var organization = await organizationRepository.GetByIdAsync(organizationUser.OrganizationId);
|
||||||
|
var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||||
|
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
|
||||||
|
|
||||||
|
if (availableSeats < 1)
|
||||||
|
{
|
||||||
|
await organizationService.AutoAddSeatsAsync(organization, 1); // Hooray
|
||||||
|
}
|
||||||
|
|
||||||
|
var userTwoFactorIsEnabled = false;
|
||||||
|
// Only check 2FA status if the user is linked to a user account
|
||||||
|
if (organizationUser.UserId.HasValue)
|
||||||
|
{
|
||||||
|
userTwoFactorIsEnabled =
|
||||||
|
(await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync([organizationUser.UserId.Value]))
|
||||||
|
.FirstOrDefault()
|
||||||
|
.twoFactorIsEnabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
await CheckUserForOtherFreeOrganizationOwnershipAsync(organizationUser);
|
||||||
|
|
||||||
|
await CheckPoliciesBeforeRestoreAsync(organizationUser, userTwoFactorIsEnabled);
|
||||||
|
|
||||||
|
var status = OrganizationService.GetPriorActiveOrganizationUserStatusType(organizationUser);
|
||||||
|
|
||||||
|
await organizationUserRepository.RestoreAsync(organizationUser.Id, status);
|
||||||
|
|
||||||
|
organizationUser.Status = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CheckUserForOtherFreeOrganizationOwnershipAsync(OrganizationUser organizationUser)
|
||||||
|
{
|
||||||
|
var relatedOrgUsersFromOtherOrgs = await organizationUserRepository.GetManyByUserAsync(organizationUser.UserId.Value);
|
||||||
|
var otherOrgs = await organizationRepository.GetManyByUserIdAsync(organizationUser.UserId.Value);
|
||||||
|
|
||||||
|
var orgOrgUserDict = relatedOrgUsersFromOtherOrgs
|
||||||
|
.Where(x => x.Id != organizationUser.Id)
|
||||||
|
.ToDictionary(x => x, x => otherOrgs.FirstOrDefault(y => y.Id == x.OrganizationId));
|
||||||
|
|
||||||
|
CheckForOtherFreeOrganizationOwnership(organizationUser, orgOrgUserDict);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<Dictionary<OrganizationUser, Organization>> GetRelatedOrganizationUsersAndOrganizations(
|
||||||
|
IEnumerable<OrganizationUser> organizationUsers)
|
||||||
|
{
|
||||||
|
var allUserIds = organizationUsers.Select(x => x.UserId.Value);
|
||||||
|
|
||||||
|
var otherOrganizationUsers = (await organizationUserRepository.GetManyByManyUsersAsync(allUserIds))
|
||||||
|
.Where(x => organizationUsers.Any(y => y.Id == x.Id) == false);
|
||||||
|
|
||||||
|
var otherOrgs = await organizationRepository.GetManyByIdsAsync(otherOrganizationUsers
|
||||||
|
.Select(x => x.OrganizationId)
|
||||||
|
.Distinct());
|
||||||
|
|
||||||
|
return otherOrganizationUsers
|
||||||
|
.ToDictionary(x => x, x => otherOrgs.FirstOrDefault(y => y.Id == x.OrganizationId));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void CheckForOtherFreeOrganizationOwnership(OrganizationUser organizationUser,
|
||||||
|
Dictionary<OrganizationUser, Organization> otherOrgUsersAndOrgs)
|
||||||
|
{
|
||||||
|
var ownerOrAdminList = new[] { OrganizationUserType.Owner, OrganizationUserType.Admin };
|
||||||
|
if (otherOrgUsersAndOrgs.Any(x =>
|
||||||
|
x.Key.UserId == organizationUser.UserId &&
|
||||||
|
ownerOrAdminList.Any(userType => userType == x.Key.Type) &&
|
||||||
|
x.Key.Status == OrganizationUserStatusType.Confirmed &&
|
||||||
|
x.Value.PlanType == PlanType.Free))
|
||||||
|
{
|
||||||
|
throw new BadRequestException(
|
||||||
|
"User is an owner/admin of another free organization. Please have them upgrade to a paid plan to restore their account.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,
|
||||||
|
IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService)
|
||||||
|
{
|
||||||
|
var orgUsers = await organizationUserRepository.GetManyAsync(organizationUserIds);
|
||||||
|
var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (filteredUsers.Count == 0)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Users invalid.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var organization = await organizationRepository.GetByIdAsync(organizationId);
|
||||||
|
var occupiedSeats = await organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
||||||
|
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
|
||||||
|
var newSeatsRequired = organizationUserIds.Count() - availableSeats;
|
||||||
|
await organizationService.AutoAddSeatsAsync(organization, newSeatsRequired);
|
||||||
|
|
||||||
|
var deletingUserIsOwner = false;
|
||||||
|
if (restoringUserId.HasValue)
|
||||||
|
{
|
||||||
|
deletingUserIsOwner = await currentContext.OrganizationOwner(organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query Two Factor Authentication status for all users in the organization
|
||||||
|
// This is an optimization to avoid querying the Two Factor Authentication status for each user individually
|
||||||
|
var organizationUsersTwoFactorEnabled = await twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(
|
||||||
|
filteredUsers.Where(ou => ou.UserId.HasValue).Select(ou => ou.UserId.Value));
|
||||||
|
|
||||||
|
var orgUsersAndOrgs = await GetRelatedOrganizationUsersAndOrganizations(filteredUsers);
|
||||||
|
|
||||||
|
var result = new List<Tuple<OrganizationUser, string>>();
|
||||||
|
|
||||||
|
foreach (var organizationUser in filteredUsers)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (organizationUser.Status != OrganizationUserStatusType.Revoked)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Already active.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (restoringUserId.HasValue && organizationUser.UserId == restoringUserId)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("You cannot restore yourself.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (organizationUser.Type == OrganizationUserType.Owner && restoringUserId.HasValue &&
|
||||||
|
!deletingUserIsOwner)
|
||||||
|
{
|
||||||
|
throw new BadRequestException("Only owners can restore other owners.");
|
||||||
|
}
|
||||||
|
|
||||||
|
var twoFactorIsEnabled = organizationUser.UserId.HasValue
|
||||||
|
&& organizationUsersTwoFactorEnabled
|
||||||
|
.FirstOrDefault(ou => ou.userId == organizationUser.UserId.Value)
|
||||||
|
.twoFactorIsEnabled;
|
||||||
|
|
||||||
|
await CheckPoliciesBeforeRestoreAsync(organizationUser, twoFactorIsEnabled);
|
||||||
|
|
||||||
|
CheckForOtherFreeOrganizationOwnership(organizationUser, orgUsersAndOrgs);
|
||||||
|
|
||||||
|
var status = OrganizationService.GetPriorActiveOrganizationUserStatusType(organizationUser);
|
||||||
|
|
||||||
|
await organizationUserRepository.RestoreAsync(organizationUser.Id, status);
|
||||||
|
organizationUser.Status = status;
|
||||||
|
await eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||||
|
if (featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) &&
|
||||||
|
organizationUser.UserId.HasValue)
|
||||||
|
{
|
||||||
|
await pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add(Tuple.Create(organizationUser, ""));
|
||||||
|
}
|
||||||
|
catch (BadRequestException e)
|
||||||
|
{
|
||||||
|
result.Add(Tuple.Create(organizationUser, e.Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CheckPoliciesBeforeRestoreAsync(OrganizationUser orgUser, bool userHasTwoFactorEnabled)
|
||||||
|
{
|
||||||
|
// An invited OrganizationUser isn't linked with a user account yet, so these checks are irrelevant
|
||||||
|
// The user will be subject to the same checks when they try to accept the invite
|
||||||
|
if (OrganizationService.GetPriorActiveOrganizationUserStatusType(orgUser) == OrganizationUserStatusType.Invited)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var userId = orgUser.UserId.Value;
|
||||||
|
|
||||||
|
// Enforce Single Organization Policy of organization user is being restored to
|
||||||
|
var allOrgUsers = await organizationUserRepository.GetManyByUserAsync(userId);
|
||||||
|
var hasOtherOrgs = allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId);
|
||||||
|
var singleOrgPoliciesApplyingToRevokedUsers = await policyService.GetPoliciesApplicableToUserAsync(userId,
|
||||||
|
PolicyType.SingleOrg, OrganizationUserStatusType.Revoked);
|
||||||
|
var singleOrgPolicyApplies =
|
||||||
|
singleOrgPoliciesApplyingToRevokedUsers.Any(p => p.OrganizationId == orgUser.OrganizationId);
|
||||||
|
|
||||||
|
var singleOrgCompliant = true;
|
||||||
|
var belongsToOtherOrgCompliant = true;
|
||||||
|
var twoFactorCompliant = true;
|
||||||
|
|
||||||
|
if (hasOtherOrgs && singleOrgPolicyApplies)
|
||||||
|
{
|
||||||
|
singleOrgCompliant = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce Single Organization Policy of other organizations user is a member of
|
||||||
|
var anySingleOrgPolicies = await policyService.AnyPoliciesApplicableToUserAsync(userId, PolicyType.SingleOrg);
|
||||||
|
if (anySingleOrgPolicies)
|
||||||
|
{
|
||||||
|
belongsToOtherOrgCompliant = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce 2FA Policy of organization user is trying to join
|
||||||
|
if (!userHasTwoFactorEnabled)
|
||||||
|
{
|
||||||
|
var invitedTwoFactorPolicies = await policyService.GetPoliciesApplicableToUserAsync(userId,
|
||||||
|
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Revoked);
|
||||||
|
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
|
||||||
|
{
|
||||||
|
twoFactorCompliant = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var user = await userRepository.GetByIdAsync(userId);
|
||||||
|
|
||||||
|
if (!singleOrgCompliant && !twoFactorCompliant)
|
||||||
|
{
|
||||||
|
throw new BadRequestException(user.Email +
|
||||||
|
" is not compliant with the single organization and two-step login policy");
|
||||||
|
}
|
||||||
|
else if (!singleOrgCompliant)
|
||||||
|
{
|
||||||
|
throw new BadRequestException(user.Email + " is not compliant with the single organization policy");
|
||||||
|
}
|
||||||
|
else if (!belongsToOtherOrgCompliant)
|
||||||
|
{
|
||||||
|
throw new BadRequestException(user.Email +
|
||||||
|
" belongs to an organization that doesn't allow them to join multiple organizations");
|
||||||
|
}
|
||||||
|
else if (!twoFactorCompliant)
|
||||||
|
{
|
||||||
|
throw new BadRequestException(user.Email + " is not compliant with the two-step login policy");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
|
||||||
|
namespace Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Policy requirements for the Disable Personal Ownership policy.
|
||||||
|
/// </summary>
|
||||||
|
public class PersonalOwnershipPolicyRequirement : IPolicyRequirement
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates whether Personal Ownership is disabled for the user. If true, members are required to save items to an organization.
|
||||||
|
/// </summary>
|
||||||
|
public bool DisablePersonalOwnership { get; init; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PersonalOwnershipPolicyRequirementFactory : BasePolicyRequirementFactory<PersonalOwnershipPolicyRequirement>
|
||||||
|
{
|
||||||
|
public override PolicyType PolicyType => PolicyType.PersonalOwnership;
|
||||||
|
|
||||||
|
public override PersonalOwnershipPolicyRequirement Create(IEnumerable<PolicyDetails> policyDetails)
|
||||||
|
{
|
||||||
|
var result = new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = policyDetails.Any() };
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
@ -34,5 +34,6 @@ public static class PolicyServiceCollectionExtensions
|
|||||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, DisableSendPolicyRequirementFactory>();
|
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, DisableSendPolicyRequirementFactory>();
|
||||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, SendOptionsPolicyRequirementFactory>();
|
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, SendOptionsPolicyRequirementFactory>();
|
||||||
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, ResetPasswordPolicyRequirementFactory>();
|
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, ResetPasswordPolicyRequirementFactory>();
|
||||||
|
services.AddScoped<IPolicyRequirementFactory<IPolicyRequirement>, PersonalOwnershipPolicyRequirementFactory>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,4 +24,5 @@ public interface IOrganizationRepository : IRepository<Organization, Guid>
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId);
|
Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId);
|
||||||
Task<ICollection<Organization>> GetAddableToProviderByUserIdAsync(Guid userId, ProviderType providerType);
|
Task<ICollection<Organization>> GetAddableToProviderByUserIdAsync(Guid userId, ProviderType providerType);
|
||||||
|
Task<ICollection<Organization>> GetManyByIdsAsync(IEnumerable<Guid> ids);
|
||||||
}
|
}
|
||||||
|
@ -48,10 +48,6 @@ public interface IOrganizationService
|
|||||||
Task RevokeUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser);
|
Task RevokeUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser);
|
||||||
Task<List<Tuple<OrganizationUser, string>>> RevokeUsersAsync(Guid organizationId,
|
Task<List<Tuple<OrganizationUser, string>>> RevokeUsersAsync(Guid organizationId,
|
||||||
IEnumerable<Guid> organizationUserIds, Guid? revokingUserId);
|
IEnumerable<Guid> organizationUserIds, Guid? revokingUserId);
|
||||||
Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId);
|
|
||||||
Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser);
|
|
||||||
Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,
|
|
||||||
IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService);
|
|
||||||
Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted);
|
Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted);
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Update an Organization entry by setting the public/private keys, set it as 'Enabled' and move the Status from 'Pending' to 'Created'.
|
/// Update an Organization entry by setting the public/private keys, set it as 'Enabled' and move the Status from 'Pending' to 'Created'.
|
||||||
|
@ -19,7 +19,6 @@ using Bit.Core.Billing.Constants;
|
|||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Extensions;
|
using Bit.Core.Billing.Extensions;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Billing.Services;
|
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -75,7 +74,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
|
||||||
private readonly IFeatureService _featureService;
|
private readonly IFeatureService _featureService;
|
||||||
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
|
||||||
private readonly IOrganizationBillingService _organizationBillingService;
|
|
||||||
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
|
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
|
||||||
private readonly IPricingClient _pricingClient;
|
private readonly IPricingClient _pricingClient;
|
||||||
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||||
@ -112,7 +110,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
IProviderRepository providerRepository,
|
IProviderRepository providerRepository,
|
||||||
IFeatureService featureService,
|
IFeatureService featureService,
|
||||||
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
|
||||||
IOrganizationBillingService organizationBillingService,
|
|
||||||
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery,
|
||||||
IPricingClient pricingClient,
|
IPricingClient pricingClient,
|
||||||
IPolicyRequirementQuery policyRequirementQuery)
|
IPolicyRequirementQuery policyRequirementQuery)
|
||||||
@ -148,7 +145,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
|
||||||
_featureService = featureService;
|
_featureService = featureService;
|
||||||
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
|
||||||
_organizationBillingService = organizationBillingService;
|
|
||||||
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
|
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
|
||||||
_pricingClient = pricingClient;
|
_pricingClient = pricingClient;
|
||||||
_policyRequirementQuery = policyRequirementQuery;
|
_policyRequirementQuery = policyRequirementQuery;
|
||||||
@ -579,6 +575,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
UseSecretsManager = license.UseSecretsManager,
|
UseSecretsManager = license.UseSecretsManager,
|
||||||
SmSeats = license.SmSeats,
|
SmSeats = license.SmSeats,
|
||||||
SmServiceAccounts = license.SmServiceAccounts,
|
SmServiceAccounts = license.SmServiceAccounts,
|
||||||
|
UseRiskInsights = license.UseRiskInsights,
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);
|
var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);
|
||||||
@ -1891,144 +1888,6 @@ public class OrganizationService : IOrganizationService
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId)
|
|
||||||
{
|
|
||||||
if (restoringUserId.HasValue && organizationUser.UserId == restoringUserId.Value)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("You cannot restore yourself.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (organizationUser.Type == OrganizationUserType.Owner && restoringUserId.HasValue &&
|
|
||||||
!await _currentContext.OrganizationOwner(organizationUser.OrganizationId))
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Only owners can restore other owners.");
|
|
||||||
}
|
|
||||||
|
|
||||||
await RepositoryRestoreUserAsync(organizationUser);
|
|
||||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
|
||||||
|
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) && organizationUser.UserId.HasValue)
|
|
||||||
{
|
|
||||||
await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser)
|
|
||||||
{
|
|
||||||
await RepositoryRestoreUserAsync(organizationUser);
|
|
||||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored, systemUser);
|
|
||||||
|
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) && organizationUser.UserId.HasValue)
|
|
||||||
{
|
|
||||||
await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RepositoryRestoreUserAsync(OrganizationUser organizationUser)
|
|
||||||
{
|
|
||||||
if (organizationUser.Status != OrganizationUserStatusType.Revoked)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Already active.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(organizationUser.OrganizationId);
|
|
||||||
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
|
||||||
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
|
|
||||||
if (availableSeats < 1)
|
|
||||||
{
|
|
||||||
await AutoAddSeatsAsync(organization, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
var userTwoFactorIsEnabled = false;
|
|
||||||
// Only check Two Factor Authentication status if the user is linked to a user account
|
|
||||||
if (organizationUser.UserId.HasValue)
|
|
||||||
{
|
|
||||||
userTwoFactorIsEnabled = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(new[] { organizationUser.UserId.Value })).FirstOrDefault().twoFactorIsEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
await CheckPoliciesBeforeRestoreAsync(organizationUser, userTwoFactorIsEnabled);
|
|
||||||
|
|
||||||
var status = GetPriorActiveOrganizationUserStatusType(organizationUser);
|
|
||||||
|
|
||||||
await _organizationUserRepository.RestoreAsync(organizationUser.Id, status);
|
|
||||||
organizationUser.Status = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,
|
|
||||||
IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService)
|
|
||||||
{
|
|
||||||
var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUserIds);
|
|
||||||
var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
if (!filteredUsers.Any())
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Users invalid.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(organizationId);
|
|
||||||
var occupiedSeats = await _organizationUserRepository.GetOccupiedSeatCountByOrganizationIdAsync(organization.Id);
|
|
||||||
var availableSeats = organization.Seats.GetValueOrDefault(0) - occupiedSeats;
|
|
||||||
var newSeatsRequired = organizationUserIds.Count() - availableSeats;
|
|
||||||
await AutoAddSeatsAsync(organization, newSeatsRequired);
|
|
||||||
|
|
||||||
var deletingUserIsOwner = false;
|
|
||||||
if (restoringUserId.HasValue)
|
|
||||||
{
|
|
||||||
deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query Two Factor Authentication status for all users in the organization
|
|
||||||
// This is an optimization to avoid querying the Two Factor Authentication status for each user individually
|
|
||||||
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(
|
|
||||||
filteredUsers.Where(ou => ou.UserId.HasValue).Select(ou => ou.UserId.Value));
|
|
||||||
|
|
||||||
var result = new List<Tuple<OrganizationUser, string>>();
|
|
||||||
|
|
||||||
foreach (var organizationUser in filteredUsers)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (organizationUser.Status != OrganizationUserStatusType.Revoked)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Already active.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (restoringUserId.HasValue && organizationUser.UserId == restoringUserId)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("You cannot restore yourself.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (organizationUser.Type == OrganizationUserType.Owner && restoringUserId.HasValue && !deletingUserIsOwner)
|
|
||||||
{
|
|
||||||
throw new BadRequestException("Only owners can restore other owners.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var twoFactorIsEnabled = organizationUser.UserId.HasValue
|
|
||||||
&& organizationUsersTwoFactorEnabled.FirstOrDefault(ou => ou.userId == organizationUser.UserId.Value).twoFactorIsEnabled;
|
|
||||||
await CheckPoliciesBeforeRestoreAsync(organizationUser, twoFactorIsEnabled);
|
|
||||||
|
|
||||||
var status = GetPriorActiveOrganizationUserStatusType(organizationUser);
|
|
||||||
|
|
||||||
await _organizationUserRepository.RestoreAsync(organizationUser.Id, status);
|
|
||||||
organizationUser.Status = status;
|
|
||||||
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
|
||||||
if (_featureService.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore) && organizationUser.UserId.HasValue)
|
|
||||||
{
|
|
||||||
await _pushNotificationService.PushSyncOrgKeysAsync(organizationUser.UserId.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.Add(Tuple.Create(organizationUser, ""));
|
|
||||||
}
|
|
||||||
catch (BadRequestException e)
|
|
||||||
{
|
|
||||||
result.Add(Tuple.Create(organizationUser, e.Message));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CheckPoliciesBeforeRestoreAsync(OrganizationUser orgUser, bool userHasTwoFactorEnabled)
|
private async Task CheckPoliciesBeforeRestoreAsync(OrganizationUser orgUser, bool userHasTwoFactorEnabled)
|
||||||
{
|
{
|
||||||
// An invited OrganizationUser isn't linked with a user account yet, so these checks are irrelevant
|
// An invited OrganizationUser isn't linked with a user account yet, so these checks are irrelevant
|
||||||
@ -2095,7 +1954,7 @@ public class OrganizationService : IOrganizationService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static OrganizationUserStatusType GetPriorActiveOrganizationUserStatusType(OrganizationUser organizationUser)
|
public static OrganizationUserStatusType GetPriorActiveOrganizationUserStatusType(OrganizationUser organizationUser)
|
||||||
{
|
{
|
||||||
// Determine status to revert back to
|
// Determine status to revert back to
|
||||||
var status = OrganizationUserStatusType.Invited;
|
var status = OrganizationUserStatusType.Invited;
|
||||||
|
@ -32,7 +32,7 @@ public class PremiumUserBillingService(
|
|||||||
var customer = await subscriberService.GetCustomer(user);
|
var customer = await subscriberService.GetCustomer(user);
|
||||||
|
|
||||||
// Negative credit represents a balance and all Stripe denomination is in cents.
|
// Negative credit represents a balance and all Stripe denomination is in cents.
|
||||||
var credit = (long)amount * -100;
|
var credit = (long)(amount * -100);
|
||||||
|
|
||||||
if (customer == null)
|
if (customer == null)
|
||||||
{
|
{
|
||||||
|
@ -106,9 +106,77 @@ public static class FeatureFlagKeys
|
|||||||
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
|
public const string VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint";
|
||||||
public const string DeviceApprovalRequestAdminNotifications = "pm-15637-device-approval-request-admin-notifications";
|
public const string DeviceApprovalRequestAdminNotifications = "pm-15637-device-approval-request-admin-notifications";
|
||||||
public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission";
|
public const string LimitItemDeletion = "pm-15493-restrict-item-deletion-to-can-manage-permission";
|
||||||
public const string ShortcutDuplicatePatchRequests = "pm-16812-shortcut-duplicate-patch-requests";
|
|
||||||
public const string PushSyncOrgKeysOnRevokeRestore = "pm-17168-push-sync-org-keys-on-revoke-restore";
|
public const string PushSyncOrgKeysOnRevokeRestore = "pm-17168-push-sync-org-keys-on-revoke-restore";
|
||||||
public const string PolicyRequirements = "pm-14439-policy-requirements";
|
public const string PolicyRequirements = "pm-14439-policy-requirements";
|
||||||
|
public const string SsoExternalIdVisibility = "pm-18630-sso-external-id-visibility";
|
||||||
|
|
||||||
|
/* Auth Team */
|
||||||
|
public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence";
|
||||||
|
public const string TwoFactorExtensionDataPersistence = "pm-9115-two-factor-extension-data-persistence";
|
||||||
|
public const string DuoRedirect = "duo-redirect";
|
||||||
|
public const string EmailVerification = "email-verification";
|
||||||
|
public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays";
|
||||||
|
public const string DeviceTrustLogging = "pm-8285-device-trust-logging";
|
||||||
|
public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token";
|
||||||
|
public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh";
|
||||||
|
public const string NewDeviceVerification = "new-device-verification";
|
||||||
|
public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor";
|
||||||
|
public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor";
|
||||||
|
public const string RecoveryCodeLogin = "pm-17128-recovery-code-login";
|
||||||
|
|
||||||
|
/* Autofill Team */
|
||||||
|
public const string IdpAutoSubmitLogin = "idp-auto-submit-login";
|
||||||
|
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
|
||||||
|
public const string InlineMenuFieldQualification = "inline-menu-field-qualification";
|
||||||
|
public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements";
|
||||||
|
public const string SSHAgent = "ssh-agent";
|
||||||
|
public const string SSHVersionCheckQAOverride = "ssh-version-check-qa-override";
|
||||||
|
public const string GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor";
|
||||||
|
public const string DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2";
|
||||||
|
public const string NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements";
|
||||||
|
public const string BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain";
|
||||||
|
public const string NotificationRefresh = "notification-refresh";
|
||||||
|
public const string EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill";
|
||||||
|
public const string MacOsNativeCredentialSync = "macos-native-credential-sync";
|
||||||
|
public const string InlineMenuTotp = "inline-menu-totp";
|
||||||
|
|
||||||
|
/* Billing Team */
|
||||||
|
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
||||||
|
public const string TrialPayment = "PM-8163-trial-payment";
|
||||||
|
public const string ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs";
|
||||||
|
public const string UsePricingService = "use-pricing-service";
|
||||||
|
public const string P15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal";
|
||||||
|
public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features";
|
||||||
|
public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method";
|
||||||
|
|
||||||
|
/* Key Management Team */
|
||||||
|
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
||||||
|
public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service";
|
||||||
|
public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration";
|
||||||
|
public const string Argon2Default = "argon2-default";
|
||||||
|
public const string UserkeyRotationV2 = "userkey-rotation-v2";
|
||||||
|
public const string SSHKeyItemVaultItem = "ssh-key-vault-item";
|
||||||
|
public const string PM17987_BlockType0 = "pm-17987-block-type-0";
|
||||||
|
|
||||||
|
/* Mobile Team */
|
||||||
|
public const string NativeCarouselFlow = "native-carousel-flow";
|
||||||
|
public const string NativeCreateAccountFlow = "native-create-account-flow";
|
||||||
|
public const string AndroidImportLoginsFlow = "import-logins-flow";
|
||||||
|
public const string AppReviewPrompt = "app-review-prompt";
|
||||||
|
public const string EnablePasswordManagerSyncAndroid = "enable-password-manager-sync-android";
|
||||||
|
public const string EnablePasswordManagerSynciOS = "enable-password-manager-sync-ios";
|
||||||
|
public const string AndroidMutualTls = "mutual-tls";
|
||||||
|
public const string SingleTapPasskeyCreation = "single-tap-passkey-creation";
|
||||||
|
public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication";
|
||||||
|
public const string EnablePMAuthenticatorSync = "enable-pm-bwa-sync";
|
||||||
|
public const string PM3503_MobileAnonAddySelfHostAlias = "anon-addy-self-host-alias";
|
||||||
|
public const string PM3553_MobileSimpleLoginSelfHostAlias = "simple-login-self-host-alias";
|
||||||
|
|
||||||
|
/* Platform Team */
|
||||||
|
public const string PersistPopupView = "persist-popup-view";
|
||||||
|
public const string StorageReseedRefactor = "storage-reseed-refactor";
|
||||||
|
public const string WebPush = "web-push";
|
||||||
|
public const string RecordInstallationLastActivityDate = "installation-last-activity-date";
|
||||||
|
|
||||||
/* Tools Team */
|
/* Tools Team */
|
||||||
public const string ItemShare = "item-share";
|
public const string ItemShare = "item-share";
|
||||||
@ -116,6 +184,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications";
|
public const string EnableRiskInsightsNotifications = "enable-risk-insights-notifications";
|
||||||
public const string DesktopSendUIRefresh = "desktop-send-ui-refresh";
|
public const string DesktopSendUIRefresh = "desktop-send-ui-refresh";
|
||||||
public const string ExportAttachments = "export-attachments";
|
public const string ExportAttachments = "export-attachments";
|
||||||
|
public const string GeneratorToolsModernization = "generator-tools-modernization";
|
||||||
|
|
||||||
/* Vault Team */
|
/* Vault Team */
|
||||||
public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge";
|
public const string PM8851_BrowserOnboardingNudge = "pm-8851-browser-onboarding-nudge";
|
||||||
@ -125,70 +194,7 @@ public static class FeatureFlagKeys
|
|||||||
public const string VaultBulkManagementAction = "vault-bulk-management-action";
|
public const string VaultBulkManagementAction = "vault-bulk-management-action";
|
||||||
public const string RestrictProviderAccess = "restrict-provider-access";
|
public const string RestrictProviderAccess = "restrict-provider-access";
|
||||||
public const string SecurityTasks = "security-tasks";
|
public const string SecurityTasks = "security-tasks";
|
||||||
|
|
||||||
/* Auth Team */
|
|
||||||
public const string PM9112DeviceApprovalPersistence = "pm-9112-device-approval-persistence";
|
|
||||||
|
|
||||||
/* Key Management Team */
|
|
||||||
public const string PM17987_BlockType0 = "pm-17987-block-type-0";
|
|
||||||
|
|
||||||
public const string ReturnErrorOnExistingKeypair = "return-error-on-existing-keypair";
|
|
||||||
public const string UseTreeWalkerApiForPageDetailsCollection = "use-tree-walker-api-for-page-details-collection";
|
|
||||||
public const string DuoRedirect = "duo-redirect";
|
|
||||||
public const string AC2101UpdateTrialInitiationEmail = "AC-2101-update-trial-initiation-email";
|
|
||||||
public const string EmailVerification = "email-verification";
|
|
||||||
public const string EmailVerificationDisableTimingDelays = "email-verification-disable-timing-delays";
|
|
||||||
public const string PM4154BulkEncryptionService = "PM-4154-bulk-encryption-service";
|
|
||||||
public const string InlineMenuFieldQualification = "inline-menu-field-qualification";
|
|
||||||
public const string InlineMenuPositioningImprovements = "inline-menu-positioning-improvements";
|
|
||||||
public const string DeviceTrustLogging = "pm-8285-device-trust-logging";
|
|
||||||
public const string SSHKeyItemVaultItem = "ssh-key-vault-item";
|
|
||||||
public const string SSHAgent = "ssh-agent";
|
|
||||||
public const string SSHVersionCheckQAOverride = "ssh-version-check-qa-override";
|
|
||||||
public const string AuthenticatorTwoFactorToken = "authenticator-2fa-token";
|
|
||||||
public const string IdpAutoSubmitLogin = "idp-auto-submit-login";
|
|
||||||
public const string UnauthenticatedExtensionUIRefresh = "unauth-ui-refresh";
|
|
||||||
public const string GenerateIdentityFillScriptRefactor = "generate-identity-fill-script-refactor";
|
|
||||||
public const string DelayFido2PageScriptInitWithinMv2 = "delay-fido2-page-script-init-within-mv2";
|
|
||||||
public const string NativeCarouselFlow = "native-carousel-flow";
|
|
||||||
public const string NativeCreateAccountFlow = "native-create-account-flow";
|
|
||||||
public const string NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements";
|
|
||||||
public const string BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain";
|
|
||||||
public const string NotificationRefresh = "notification-refresh";
|
|
||||||
public const string PersistPopupView = "persist-popup-view";
|
|
||||||
public const string CipherKeyEncryption = "cipher-key-encryption";
|
public const string CipherKeyEncryption = "cipher-key-encryption";
|
||||||
public const string EnableNewCardCombinedExpiryAutofill = "enable-new-card-combined-expiry-autofill";
|
|
||||||
public const string StorageReseedRefactor = "storage-reseed-refactor";
|
|
||||||
public const string TrialPayment = "PM-8163-trial-payment";
|
|
||||||
public const string RemoveServerVersionHeader = "remove-server-version-header";
|
|
||||||
public const string GeneratorToolsModernization = "generator-tools-modernization";
|
|
||||||
public const string NewDeviceVerification = "new-device-verification";
|
|
||||||
public const string MacOsNativeCredentialSync = "macos-native-credential-sync";
|
|
||||||
public const string InlineMenuTotp = "inline-menu-totp";
|
|
||||||
public const string PrivateKeyRegeneration = "pm-12241-private-key-regeneration";
|
|
||||||
public const string AppReviewPrompt = "app-review-prompt";
|
|
||||||
public const string ResellerManagedOrgAlert = "PM-15814-alert-owners-of-reseller-managed-orgs";
|
|
||||||
public const string Argon2Default = "argon2-default";
|
|
||||||
public const string UsePricingService = "use-pricing-service";
|
|
||||||
public const string RecordInstallationLastActivityDate = "installation-last-activity-date";
|
|
||||||
public const string UserkeyRotationV2 = "userkey-rotation-v2";
|
|
||||||
public const string EnablePasswordManagerSyncAndroid = "enable-password-manager-sync-android";
|
|
||||||
public const string EnablePasswordManagerSynciOS = "enable-password-manager-sync-ios";
|
|
||||||
public const string AccountDeprovisioningBanner = "pm-17120-account-deprovisioning-admin-console-banner";
|
|
||||||
public const string SingleTapPasskeyCreation = "single-tap-passkey-creation";
|
|
||||||
public const string SingleTapPasskeyAuthentication = "single-tap-passkey-authentication";
|
|
||||||
public const string EnablePMAuthenticatorSync = "enable-pm-bwa-sync";
|
|
||||||
public const string P15179_AddExistingOrgsFromProviderPortal = "pm-15179-add-existing-orgs-from-provider-portal";
|
|
||||||
public const string AndroidMutualTls = "mutual-tls";
|
|
||||||
public const string RecoveryCodeLogin = "pm-17128-recovery-code-login";
|
|
||||||
public const string PM3503_MobileAnonAddySelfHostAlias = "anon-addy-self-host-alias";
|
|
||||||
public const string WebPush = "web-push";
|
|
||||||
public const string AndroidImportLoginsFlow = "import-logins-flow";
|
|
||||||
public const string PM12276Breadcrumbing = "pm-12276-breadcrumbing-for-business-features";
|
|
||||||
public const string PM18794_ProviderPaymentMethod = "pm-18794-provider-payment-method";
|
|
||||||
public const string PM3553_MobileSimpleLoginSelfHostAlias = "simple-login-self-host-alias";
|
|
||||||
public const string SetInitialPasswordRefactor = "pm-16117-set-initial-password-refactor";
|
|
||||||
public const string ChangeExistingPasswordRefactor = "pm-16117-change-existing-password-refactor";
|
|
||||||
|
|
||||||
public static List<string> GetAllKeys()
|
public static List<string> GetAllKeys()
|
||||||
{
|
{
|
||||||
|
@ -23,8 +23,8 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
|
||||||
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.402.28" />
|
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.402.61" />
|
||||||
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.85" />
|
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.118" />
|
||||||
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
|
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
|
||||||
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
|
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.10" />
|
||||||
@ -61,7 +61,7 @@
|
|||||||
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
<PackageReference Include="Otp.NET" Version="1.4.0" />
|
||||||
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
<PackageReference Include="YubicoDotNetClient" Version="1.2.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.10" />
|
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="8.0.10" />
|
||||||
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.6.0" />
|
<PackageReference Include="LaunchDarkly.ServerSdk" Version="8.7.0" />
|
||||||
<PackageReference Include="Quartz" Version="3.13.1" />
|
<PackageReference Include="Quartz" Version="3.13.1" />
|
||||||
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.13.1" />
|
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.13.1" />
|
||||||
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.13.1" />
|
<PackageReference Include="Quartz.Extensions.DependencyInjection" Version="3.13.1" />
|
||||||
|
@ -14,5 +14,7 @@ public enum ClientType : byte
|
|||||||
[Display(Name = "Desktop App")]
|
[Display(Name = "Desktop App")]
|
||||||
Desktop = 3,
|
Desktop = 3,
|
||||||
[Display(Name = "Mobile App")]
|
[Display(Name = "Mobile App")]
|
||||||
Mobile = 4
|
Mobile = 4,
|
||||||
|
[Display(Name = "CLI")]
|
||||||
|
Cli = 5
|
||||||
}
|
}
|
||||||
|
@ -14,18 +14,17 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<table width="100%" border="0" cellpadding="0" cellspacing="0"
|
<table width="100%" border="0" cellpadding="0" cellspacing="0" style="padding-bottom: 24px; padding-left: 24px; padding-right: 24px; text-align: center;" align="center">
|
||||||
style="display: table; width:100%; padding-bottom: 24px; text-align: center;" align="center">
|
|
||||||
<tr>
|
<tr>
|
||||||
<td display="display: table-cell">
|
<td>
|
||||||
<a href="{{ReviewPasswordsUrl}}" clicktracking=off target="_blank"
|
<a href="{{ReviewPasswordsUrl}}" clicktracking=off target="_blank"
|
||||||
style="display: inline-block; font-weight: bold; color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; border-radius: 999px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
style="display: inline-block; font-weight: bold; color: #ffffff; text-decoration: none; text-align: center; cursor: pointer; border-radius: 999px; background-color: #175DDC; border-color: #175DDC; border-style: solid; border-width: 10px 20px; margin: 0; font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; line-height: 25px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none;">
|
||||||
Review at-risk passwords
|
Review at-risk passwords
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<table width="100%" border="0" cellpadding="0" cellspacing="0"
|
</table>
|
||||||
style="display: table; width:100%; padding-bottom: 24px; text-align: center;" align="center">
|
<table width="100%" border="0" cellpadding="0" cellspacing="0" style="padding-bottom: 24px; padding-left: 24px; padding-right: 24px; text-align: center;" align="center">
|
||||||
<tr>
|
<tr>
|
||||||
<td display="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 12px; line-height: 16px;">
|
<td display="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; font-style: normal; font-weight: 400; font-size: 12px; line-height: 16px;">
|
||||||
{{formatAdminOwnerEmails AdminOwnerEmails}}
|
{{formatAdminOwnerEmails AdminOwnerEmails}}
|
||||||
|
@ -55,6 +55,7 @@ public class OrganizationLicense : ILicense
|
|||||||
UseSecretsManager = org.UseSecretsManager;
|
UseSecretsManager = org.UseSecretsManager;
|
||||||
SmSeats = org.SmSeats;
|
SmSeats = org.SmSeats;
|
||||||
SmServiceAccounts = org.SmServiceAccounts;
|
SmServiceAccounts = org.SmServiceAccounts;
|
||||||
|
UseRiskInsights = org.UseRiskInsights;
|
||||||
|
|
||||||
// Deprecated. Left for backwards compatibility with old license versions.
|
// Deprecated. Left for backwards compatibility with old license versions.
|
||||||
LimitCollectionCreationDeletion = org.LimitCollectionCreation || org.LimitCollectionDeletion;
|
LimitCollectionCreationDeletion = org.LimitCollectionCreation || org.LimitCollectionDeletion;
|
||||||
@ -143,6 +144,7 @@ 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; }
|
||||||
|
public bool UseRiskInsights { get; set; }
|
||||||
|
|
||||||
// Deprecated. Left for backwards compatibility with old license versions.
|
// Deprecated. Left for backwards compatibility with old license versions.
|
||||||
public bool LimitCollectionCreationDeletion { get; set; } = true;
|
public bool LimitCollectionCreationDeletion { get; set; } = true;
|
||||||
@ -218,7 +220,8 @@ public class OrganizationLicense : ILicense
|
|||||||
!p.Name.Equals(nameof(Issued)) &&
|
!p.Name.Equals(nameof(Issued)) &&
|
||||||
!p.Name.Equals(nameof(Refresh))
|
!p.Name.Equals(nameof(Refresh))
|
||||||
)
|
)
|
||||||
))
|
) &&
|
||||||
|
!p.Name.Equals(nameof(UseRiskInsights)))
|
||||||
.OrderBy(p => p.Name)
|
.OrderBy(p => p.Name)
|
||||||
.Select(p => $"{p.Name}:{Utilities.CoreHelpers.FormatLicenseSignatureValue(p.GetValue(this, null))}")
|
.Select(p => $"{p.Name}:{Utilities.CoreHelpers.FormatLicenseSignatureValue(p.GetValue(this, null))}")
|
||||||
.Aggregate((c, n) => $"{c}|{n}");
|
.Aggregate((c, n) => $"{c}|{n}");
|
||||||
|
@ -8,7 +8,7 @@ public class SecurityTaskNotificationViewModel : BaseMailModel
|
|||||||
|
|
||||||
public bool TaskCountPlural => TaskCount != 1;
|
public bool TaskCountPlural => TaskCount != 1;
|
||||||
|
|
||||||
public IEnumerable<string> AdminOwnerEmails { get; set; }
|
public List<string> AdminOwnerEmails { get; set; }
|
||||||
|
|
||||||
public string ReviewPasswordsUrl => $"{WebVaultUrl}/browser-extension-prompt";
|
public string ReviewPasswordsUrl => $"{WebVaultUrl}/browser-extension-prompt";
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.OrganizationFeatures.Organizations.Interfaces;
|
|||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Authorization;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||||
using Bit.Core.Models.Business.Tokenables;
|
using Bit.Core.Models.Business.Tokenables;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationCollections;
|
using Bit.Core.OrganizationFeatures.OrganizationCollections;
|
||||||
using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;
|
using Bit.Core.OrganizationFeatures.OrganizationCollections.Interfaces;
|
||||||
@ -168,6 +169,8 @@ public static class OrganizationServiceCollectionExtensions
|
|||||||
services.AddScoped<IOrganizationUserUserDetailsQuery, OrganizationUserUserDetailsQuery>();
|
services.AddScoped<IOrganizationUserUserDetailsQuery, OrganizationUserUserDetailsQuery>();
|
||||||
services.AddScoped<IGetOrganizationUsersManagementStatusQuery, GetOrganizationUsersManagementStatusQuery>();
|
services.AddScoped<IGetOrganizationUsersManagementStatusQuery, GetOrganizationUsersManagementStatusQuery>();
|
||||||
|
|
||||||
|
services.AddScoped<IRestoreOrganizationUserCommand, RestoreOrganizationUserCommand>();
|
||||||
|
|
||||||
services.AddScoped<IAuthorizationHandler, OrganizationUserUserMiniDetailsAuthorizationHandler>();
|
services.AddScoped<IAuthorizationHandler, OrganizationUserUserMiniDetailsAuthorizationHandler>();
|
||||||
services.AddScoped<IAuthorizationHandler, OrganizationUserUserDetailsAuthorizationHandler>();
|
services.AddScoped<IAuthorizationHandler, OrganizationUserUserDetailsAuthorizationHandler>();
|
||||||
services.AddScoped<IHasConfirmedOwnersExceptQuery, HasConfirmedOwnersExceptQuery>();
|
services.AddScoped<IHasConfirmedOwnersExceptQuery, HasConfirmedOwnersExceptQuery>();
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Models.Mail;
|
using Bit.Core.AdminConsole.Models.Mail;
|
||||||
@ -752,7 +753,21 @@ public class HandlebarsMailService : IMailService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var emailList = ((IEnumerable<string>)parameters[0]).ToList();
|
var emailList = new List<string>();
|
||||||
|
if (parameters[0] is JsonElement jsonElement && jsonElement.ValueKind == JsonValueKind.Array)
|
||||||
|
{
|
||||||
|
emailList = jsonElement.EnumerateArray().Select(e => e.GetString()).ToList();
|
||||||
|
}
|
||||||
|
else if (parameters[0] is IEnumerable<string> emails)
|
||||||
|
{
|
||||||
|
emailList = emails.ToList();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
writer.WriteSafeString(string.Empty);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (emailList.Count == 0)
|
if (emailList.Count == 0)
|
||||||
{
|
{
|
||||||
writer.WriteSafeString(string.Empty);
|
writer.WriteSafeString(string.Empty);
|
||||||
@ -774,7 +789,7 @@ public class HandlebarsMailService : IMailService
|
|||||||
{
|
{
|
||||||
outputMessage += string.Join(", ", emailList.Take(emailList.Count - 1)
|
outputMessage += string.Join(", ", emailList.Take(emailList.Count - 1)
|
||||||
.Select(email => constructAnchorElement(email)));
|
.Select(email => constructAnchorElement(email)));
|
||||||
outputMessage += $", and {constructAnchorElement(emailList.Last())}.";
|
outputMessage += $" and {constructAnchorElement(emailList.Last())}.";
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.WriteSafeString($"{outputMessage}");
|
writer.WriteSafeString($"{outputMessage}");
|
||||||
@ -1250,7 +1265,7 @@ public class HandlebarsMailService : IMailService
|
|||||||
{
|
{
|
||||||
OrgName = CoreHelpers.SanitizeForEmail(sanitizedOrgName, false),
|
OrgName = CoreHelpers.SanitizeForEmail(sanitizedOrgName, false),
|
||||||
TaskCount = notification.TaskCount,
|
TaskCount = notification.TaskCount,
|
||||||
AdminOwnerEmails = adminOwnerEmails,
|
AdminOwnerEmails = adminOwnerEmails.ToList(),
|
||||||
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
WebVaultUrl = _globalSettings.BaseServiceUri.VaultWithHash,
|
||||||
};
|
};
|
||||||
message.Category = "SecurityTasksNotification";
|
message.Category = "SecurityTasksNotification";
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Platform.Push;
|
using Bit.Core.Platform.Push;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Tools.Enums;
|
using Bit.Core.Tools.Enums;
|
||||||
using Bit.Core.Tools.ImportFeatures.Interfaces;
|
using Bit.Core.Tools.ImportFeatures.Interfaces;
|
||||||
using Bit.Core.Tools.Models.Business;
|
using Bit.Core.Tools.Models.Business;
|
||||||
@ -26,7 +29,8 @@ public class ImportCiphersCommand : IImportCiphersCommand
|
|||||||
private readonly ICollectionRepository _collectionRepository;
|
private readonly ICollectionRepository _collectionRepository;
|
||||||
private readonly IReferenceEventService _referenceEventService;
|
private readonly IReferenceEventService _referenceEventService;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
|
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
|
||||||
public ImportCiphersCommand(
|
public ImportCiphersCommand(
|
||||||
ICipherRepository cipherRepository,
|
ICipherRepository cipherRepository,
|
||||||
@ -37,7 +41,9 @@ public class ImportCiphersCommand : IImportCiphersCommand
|
|||||||
IPushNotificationService pushService,
|
IPushNotificationService pushService,
|
||||||
IPolicyService policyService,
|
IPolicyService policyService,
|
||||||
IReferenceEventService referenceEventService,
|
IReferenceEventService referenceEventService,
|
||||||
ICurrentContext currentContext)
|
ICurrentContext currentContext,
|
||||||
|
IPolicyRequirementQuery policyRequirementQuery,
|
||||||
|
IFeatureService featureService)
|
||||||
{
|
{
|
||||||
_cipherRepository = cipherRepository;
|
_cipherRepository = cipherRepository;
|
||||||
_folderRepository = folderRepository;
|
_folderRepository = folderRepository;
|
||||||
@ -48,9 +54,10 @@ public class ImportCiphersCommand : IImportCiphersCommand
|
|||||||
_policyService = policyService;
|
_policyService = policyService;
|
||||||
_referenceEventService = referenceEventService;
|
_referenceEventService = referenceEventService;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
|
_policyRequirementQuery = policyRequirementQuery;
|
||||||
|
_featureService = featureService;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public async Task ImportIntoIndividualVaultAsync(
|
public async Task ImportIntoIndividualVaultAsync(
|
||||||
List<Folder> folders,
|
List<Folder> folders,
|
||||||
List<CipherDetails> ciphers,
|
List<CipherDetails> ciphers,
|
||||||
@ -58,8 +65,11 @@ public class ImportCiphersCommand : IImportCiphersCommand
|
|||||||
Guid importingUserId)
|
Guid importingUserId)
|
||||||
{
|
{
|
||||||
// Make sure the user can save new ciphers to their personal vault
|
// Make sure the user can save new ciphers to their personal vault
|
||||||
var anyPersonalOwnershipPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.PersonalOwnership);
|
var isPersonalVaultRestricted = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||||
if (anyPersonalOwnershipPolicies)
|
? (await _policyRequirementQuery.GetAsync<PersonalOwnershipPolicyRequirement>(importingUserId)).DisablePersonalOwnership
|
||||||
|
: await _policyService.AnyPoliciesApplicableToUserAsync(importingUserId, PolicyType.PersonalOwnership);
|
||||||
|
|
||||||
|
if (isPersonalVaultRestricted)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("You cannot import items into your personal vault because you are " +
|
throw new BadRequestException("You cannot import items into your personal vault because you are " +
|
||||||
"a member of an organization which forbids it.");
|
"a member of an organization which forbids it.");
|
||||||
|
@ -16,7 +16,11 @@ public static class DeviceTypes
|
|||||||
DeviceType.LinuxDesktop,
|
DeviceType.LinuxDesktop,
|
||||||
DeviceType.MacOsDesktop,
|
DeviceType.MacOsDesktop,
|
||||||
DeviceType.WindowsDesktop,
|
DeviceType.WindowsDesktop,
|
||||||
DeviceType.UWP,
|
DeviceType.UWP
|
||||||
|
];
|
||||||
|
|
||||||
|
public static IReadOnlyCollection<DeviceType> CliTypes { get; } =
|
||||||
|
[
|
||||||
DeviceType.WindowsCLI,
|
DeviceType.WindowsCLI,
|
||||||
DeviceType.MacOsCLI,
|
DeviceType.MacOsCLI,
|
||||||
DeviceType.LinuxCLI
|
DeviceType.LinuxCLI
|
||||||
@ -50,6 +54,7 @@ public static class DeviceTypes
|
|||||||
{
|
{
|
||||||
not null when MobileTypes.Contains(deviceType.Value) => ClientType.Mobile,
|
not null when MobileTypes.Contains(deviceType.Value) => ClientType.Mobile,
|
||||||
not null when DesktopTypes.Contains(deviceType.Value) => ClientType.Desktop,
|
not null when DesktopTypes.Contains(deviceType.Value) => ClientType.Desktop,
|
||||||
|
not null when CliTypes.Contains(deviceType.Value) => ClientType.Cli,
|
||||||
not null when BrowserExtensionTypes.Contains(deviceType.Value) => ClientType.Browser,
|
not null when BrowserExtensionTypes.Contains(deviceType.Value) => ClientType.Browser,
|
||||||
not null when BrowserTypes.Contains(deviceType.Value) => ClientType.Web,
|
not null when BrowserTypes.Contains(deviceType.Value) => ClientType.Web,
|
||||||
_ => ClientType.All
|
_ => ClientType.All
|
||||||
|
@ -48,9 +48,16 @@ public class CreateManyTaskNotificationsCommand : ICreateManyTaskNotificationsCo
|
|||||||
}).ToList();
|
}).ToList();
|
||||||
|
|
||||||
var organization = await _organizationRepository.GetByIdAsync(orgId);
|
var organization = await _organizationRepository.GetByIdAsync(orgId);
|
||||||
var orgAdminEmails = await _organizationUserRepository.GetManyDetailsByRoleAsync(orgId, OrganizationUserType.Admin);
|
var orgAdminEmails = (await _organizationUserRepository.GetManyDetailsByRoleAsync(orgId, OrganizationUserType.Admin))
|
||||||
var orgOwnerEmails = await _organizationUserRepository.GetManyDetailsByRoleAsync(orgId, OrganizationUserType.Owner);
|
.Select(u => u.Email)
|
||||||
var orgAdminAndOwnerEmails = orgAdminEmails.Concat(orgOwnerEmails).Select(x => x.Email).Distinct().ToList();
|
.ToList();
|
||||||
|
|
||||||
|
var orgOwnerEmails = (await _organizationUserRepository.GetManyDetailsByRoleAsync(orgId, OrganizationUserType.Owner))
|
||||||
|
.Select(u => u.Email)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
// Ensure proper deserialization of emails
|
||||||
|
var orgAdminAndOwnerEmails = orgAdminEmails.Concat(orgOwnerEmails).Distinct().ToList();
|
||||||
|
|
||||||
await _mailService.SendBulkSecurityTaskNotificationsAsync(organization, userTaskCount, orgAdminAndOwnerEmails);
|
await _mailService.SendBulkSecurityTaskNotificationsAsync(organization, userTaskCount, orgAdminAndOwnerEmails);
|
||||||
|
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -41,6 +43,8 @@ public class CipherService : ICipherService
|
|||||||
private readonly IReferenceEventService _referenceEventService;
|
private readonly IReferenceEventService _referenceEventService;
|
||||||
private readonly ICurrentContext _currentContext;
|
private readonly ICurrentContext _currentContext;
|
||||||
private readonly IGetCipherPermissionsForUserQuery _getCipherPermissionsForUserQuery;
|
private readonly IGetCipherPermissionsForUserQuery _getCipherPermissionsForUserQuery;
|
||||||
|
private readonly IPolicyRequirementQuery _policyRequirementQuery;
|
||||||
|
private readonly IFeatureService _featureService;
|
||||||
|
|
||||||
public CipherService(
|
public CipherService(
|
||||||
ICipherRepository cipherRepository,
|
ICipherRepository cipherRepository,
|
||||||
@ -58,7 +62,9 @@ public class CipherService : ICipherService
|
|||||||
GlobalSettings globalSettings,
|
GlobalSettings globalSettings,
|
||||||
IReferenceEventService referenceEventService,
|
IReferenceEventService referenceEventService,
|
||||||
ICurrentContext currentContext,
|
ICurrentContext currentContext,
|
||||||
IGetCipherPermissionsForUserQuery getCipherPermissionsForUserQuery)
|
IGetCipherPermissionsForUserQuery getCipherPermissionsForUserQuery,
|
||||||
|
IPolicyRequirementQuery policyRequirementQuery,
|
||||||
|
IFeatureService featureService)
|
||||||
{
|
{
|
||||||
_cipherRepository = cipherRepository;
|
_cipherRepository = cipherRepository;
|
||||||
_folderRepository = folderRepository;
|
_folderRepository = folderRepository;
|
||||||
@ -76,6 +82,8 @@ public class CipherService : ICipherService
|
|||||||
_referenceEventService = referenceEventService;
|
_referenceEventService = referenceEventService;
|
||||||
_currentContext = currentContext;
|
_currentContext = currentContext;
|
||||||
_getCipherPermissionsForUserQuery = getCipherPermissionsForUserQuery;
|
_getCipherPermissionsForUserQuery = getCipherPermissionsForUserQuery;
|
||||||
|
_policyRequirementQuery = policyRequirementQuery;
|
||||||
|
_featureService = featureService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnownRevisionDate,
|
public async Task SaveAsync(Cipher cipher, Guid savingUserId, DateTime? lastKnownRevisionDate,
|
||||||
@ -143,9 +151,11 @@ public class CipherService : ICipherService
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// Make sure the user can save new ciphers to their personal vault
|
var isPersonalVaultRestricted = _featureService.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||||
var anyPersonalOwnershipPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership);
|
? (await _policyRequirementQuery.GetAsync<PersonalOwnershipPolicyRequirement>(savingUserId)).DisablePersonalOwnership
|
||||||
if (anyPersonalOwnershipPolicies)
|
: await _policyService.AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership);
|
||||||
|
|
||||||
|
if (isPersonalVaultRestricted)
|
||||||
{
|
{
|
||||||
throw new BadRequestException("Due to an Enterprise Policy, you are restricted from saving items to your personal vault.");
|
throw new BadRequestException("Due to an Enterprise Policy, you are restricted from saving items to your personal vault.");
|
||||||
}
|
}
|
||||||
|
@ -196,4 +196,15 @@ public class OrganizationRepository : Repository<Organization, Guid>, IOrganizat
|
|||||||
return result.ToList();
|
return result.ToList();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ICollection<Organization>> GetManyByIdsAsync(IEnumerable<Guid> ids)
|
||||||
|
{
|
||||||
|
await using var connection = new SqlConnection(ConnectionString);
|
||||||
|
|
||||||
|
return (await connection.QueryAsync<Organization>(
|
||||||
|
$"[{Schema}].[{Table}_ReadManyByIds]",
|
||||||
|
new { OrganizationIds = ids.ToGuidIdArrayTVP() },
|
||||||
|
commandType: CommandType.StoredProcedure))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,17 @@
|
|||||||
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.EntityFramework.AdminConsole.Configurations;
|
||||||
|
|
||||||
|
public class OrganizationIntegrationConfigurationEntityTypeConfiguration : IEntityTypeConfiguration<OrganizationIntegrationConfiguration>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<OrganizationIntegrationConfiguration> builder)
|
||||||
|
{
|
||||||
|
builder
|
||||||
|
.Property(p => p.Id)
|
||||||
|
.ValueGeneratedNever();
|
||||||
|
|
||||||
|
builder.ToTable(nameof(OrganizationIntegrationConfiguration));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
using Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.EntityFramework.AdminConsole.Configurations;
|
||||||
|
|
||||||
|
public class OrganizationIntegrationEntityTypeConfiguration : IEntityTypeConfiguration<OrganizationIntegration>
|
||||||
|
{
|
||||||
|
public void Configure(EntityTypeBuilder<OrganizationIntegration> builder)
|
||||||
|
{
|
||||||
|
builder
|
||||||
|
.Property(p => p.Id)
|
||||||
|
.ValueGeneratedNever();
|
||||||
|
|
||||||
|
builder
|
||||||
|
.HasIndex(p => p.OrganizationId)
|
||||||
|
.IsClustered(false);
|
||||||
|
|
||||||
|
builder
|
||||||
|
.HasIndex(p => new { p.OrganizationId, p.Type })
|
||||||
|
.IsUnique()
|
||||||
|
.IsClustered(false);
|
||||||
|
|
||||||
|
builder.ToTable(nameof(OrganizationIntegration));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||||
|
|
||||||
|
public class OrganizationIntegration : Core.AdminConsole.Entities.OrganizationIntegration
|
||||||
|
{
|
||||||
|
public virtual Organization Organization { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OrganizationIntegrationMapperProfile : Profile
|
||||||
|
{
|
||||||
|
public OrganizationIntegrationMapperProfile()
|
||||||
|
{
|
||||||
|
CreateMap<Core.AdminConsole.Entities.OrganizationIntegration, OrganizationIntegration>().ReverseMap();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
using AutoMapper;
|
||||||
|
|
||||||
|
namespace Bit.Infrastructure.EntityFramework.AdminConsole.Models;
|
||||||
|
|
||||||
|
public class OrganizationIntegrationConfiguration : Core.AdminConsole.Entities.OrganizationIntegrationConfiguration
|
||||||
|
{
|
||||||
|
public virtual OrganizationIntegration OrganizationIntegration { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class OrganizationIntegrationConfigurationMapperProfile : Profile
|
||||||
|
{
|
||||||
|
public OrganizationIntegrationConfigurationMapperProfile()
|
||||||
|
{
|
||||||
|
CreateMap<Core.AdminConsole.Entities.OrganizationIntegrationConfiguration, OrganizationIntegrationConfiguration>().ReverseMap();
|
||||||
|
}
|
||||||
|
}
|
@ -354,6 +354,19 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<ICollection<Core.AdminConsole.Entities.Organization>> GetManyByIdsAsync(IEnumerable<Guid> ids)
|
||||||
|
{
|
||||||
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
|
|
||||||
|
var dbContext = GetDatabaseContext(scope);
|
||||||
|
|
||||||
|
var query = from organization in dbContext.Organizations
|
||||||
|
where ids.Contains(organization.Id)
|
||||||
|
select organization;
|
||||||
|
|
||||||
|
return await query.ToArrayAsync();
|
||||||
|
}
|
||||||
|
|
||||||
public Task EnableCollectionEnhancements(Guid organizationId)
|
public Task EnableCollectionEnhancements(Guid organizationId)
|
||||||
{
|
{
|
||||||
throw new NotImplementedException("Collection enhancements migration is not yet supported for Entity Framework.");
|
throw new NotImplementedException("Collection enhancements migration is not yet supported for Entity Framework.");
|
||||||
|
@ -68,12 +68,11 @@ public class WebAuthnCredentialRepository : Repository<Core.Auth.Entities.WebAut
|
|||||||
var newCreds = credentials.ToList();
|
var newCreds = credentials.ToList();
|
||||||
using var scope = ServiceScopeFactory.CreateScope();
|
using var scope = ServiceScopeFactory.CreateScope();
|
||||||
var dbContext = GetDatabaseContext(scope);
|
var dbContext = GetDatabaseContext(scope);
|
||||||
var userWebauthnCredentials = await GetDbSet(dbContext)
|
|
||||||
.Where(wc => wc.Id == wc.Id)
|
var newCredIds = newCreds.Select(nwc => nwc.Id).ToList();
|
||||||
|
var validUserWebauthnCredentials = await GetDbSet(dbContext)
|
||||||
|
.Where(wc => wc.UserId == userId && newCredIds.Contains(wc.Id))
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
var validUserWebauthnCredentials = userWebauthnCredentials
|
|
||||||
.Where(wc => newCreds.Any(nwc => nwc.Id == wc.Id))
|
|
||||||
.Where(wc => wc.UserId == userId);
|
|
||||||
|
|
||||||
foreach (var wc in validUserWebauthnCredentials)
|
foreach (var wc in validUserWebauthnCredentials)
|
||||||
{
|
{
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
using System.Collections;
|
using System.Collections;
|
||||||
using Bit.Core;
|
|
||||||
using Bit.Core.Services;
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Settings;
|
using Bit.Core.Settings;
|
||||||
using Bit.Core.Utilities;
|
using Bit.Core.Utilities;
|
||||||
@ -25,15 +24,6 @@ public sealed class RequestLoggingMiddleware
|
|||||||
|
|
||||||
public Task Invoke(HttpContext context, IFeatureService featureService)
|
public Task Invoke(HttpContext context, IFeatureService featureService)
|
||||||
{
|
{
|
||||||
if (!featureService.IsEnabled(FeatureFlagKeys.RemoveServerVersionHeader))
|
|
||||||
{
|
|
||||||
context.Response.OnStarting(() =>
|
|
||||||
{
|
|
||||||
context.Response.Headers.Append("Server-Version", AssemblyHelpers.GetVersion());
|
|
||||||
return Task.CompletedTask;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
using (_logger.BeginScope(
|
using (_logger.BeginScope(
|
||||||
new RequestLogScope(context.GetIpAddress(_globalSettings),
|
new RequestLogScope(context.GetIpAddress(_globalSettings),
|
||||||
GetHeaderValue(context, "user-agent"),
|
GetHeaderValue(context, "user-agent"),
|
||||||
|
@ -0,0 +1,20 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[OrganizationIntegrationConfigurationDetails_ReadManyByEventTypeOrganizationIdIntegrationType]
|
||||||
|
@EventType SMALLINT,
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER,
|
||||||
|
@IntegrationType SMALLINT
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
oic.*
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationIntegrationConfigurationDetailsView] oic
|
||||||
|
WHERE
|
||||||
|
oic.[EventType] = @EventType
|
||||||
|
AND
|
||||||
|
oic.[OrganizationId] = @OrganizationId
|
||||||
|
AND
|
||||||
|
oic.[IntegrationType] = @IntegrationType
|
||||||
|
END
|
||||||
|
GO
|
67
src/Sql/dbo/Stored Procedures/Organization_ReadManyByIds.sql
Normal file
67
src/Sql/dbo/Stored Procedures/Organization_ReadManyByIds.sql
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
CREATE PROCEDURE [dbo].[Organization_ReadManyByIds] @OrganizationIds AS [dbo].[GuidIdArray] READONLY
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT o.[Id],
|
||||||
|
o.[Identifier],
|
||||||
|
o.[Name],
|
||||||
|
o.[BusinessName],
|
||||||
|
o.[BusinessAddress1],
|
||||||
|
o.[BusinessAddress2],
|
||||||
|
o.[BusinessAddress3],
|
||||||
|
o.[BusinessCountry],
|
||||||
|
o.[BusinessTaxNumber],
|
||||||
|
o.[BillingEmail],
|
||||||
|
o.[Plan],
|
||||||
|
o.[PlanType],
|
||||||
|
o.[Seats],
|
||||||
|
o.[MaxCollections],
|
||||||
|
o.[UsePolicies],
|
||||||
|
o.[UseSso],
|
||||||
|
o.[UseGroups],
|
||||||
|
o.[UseDirectory],
|
||||||
|
o.[UseEvents],
|
||||||
|
o.[UseTotp],
|
||||||
|
o.[Use2fa],
|
||||||
|
o.[UseApi],
|
||||||
|
o.[UseResetPassword],
|
||||||
|
o.[SelfHost],
|
||||||
|
o.[UsersGetPremium],
|
||||||
|
o.[Storage],
|
||||||
|
o.[MaxStorageGb],
|
||||||
|
o.[Gateway],
|
||||||
|
o.[GatewayCustomerId],
|
||||||
|
o.[GatewaySubscriptionId],
|
||||||
|
o.[ReferenceData],
|
||||||
|
o.[Enabled],
|
||||||
|
o.[LicenseKey],
|
||||||
|
o.[PublicKey],
|
||||||
|
o.[PrivateKey],
|
||||||
|
o.[TwoFactorProviders],
|
||||||
|
o.[ExpirationDate],
|
||||||
|
o.[CreationDate],
|
||||||
|
o.[RevisionDate],
|
||||||
|
o.[OwnersNotifiedOfAutoscaling],
|
||||||
|
o.[MaxAutoscaleSeats],
|
||||||
|
o.[UseKeyConnector],
|
||||||
|
o.[UseScim],
|
||||||
|
o.[UseCustomPermissions],
|
||||||
|
o.[UseSecretsManager],
|
||||||
|
o.[Status],
|
||||||
|
o.[UsePasswordManager],
|
||||||
|
o.[SmSeats],
|
||||||
|
o.[SmServiceAccounts],
|
||||||
|
o.[MaxAutoscaleSmSeats],
|
||||||
|
o.[MaxAutoscaleSmServiceAccounts],
|
||||||
|
o.[SecretsManagerBeta],
|
||||||
|
o.[LimitCollectionCreation],
|
||||||
|
o.[LimitCollectionDeletion],
|
||||||
|
o.[LimitItemDeletion],
|
||||||
|
o.[AllowAdminAccessToAllCollectionItems],
|
||||||
|
o.[UseRiskInsights]
|
||||||
|
FROM [dbo].[OrganizationView] o
|
||||||
|
INNER JOIN @OrganizationIds ids ON o.[Id] = ids.[Id]
|
||||||
|
|
||||||
|
END
|
||||||
|
|
20
src/Sql/dbo/Tables/OrganizationIntegration.sql
Normal file
20
src/Sql/dbo/Tables/OrganizationIntegration.sql
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
CREATE TABLE [dbo].[OrganizationIntegration]
|
||||||
|
(
|
||||||
|
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
[OrganizationId] UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
[Type] SMALLINT NOT NULL,
|
||||||
|
[Configuration] VARCHAR (MAX) NULL,
|
||||||
|
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||||
|
[RevisionDate] DATETIME2 (7) NOT NULL,
|
||||||
|
CONSTRAINT [PK_OrganizationIntegration] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||||
|
CONSTRAINT [FK_OrganizationIntegration_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id])
|
||||||
|
);
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE NONCLUSTERED INDEX [IX_OrganizationIntegration_OrganizationId]
|
||||||
|
ON [dbo].[OrganizationIntegration]([OrganizationId] ASC);
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX [IX_OrganizationIntegration_Organization_Type]
|
||||||
|
ON [dbo].[OrganizationIntegration]([OrganizationId], [Type]);
|
||||||
|
GO
|
13
src/Sql/dbo/Tables/OrganizationIntegrationConfiguration.sql
Normal file
13
src/Sql/dbo/Tables/OrganizationIntegrationConfiguration.sql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
CREATE TABLE [dbo].[OrganizationIntegrationConfiguration]
|
||||||
|
(
|
||||||
|
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
[OrganizationIntegrationId] UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
[EventType] SMALLINT NOT NULL,
|
||||||
|
[Configuration] VARCHAR (MAX) NULL,
|
||||||
|
[Template] VARCHAR (MAX) NULL,
|
||||||
|
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||||
|
[RevisionDate] DATETIME2 (7) NOT NULL,
|
||||||
|
CONSTRAINT [PK_OrganizationIntegrationConfiguration] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||||
|
CONSTRAINT [FK_OrganizationIntegrationConfiguration_OrganizationIntegration] FOREIGN KEY ([OrganizationIntegrationId]) REFERENCES [dbo].[OrganizationIntegration] ([Id])
|
||||||
|
);
|
||||||
|
GO
|
@ -0,0 +1,13 @@
|
|||||||
|
CREATE VIEW [dbo].[OrganizationIntegrationConfigurationDetailsView]
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
oi.[OrganizationId],
|
||||||
|
oi.[Type] AS [IntegrationType],
|
||||||
|
oic.[EventType],
|
||||||
|
oic.[Configuration],
|
||||||
|
oi.[Configuration] AS [IntegrationConfiguration],
|
||||||
|
oic.[Template]
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationIntegrationConfiguration] oic
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[OrganizationIntegration] oi ON oi.[Id] = oic.[OrganizationIntegrationId]
|
@ -0,0 +1,6 @@
|
|||||||
|
CREATE VIEW [dbo].[OrganizationIntegrationConfigurationView]
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationIntegrationConfiguration]
|
6
src/Sql/dbo/Views/OrganizationIntegrationView.sql
Normal file
6
src/Sql/dbo/Views/OrganizationIntegrationView.sql
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
CREATE VIEW [dbo].[OrganizationIntegrationView]
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationIntegration]
|
@ -10,14 +10,19 @@ namespace Bit.Api.Test.AdminConsole.Models.Response.Helpers;
|
|||||||
|
|
||||||
public class PolicyDetailResponsesTests
|
public class PolicyDetailResponsesTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Theory]
|
||||||
public async Task GetSingleOrgPolicyDetailResponseAsync_GivenPolicyEntity_WhenIsSingleOrgTypeAndHasVerifiedDomains_ThenShouldNotBeAbleToToggle()
|
[InlineData(true, false)]
|
||||||
|
[InlineData(false, true)]
|
||||||
|
public async Task GetSingleOrgPolicyDetailResponseAsync_WhenIsSingleOrgTypeAndHasVerifiedDomains_ShouldReturnExpectedToggleState(
|
||||||
|
bool policyEnabled,
|
||||||
|
bool expectedCanToggle)
|
||||||
{
|
{
|
||||||
var fixture = new Fixture();
|
var fixture = new Fixture();
|
||||||
|
|
||||||
var policy = fixture.Build<Policy>()
|
var policy = fixture.Build<Policy>()
|
||||||
.Without(p => p.Data)
|
.Without(p => p.Data)
|
||||||
.With(p => p.Type, PolicyType.SingleOrg)
|
.With(p => p.Type, PolicyType.SingleOrg)
|
||||||
|
.With(p => p.Enabled, policyEnabled)
|
||||||
.Create();
|
.Create();
|
||||||
|
|
||||||
var querySub = Substitute.For<IOrganizationHasVerifiedDomainsQuery>();
|
var querySub = Substitute.For<IOrganizationHasVerifiedDomainsQuery>();
|
||||||
@ -26,11 +31,11 @@ public class PolicyDetailResponsesTests
|
|||||||
|
|
||||||
var result = await policy.GetSingleOrgPolicyDetailResponseAsync(querySub);
|
var result = await policy.GetSingleOrgPolicyDetailResponseAsync(querySub);
|
||||||
|
|
||||||
Assert.False(result.CanToggleState);
|
Assert.Equal(expectedCanToggle, result.CanToggleState);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetSingleOrgPolicyDetailResponseAsync_GivenPolicyEntity_WhenIsNotSingleOrgType_ThenShouldThrowArgumentException()
|
public async Task GetSingleOrgPolicyDetailResponseAsync_WhenIsNotSingleOrgType_ThenShouldThrowArgumentException()
|
||||||
{
|
{
|
||||||
var fixture = new Fixture();
|
var fixture = new Fixture();
|
||||||
|
|
||||||
@ -49,7 +54,7 @@ public class PolicyDetailResponsesTests
|
|||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task GetSingleOrgPolicyDetailResponseAsync_GivenPolicyEntity_WhenIsSingleOrgTypeAndDoesNotHaveVerifiedDomains_ThenShouldBeAbleToToggle()
|
public async Task GetSingleOrgPolicyDetailResponseAsync_WhenIsSingleOrgTypeAndDoesNotHaveVerifiedDomains_ThenShouldBeAbleToToggle()
|
||||||
{
|
{
|
||||||
var fixture = new Fixture();
|
var fixture = new Fixture();
|
||||||
|
|
||||||
|
@ -0,0 +1,693 @@
|
|||||||
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser.v1;
|
||||||
|
using Bit.Core.AdminConsole.Services;
|
||||||
|
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
||||||
|
using Bit.Core.Billing.Enums;
|
||||||
|
using Bit.Core.Context;
|
||||||
|
using Bit.Core.Entities;
|
||||||
|
using Bit.Core.Enums;
|
||||||
|
using Bit.Core.Exceptions;
|
||||||
|
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
|
||||||
|
using Bit.Core.Platform.Push;
|
||||||
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
|
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using NSubstitute;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers.RestoreUser;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class RestoreOrganizationUserCommandTests
|
||||||
|
{
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||||
|
|
||||||
|
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited);
|
||||||
|
await sutProvider.GetDependency<IEventService>()
|
||||||
|
.Received(1)
|
||||||
|
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUser_WithPushSyncOrgKeysOnRevokeRestoreEnabled_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited);
|
||||||
|
await sutProvider.GetDependency<IEventService>()
|
||||||
|
.Received(1)
|
||||||
|
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>()
|
||||||
|
.Received(1)
|
||||||
|
.PushSyncOrgKeysAsync(organizationUser.UserId!.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUser_WithEventSystemUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
RestoreUser_Setup(organization, null, organizationUser, sutProvider);
|
||||||
|
|
||||||
|
await sutProvider.Sut.RestoreUserAsync(organizationUser, eventSystemUser);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited);
|
||||||
|
await sutProvider.GetDependency<IEventService>()
|
||||||
|
.Received(1)
|
||||||
|
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored, eventSystemUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUser_WithEventSystemUser_WithPushSyncOrgKeysOnRevokeRestoreEnabled_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
RestoreUser_Setup(organization, null, organizationUser, sutProvider);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
await sutProvider.Sut.RestoreUserAsync(organizationUser, eventSystemUser);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited);
|
||||||
|
await sutProvider.GetDependency<IEventService>()
|
||||||
|
.Received(1)
|
||||||
|
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored, eventSystemUser);
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>()
|
||||||
|
.Received(1)
|
||||||
|
.PushSyncOrgKeysAsync(organizationUser.UserId!.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUser_RestoreThemselves_Fails(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
organizationUser.UserId = owner.Id;
|
||||||
|
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||||
|
|
||||||
|
Assert.Contains("you cannot restore yourself", exception.Message.ToLowerInvariant());
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
await sutProvider.GetDependency<IEventService>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(OrganizationUserType.Admin)]
|
||||||
|
[BitAutoData(OrganizationUserType.Custom)]
|
||||||
|
public async Task RestoreUser_AdminRestoreOwner_Fails(OrganizationUserType restoringUserType,
|
||||||
|
Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed)] OrganizationUser restoringUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked, OrganizationUserType.Owner)] OrganizationUser organizationUser, SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
restoringUser.Type = restoringUserType;
|
||||||
|
RestoreUser_Setup(organization, restoringUser, organizationUser, sutProvider);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.RestoreUserAsync(organizationUser, restoringUser.Id));
|
||||||
|
|
||||||
|
Assert.Contains("only owners can restore other owners", exception.Message.ToLowerInvariant());
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
await sutProvider.GetDependency<IEventService>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Invited)]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Accepted)]
|
||||||
|
[BitAutoData(OrganizationUserStatusType.Confirmed)]
|
||||||
|
public async Task RestoreUser_WithStatusOtherThanRevoked_Fails(OrganizationUserStatusType userStatus, Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser] OrganizationUser organizationUser, SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
organizationUser.Status = userStatus;
|
||||||
|
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||||
|
|
||||||
|
Assert.Contains("already active", exception.Message.ToLowerInvariant());
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
await sutProvider.GetDependency<IEventService>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUser_WithOtherOrganizationSingleOrgPolicyEnabled_Fails(
|
||||||
|
Organization organization,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||||
|
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
||||||
|
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.AnyPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
var user = new User();
|
||||||
|
user.Email = "test@bitwarden.com";
|
||||||
|
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||||
|
|
||||||
|
Assert.Contains("test@bitwarden.com belongs to an organization that doesn't allow them to join multiple organizations", exception.Message.ToLowerInvariant());
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
await sutProvider.GetDependency<IEventService>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUser_With2FAPolicyEnabled_WithoutUser2FAConfigured_Fails(
|
||||||
|
Organization organization,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||||
|
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
organizationUser.Email = null;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||||
|
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
|
||||||
|
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, false) });
|
||||||
|
|
||||||
|
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
||||||
|
.Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } });
|
||||||
|
|
||||||
|
var user = new User();
|
||||||
|
user.Email = "test@bitwarden.com";
|
||||||
|
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||||
|
|
||||||
|
Assert.Contains("test@bitwarden.com is not compliant with the two-step login policy", exception.Message.ToLowerInvariant());
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
await sutProvider.GetDependency<IEventService>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUser_With2FAPolicyEnabled_WithUser2FAConfigured_Success(
|
||||||
|
Organization organization,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||||
|
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
||||||
|
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
||||||
|
.Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } });
|
||||||
|
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||||
|
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
|
||||||
|
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) });
|
||||||
|
|
||||||
|
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed);
|
||||||
|
await sutProvider.GetDependency<IEventService>()
|
||||||
|
.Received(1)
|
||||||
|
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUser_WithSingleOrgPolicyEnabled_Fails(
|
||||||
|
Organization organization,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser,
|
||||||
|
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
||||||
|
secondOrganizationUser.UserId = organizationUser.UserId;
|
||||||
|
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyByUserAsync(organizationUser.UserId.Value)
|
||||||
|
.Returns(new[] { organizationUser, secondOrganizationUser });
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
|
||||||
|
.Returns(new[]
|
||||||
|
{
|
||||||
|
new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.SingleOrg, OrganizationUserStatus = OrganizationUserStatusType.Revoked }
|
||||||
|
});
|
||||||
|
|
||||||
|
var user = new User();
|
||||||
|
user.Email = "test@bitwarden.com";
|
||||||
|
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||||
|
|
||||||
|
Assert.Contains("test@bitwarden.com is not compliant with the single organization policy", exception.Message.ToLowerInvariant());
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
await sutProvider.GetDependency<IEventService>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUser_vNext_WithOtherOrganizationSingleOrgPolicyEnabled_Fails(
|
||||||
|
Organization organization,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser,
|
||||||
|
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
||||||
|
secondOrganizationUser.UserId = organizationUser.UserId;
|
||||||
|
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||||
|
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
|
||||||
|
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)> { (organizationUser.UserId.Value, true) });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.AnyPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
var user = new User { Email = "test@bitwarden.com" };
|
||||||
|
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||||
|
|
||||||
|
Assert.Contains("test@bitwarden.com belongs to an organization that doesn't allow them to join multiple organizations", exception.Message.ToLowerInvariant());
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
await sutProvider.GetDependency<IEventService>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUser_WithSingleOrgPolicyEnabled_And_2FA_Policy_Fails(
|
||||||
|
Organization organization,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser,
|
||||||
|
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
||||||
|
secondOrganizationUser.UserId = organizationUser.UserId;
|
||||||
|
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyByUserAsync(organizationUser.UserId.Value)
|
||||||
|
.Returns(new[] { organizationUser, secondOrganizationUser });
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
|
||||||
|
.Returns(new[]
|
||||||
|
{
|
||||||
|
new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.SingleOrg, OrganizationUserStatus = OrganizationUserStatusType.Revoked }
|
||||||
|
});
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
||||||
|
.Returns([
|
||||||
|
new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication, OrganizationUserStatus = OrganizationUserStatusType.Revoked }
|
||||||
|
]);
|
||||||
|
|
||||||
|
var user = new User { Email = "test@bitwarden.com" };
|
||||||
|
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||||
|
|
||||||
|
Assert.Contains("test@bitwarden.com is not compliant with the single organization and two-step login policy", exception.Message.ToLowerInvariant());
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
await sutProvider.GetDependency<IEventService>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUser_vNext_With2FAPolicyEnabled_WithoutUser2FAConfigured_Fails(
|
||||||
|
Organization organization,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||||
|
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
organizationUser.Email = null;
|
||||||
|
|
||||||
|
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
||||||
|
.Returns([new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication }
|
||||||
|
]);
|
||||||
|
|
||||||
|
var user = new User { Email = "test@bitwarden.com" };
|
||||||
|
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||||
|
|
||||||
|
Assert.Contains("test@bitwarden.com is not compliant with the two-step login policy", exception.Message.ToLowerInvariant());
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
||||||
|
await sutProvider.GetDependency<IEventService>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>()
|
||||||
|
.DidNotReceiveWithAnyArgs()
|
||||||
|
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUser_vNext_With2FAPolicyEnabled_WithUser2FAConfigured_Success(
|
||||||
|
Organization organization,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||||
|
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
||||||
|
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
||||||
|
.Returns([new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication }
|
||||||
|
]);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||||
|
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
|
||||||
|
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)> { (organizationUser.UserId.Value, true) });
|
||||||
|
|
||||||
|
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed);
|
||||||
|
await sutProvider.GetDependency<IEventService>()
|
||||||
|
.Received(1)
|
||||||
|
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUser_WhenUserOwningAnotherFreeOrganization_ThenRestoreUserFails(
|
||||||
|
Organization organization,
|
||||||
|
Organization otherOrganization,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUserOwnerFromDifferentOrg,
|
||||||
|
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
||||||
|
|
||||||
|
orgUserOwnerFromDifferentOrg.UserId = organizationUser.UserId;
|
||||||
|
otherOrganization.Id = orgUserOwnerFromDifferentOrg.OrganizationId;
|
||||||
|
otherOrganization.PlanType = PlanType.Free;
|
||||||
|
|
||||||
|
RestoreUser_Setup(organization, owner, organizationUser, sutProvider);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyByUserAsync(organizationUser.UserId.Value)
|
||||||
|
.Returns([orgUserOwnerFromDifferentOrg]);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetManyByUserIdAsync(organizationUser.UserId.Value)
|
||||||
|
.Returns([otherOrganization]);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
||||||
|
.Returns([new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication }
|
||||||
|
]);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||||
|
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
|
||||||
|
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)> { (organizationUser.UserId.Value, true) });
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
||||||
|
|
||||||
|
Assert.Equal("User is an owner/admin of another free organization. Please have them upgrade to a paid plan to restore their account.", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUsers_Success(Organization organization,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser1,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2,
|
||||||
|
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
RestoreUser_Setup(organization, owner, orgUser1, sutProvider);
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var eventService = sutProvider.GetDependency<IEventService>();
|
||||||
|
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
||||||
|
var userService = Substitute.For<IUserService>();
|
||||||
|
|
||||||
|
orgUser1.Email = orgUser2.Email = null; // Mock that users were previously confirmed
|
||||||
|
orgUser1.OrganizationId = orgUser2.OrganizationId = organization.Id;
|
||||||
|
organizationUserRepository
|
||||||
|
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id)))
|
||||||
|
.Returns([orgUser1, orgUser2]);
|
||||||
|
|
||||||
|
twoFactorIsEnabledQuery
|
||||||
|
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.UserId!.Value) && ids.Contains(orgUser2.UserId!.Value)))
|
||||||
|
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>
|
||||||
|
{
|
||||||
|
(orgUser1.UserId!.Value, true),
|
||||||
|
(orgUser2.UserId!.Value, false)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, new[] { orgUser1.Id, orgUser2.Id }, owner.Id, userService);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, result.Count);
|
||||||
|
Assert.All(result, r => Assert.Empty(r.Item2)); // No error messages
|
||||||
|
await organizationUserRepository
|
||||||
|
.Received(1)
|
||||||
|
.RestoreAsync(orgUser1.Id, OrganizationUserStatusType.Confirmed);
|
||||||
|
await organizationUserRepository
|
||||||
|
.Received(1)
|
||||||
|
.RestoreAsync(orgUser2.Id, OrganizationUserStatusType.Confirmed);
|
||||||
|
await eventService.Received(1)
|
||||||
|
.LogOrganizationUserEventAsync(orgUser1, EventType.OrganizationUser_Restored);
|
||||||
|
await eventService.Received(1)
|
||||||
|
.LogOrganizationUserEventAsync(orgUser2, EventType.OrganizationUser_Restored);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUsers_With2FAPolicy_BlocksNonCompliantUser(Organization organization,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser1,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser3,
|
||||||
|
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
RestoreUser_Setup(organization, owner, orgUser1, sutProvider);
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
|
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||||
|
var userService = Substitute.For<IUserService>();
|
||||||
|
|
||||||
|
orgUser1.Email = orgUser2.Email = null;
|
||||||
|
orgUser3.UserId = null;
|
||||||
|
orgUser3.Key = null;
|
||||||
|
orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = organization.Id;
|
||||||
|
organizationUserRepository
|
||||||
|
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id) && ids.Contains(orgUser3.Id)))
|
||||||
|
.Returns(new[] { orgUser1, orgUser2, orgUser3 });
|
||||||
|
|
||||||
|
userRepository.GetByIdAsync(orgUser2.UserId!.Value).Returns(new User { Email = "test@example.com" });
|
||||||
|
|
||||||
|
// Setup 2FA policy
|
||||||
|
policyService.GetPoliciesApplicableToUserAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
||||||
|
.Returns([new OrganizationUserPolicyDetails { OrganizationId = organization.Id, PolicyType = PolicyType.TwoFactorAuthentication }]);
|
||||||
|
|
||||||
|
// User1 has 2FA, User2 doesn't
|
||||||
|
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||||
|
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.UserId!.Value) && ids.Contains(orgUser2.UserId!.Value)))
|
||||||
|
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>
|
||||||
|
{
|
||||||
|
(orgUser1.UserId!.Value, true),
|
||||||
|
(orgUser2.UserId!.Value, false)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, [orgUser1.Id, orgUser2.Id, orgUser3.Id], owner.Id, userService);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(3, result.Count);
|
||||||
|
Assert.Empty(result[0].Item2); // First user should succeed
|
||||||
|
Assert.Contains("two-step login", result[1].Item2); // Second user should fail
|
||||||
|
Assert.Empty(result[2].Item2); // Third user should succeed
|
||||||
|
await organizationUserRepository
|
||||||
|
.Received(1)
|
||||||
|
.RestoreAsync(orgUser1.Id, OrganizationUserStatusType.Confirmed);
|
||||||
|
await organizationUserRepository
|
||||||
|
.DidNotReceive()
|
||||||
|
.RestoreAsync(orgUser2.Id, Arg.Any<OrganizationUserStatusType>());
|
||||||
|
await organizationUserRepository
|
||||||
|
.Received(1)
|
||||||
|
.RestoreAsync(orgUser3.Id, OrganizationUserStatusType.Invited);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task RestoreUsers_UserOwnsAnotherFreeOrganization_BlocksOwnerUserFromBeingRestored(Organization organization,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser1,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser3,
|
||||||
|
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUserFromOtherOrg,
|
||||||
|
Organization otherOrganization,
|
||||||
|
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
RestoreUser_Setup(organization, owner, orgUser1, sutProvider);
|
||||||
|
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
||||||
|
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
||||||
|
var policyService = sutProvider.GetDependency<IPolicyService>();
|
||||||
|
var userService = Substitute.For<IUserService>();
|
||||||
|
|
||||||
|
orgUser1.Email = orgUser2.Email = null;
|
||||||
|
orgUser3.UserId = null;
|
||||||
|
orgUser3.Key = null;
|
||||||
|
orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = organization.Id;
|
||||||
|
|
||||||
|
orgUserFromOtherOrg.UserId = orgUser1.UserId;
|
||||||
|
otherOrganization.Id = orgUserFromOtherOrg.OrganizationId;
|
||||||
|
otherOrganization.PlanType = PlanType.Free;
|
||||||
|
|
||||||
|
organizationUserRepository
|
||||||
|
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id) && ids.Contains(orgUser3.Id)))
|
||||||
|
.Returns(new[] { orgUser1, orgUser2, orgUser3 });
|
||||||
|
|
||||||
|
userRepository.GetByIdAsync(orgUser2.UserId!.Value).Returns(new User { Email = "test@example.com" });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationUserRepository>()
|
||||||
|
.GetManyByManyUsersAsync(Arg.Any<IEnumerable<Guid>>())
|
||||||
|
.Returns([orgUserFromOtherOrg]);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>()
|
||||||
|
.GetManyByIdsAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUserFromOtherOrg.OrganizationId)))
|
||||||
|
.Returns([otherOrganization]);
|
||||||
|
|
||||||
|
|
||||||
|
// Setup 2FA policy
|
||||||
|
policyService.GetPoliciesApplicableToUserAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
||||||
|
.Returns([new OrganizationUserPolicyDetails { OrganizationId = organization.Id, PolicyType = PolicyType.TwoFactorAuthentication }]);
|
||||||
|
|
||||||
|
// User1 has 2FA, User2 doesn't
|
||||||
|
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
||||||
|
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.UserId!.Value) && ids.Contains(orgUser2.UserId!.Value)))
|
||||||
|
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>
|
||||||
|
{
|
||||||
|
(orgUser1.UserId!.Value, true),
|
||||||
|
(orgUser2.UserId!.Value, false)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, [orgUser1.Id, orgUser2.Id, orgUser3.Id], owner.Id, userService);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(3, result.Count);
|
||||||
|
Assert.Contains("owner", result[0].Item2); // Owner should fail
|
||||||
|
await organizationUserRepository
|
||||||
|
.DidNotReceive()
|
||||||
|
.RestoreAsync(orgUser1.Id, OrganizationUserStatusType.Confirmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void RestoreUser_Setup(
|
||||||
|
Organization organization,
|
||||||
|
OrganizationUser? requestingOrganizationUser,
|
||||||
|
OrganizationUser targetOrganizationUser,
|
||||||
|
SutProvider<RestoreOrganizationUserCommand> sutProvider)
|
||||||
|
{
|
||||||
|
if (requestingOrganizationUser != null)
|
||||||
|
{
|
||||||
|
requestingOrganizationUser.OrganizationId = organization.Id;
|
||||||
|
}
|
||||||
|
targetOrganizationUser.OrganizationId = organization.Id;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IOrganizationRepository>().GetByIdAsync(organization.Id).Returns(organization);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().OrganizationOwner(organization.Id).Returns(requestingOrganizationUser != null && requestingOrganizationUser.Type is OrganizationUserType.Owner);
|
||||||
|
sutProvider.GetDependency<ICurrentContext>().ManageUsers(organization.Id).Returns(requestingOrganizationUser != null && (requestingOrganizationUser.Type is OrganizationUserType.Owner or OrganizationUserType.Admin));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
|
using Bit.Core.Test.AdminConsole.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture;
|
||||||
|
using Bit.Test.Common.AutoFixture.Attributes;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
|
|
||||||
|
[SutProviderCustomize]
|
||||||
|
public class PersonalOwnershipPolicyRequirementFactoryTests
|
||||||
|
{
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void DisablePersonalOwnership_WithNoPolicies_ReturnsFalse(SutProvider<PersonalOwnershipPolicyRequirementFactory> sutProvider)
|
||||||
|
{
|
||||||
|
var actual = sutProvider.Sut.Create([]);
|
||||||
|
|
||||||
|
Assert.False(actual.DisablePersonalOwnership);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public void DisablePersonalOwnership_WithPersonalOwnershipPolicies_ReturnsTrue(
|
||||||
|
[PolicyDetails(PolicyType.PersonalOwnership)] PolicyDetails[] policies,
|
||||||
|
SutProvider<PersonalOwnershipPolicyRequirementFactory> sutProvider)
|
||||||
|
{
|
||||||
|
var actual = sutProvider.Sut.Create(policies);
|
||||||
|
|
||||||
|
Assert.True(actual.DisablePersonalOwnership);
|
||||||
|
}
|
||||||
|
}
|
@ -1,14 +1,11 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Bit.Core.AdminConsole.Entities.Provider;
|
using Bit.Core.AdminConsole.Entities.Provider;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
|
||||||
using Bit.Core.AdminConsole.Enums.Provider;
|
using Bit.Core.AdminConsole.Enums.Provider;
|
||||||
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
|
||||||
using Bit.Core.AdminConsole.Repositories;
|
using Bit.Core.AdminConsole.Repositories;
|
||||||
using Bit.Core.AdminConsole.Services;
|
|
||||||
using Bit.Core.Auth.Entities;
|
using Bit.Core.Auth.Entities;
|
||||||
using Bit.Core.Auth.Models.Business.Tokenables;
|
using Bit.Core.Auth.Models.Business.Tokenables;
|
||||||
using Bit.Core.Auth.Repositories;
|
using Bit.Core.Auth.Repositories;
|
||||||
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
|
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Billing.Pricing;
|
using Bit.Core.Billing.Pricing;
|
||||||
using Bit.Core.Context;
|
using Bit.Core.Context;
|
||||||
@ -1233,451 +1230,6 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
.PushSyncOrgKeysAsync(organizationUser.UserId!.Value);
|
.PushSyncOrgKeysAsync(organizationUser.UserId!.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task RestoreUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
|
||||||
|
|
||||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
|
||||||
.Received(1)
|
|
||||||
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited);
|
|
||||||
await sutProvider.GetDependency<IEventService>()
|
|
||||||
.Received(1)
|
|
||||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task RestoreUser_WithPushSyncOrgKeysOnRevokeRestoreEnabled_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
|
||||||
.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore)
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
|
||||||
.Received(1)
|
|
||||||
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited);
|
|
||||||
await sutProvider.GetDependency<IEventService>()
|
|
||||||
.Received(1)
|
|
||||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>()
|
|
||||||
.Received(1)
|
|
||||||
.PushSyncOrgKeysAsync(organizationUser.UserId!.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task RestoreUser_WithEventSystemUser_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
RestoreRevokeUser_Setup(organization, null, organizationUser, sutProvider);
|
|
||||||
|
|
||||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, eventSystemUser);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
|
||||||
.Received(1)
|
|
||||||
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited);
|
|
||||||
await sutProvider.GetDependency<IEventService>()
|
|
||||||
.Received(1)
|
|
||||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored, eventSystemUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task RestoreUser_WithEventSystemUser_WithPushSyncOrgKeysOnRevokeRestoreEnabled_Success(Organization organization, [OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, EventSystemUser eventSystemUser, SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
RestoreRevokeUser_Setup(organization, null, organizationUser, sutProvider);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IFeatureService>()
|
|
||||||
.IsEnabled(FeatureFlagKeys.PushSyncOrgKeysOnRevokeRestore)
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, eventSystemUser);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
|
||||||
.Received(1)
|
|
||||||
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Invited);
|
|
||||||
await sutProvider.GetDependency<IEventService>()
|
|
||||||
.Received(1)
|
|
||||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored, eventSystemUser);
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>()
|
|
||||||
.Received(1)
|
|
||||||
.PushSyncOrgKeysAsync(organizationUser.UserId!.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task RestoreUser_RestoreThemselves_Fails(Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser, SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
organizationUser.UserId = owner.Id;
|
|
||||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
|
||||||
|
|
||||||
Assert.Contains("you cannot restore yourself", exception.Message.ToLowerInvariant());
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
|
||||||
await sutProvider.GetDependency<IEventService>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData(OrganizationUserType.Admin)]
|
|
||||||
[BitAutoData(OrganizationUserType.Custom)]
|
|
||||||
public async Task RestoreUser_AdminRestoreOwner_Fails(OrganizationUserType restoringUserType,
|
|
||||||
Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed)] OrganizationUser restoringUser,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Revoked, OrganizationUserType.Owner)] OrganizationUser organizationUser, SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
restoringUser.Type = restoringUserType;
|
|
||||||
RestoreRevokeUser_Setup(organization, restoringUser, organizationUser, sutProvider);
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, restoringUser.Id));
|
|
||||||
|
|
||||||
Assert.Contains("only owners can restore other owners", exception.Message.ToLowerInvariant());
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
|
||||||
await sutProvider.GetDependency<IEventService>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
|
||||||
[BitAutoData(OrganizationUserStatusType.Invited)]
|
|
||||||
[BitAutoData(OrganizationUserStatusType.Accepted)]
|
|
||||||
[BitAutoData(OrganizationUserStatusType.Confirmed)]
|
|
||||||
public async Task RestoreUser_WithStatusOtherThanRevoked_Fails(OrganizationUserStatusType userStatus, Organization organization, [OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
|
||||||
[OrganizationUser] OrganizationUser organizationUser, SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
organizationUser.Status = userStatus;
|
|
||||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
|
||||||
|
|
||||||
Assert.Contains("already active", exception.Message.ToLowerInvariant());
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
|
||||||
await sutProvider.GetDependency<IEventService>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task RestoreUser_WithOtherOrganizationSingleOrgPolicyEnabled_Fails(
|
|
||||||
Organization organization,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
|
||||||
SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
|
||||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IPolicyService>()
|
|
||||||
.AnyPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
var user = new User();
|
|
||||||
user.Email = "test@bitwarden.com";
|
|
||||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
|
||||||
|
|
||||||
Assert.Contains("test@bitwarden.com belongs to an organization that doesn't allow them to join multiple organizations", exception.Message.ToLowerInvariant());
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
|
||||||
await sutProvider.GetDependency<IEventService>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task RestoreUser_With2FAPolicyEnabled_WithoutUser2FAConfigured_Fails(
|
|
||||||
Organization organization,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
|
||||||
SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
organizationUser.Email = null;
|
|
||||||
|
|
||||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
|
||||||
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
|
|
||||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, false) });
|
|
||||||
|
|
||||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IPolicyService>()
|
|
||||||
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
|
||||||
.Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } });
|
|
||||||
|
|
||||||
var user = new User();
|
|
||||||
user.Email = "test@bitwarden.com";
|
|
||||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
|
||||||
|
|
||||||
Assert.Contains("test@bitwarden.com is not compliant with the two-step login policy", exception.Message.ToLowerInvariant());
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
|
||||||
await sutProvider.GetDependency<IEventService>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task RestoreUser_With2FAPolicyEnabled_WithUser2FAConfigured_Success(
|
|
||||||
Organization organization,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
|
||||||
SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
|
||||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IPolicyService>()
|
|
||||||
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
|
||||||
.Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } });
|
|
||||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
|
||||||
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
|
|
||||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) });
|
|
||||||
|
|
||||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
|
||||||
.Received(1)
|
|
||||||
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed);
|
|
||||||
await sutProvider.GetDependency<IEventService>()
|
|
||||||
.Received(1)
|
|
||||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task RestoreUser_WithSingleOrgPolicyEnabled_Fails(
|
|
||||||
Organization organization,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser,
|
|
||||||
SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
|
||||||
secondOrganizationUser.UserId = organizationUser.UserId;
|
|
||||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
|
||||||
.GetManyByUserAsync(organizationUser.UserId.Value)
|
|
||||||
.Returns(new[] { organizationUser, secondOrganizationUser });
|
|
||||||
sutProvider.GetDependency<IPolicyService>()
|
|
||||||
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
|
|
||||||
.Returns(new[]
|
|
||||||
{
|
|
||||||
new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.SingleOrg, OrganizationUserStatus = OrganizationUserStatusType.Revoked }
|
|
||||||
});
|
|
||||||
|
|
||||||
var user = new User();
|
|
||||||
user.Email = "test@bitwarden.com";
|
|
||||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
|
||||||
|
|
||||||
Assert.Contains("test@bitwarden.com is not compliant with the single organization policy", exception.Message.ToLowerInvariant());
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
|
||||||
await sutProvider.GetDependency<IEventService>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task RestoreUser_vNext_WithOtherOrganizationSingleOrgPolicyEnabled_Fails(
|
|
||||||
Organization organization,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser,
|
|
||||||
SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
|
||||||
secondOrganizationUser.UserId = organizationUser.UserId;
|
|
||||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
|
||||||
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
|
|
||||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) });
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IPolicyService>()
|
|
||||||
.AnyPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
var user = new User();
|
|
||||||
user.Email = "test@bitwarden.com";
|
|
||||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
|
||||||
|
|
||||||
Assert.Contains("test@bitwarden.com belongs to an organization that doesn't allow them to join multiple organizations", exception.Message.ToLowerInvariant());
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
|
||||||
await sutProvider.GetDependency<IEventService>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task RestoreUser_WithSingleOrgPolicyEnabled_And_2FA_Policy_Fails(
|
|
||||||
Organization organization,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Accepted)] OrganizationUser secondOrganizationUser,
|
|
||||||
SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
|
||||||
secondOrganizationUser.UserId = organizationUser.UserId;
|
|
||||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IOrganizationUserRepository>()
|
|
||||||
.GetManyByUserAsync(organizationUser.UserId.Value)
|
|
||||||
.Returns(new[] { organizationUser, secondOrganizationUser });
|
|
||||||
sutProvider.GetDependency<IPolicyService>()
|
|
||||||
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.SingleOrg, Arg.Any<OrganizationUserStatusType>())
|
|
||||||
.Returns(new[]
|
|
||||||
{
|
|
||||||
new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.SingleOrg, OrganizationUserStatus = OrganizationUserStatusType.Revoked }
|
|
||||||
});
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IPolicyService>()
|
|
||||||
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
|
||||||
.Returns(new[]
|
|
||||||
{
|
|
||||||
new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication, OrganizationUserStatus = OrganizationUserStatusType.Revoked }
|
|
||||||
});
|
|
||||||
|
|
||||||
var user = new User();
|
|
||||||
user.Email = "test@bitwarden.com";
|
|
||||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
|
||||||
|
|
||||||
Assert.Contains("test@bitwarden.com is not compliant with the single organization and two-step login polciy", exception.Message.ToLowerInvariant());
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
|
||||||
await sutProvider.GetDependency<IEventService>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task RestoreUser_vNext_With2FAPolicyEnabled_WithoutUser2FAConfigured_Fails(
|
|
||||||
Organization organization,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
|
||||||
SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
organizationUser.Email = null;
|
|
||||||
|
|
||||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IPolicyService>()
|
|
||||||
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
|
||||||
.Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } });
|
|
||||||
|
|
||||||
var user = new User();
|
|
||||||
user.Email = "test@bitwarden.com";
|
|
||||||
sutProvider.GetDependency<IUserRepository>().GetByIdAsync(organizationUser.UserId.Value).Returns(user);
|
|
||||||
|
|
||||||
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
|
||||||
() => sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id));
|
|
||||||
|
|
||||||
Assert.Contains("test@bitwarden.com is not compliant with the two-step login policy", exception.Message.ToLowerInvariant());
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.RestoreAsync(Arg.Any<Guid>(), Arg.Any<OrganizationUserStatusType>());
|
|
||||||
await sutProvider.GetDependency<IEventService>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.LogOrganizationUserEventAsync(Arg.Any<OrganizationUser>(), Arg.Any<EventType>(), Arg.Any<EventSystemUser>());
|
|
||||||
await sutProvider.GetDependency<IPushNotificationService>()
|
|
||||||
.DidNotReceiveWithAnyArgs()
|
|
||||||
.PushSyncOrgKeysAsync(Arg.Any<Guid>());
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task RestoreUser_vNext_With2FAPolicyEnabled_WithUser2FAConfigured_Success(
|
|
||||||
Organization organization,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser organizationUser,
|
|
||||||
SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
organizationUser.Email = null; // this is required to mock that the user as had already been confirmed before the revoke
|
|
||||||
RestoreRevokeUser_Setup(organization, owner, organizationUser, sutProvider);
|
|
||||||
|
|
||||||
sutProvider.GetDependency<IPolicyService>()
|
|
||||||
.GetPoliciesApplicableToUserAsync(organizationUser.UserId.Value, PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
|
||||||
.Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organizationUser.OrganizationId, PolicyType = PolicyType.TwoFactorAuthentication } });
|
|
||||||
|
|
||||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
|
||||||
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.UserId.Value)))
|
|
||||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>() { (organizationUser.UserId.Value, true) });
|
|
||||||
|
|
||||||
await sutProvider.Sut.RestoreUserAsync(organizationUser, owner.Id);
|
|
||||||
|
|
||||||
await sutProvider.GetDependency<IOrganizationUserRepository>()
|
|
||||||
.Received(1)
|
|
||||||
.RestoreAsync(organizationUser.Id, OrganizationUserStatusType.Confirmed);
|
|
||||||
await sutProvider.GetDependency<IEventService>()
|
|
||||||
.Received(1)
|
|
||||||
.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData(PlanType.TeamsAnnually)]
|
[BitAutoData(PlanType.TeamsAnnually)]
|
||||||
[BitAutoData(PlanType.TeamsMonthly)]
|
[BitAutoData(PlanType.TeamsMonthly)]
|
||||||
@ -1991,107 +1543,4 @@ OrganizationUserInvite invite, SutProvider<OrganizationService> sutProvider)
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task RestoreUsers_Success(Organization organization,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser1,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2,
|
|
||||||
SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
RestoreRevokeUser_Setup(organization, owner, orgUser1, sutProvider);
|
|
||||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
|
||||||
var eventService = sutProvider.GetDependency<IEventService>();
|
|
||||||
var twoFactorIsEnabledQuery = sutProvider.GetDependency<ITwoFactorIsEnabledQuery>();
|
|
||||||
var userService = Substitute.For<IUserService>();
|
|
||||||
|
|
||||||
orgUser1.Email = orgUser2.Email = null; // Mock that users were previously confirmed
|
|
||||||
orgUser1.OrganizationId = orgUser2.OrganizationId = organization.Id;
|
|
||||||
organizationUserRepository
|
|
||||||
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id)))
|
|
||||||
.Returns(new[] { orgUser1, orgUser2 });
|
|
||||||
|
|
||||||
twoFactorIsEnabledQuery
|
|
||||||
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.UserId!.Value) && ids.Contains(orgUser2.UserId!.Value)))
|
|
||||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>
|
|
||||||
{
|
|
||||||
(orgUser1.UserId!.Value, true),
|
|
||||||
(orgUser2.UserId!.Value, false)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, new[] { orgUser1.Id, orgUser2.Id }, owner.Id, userService);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(2, result.Count);
|
|
||||||
Assert.All(result, r => Assert.Empty(r.Item2)); // No error messages
|
|
||||||
await organizationUserRepository
|
|
||||||
.Received(1)
|
|
||||||
.RestoreAsync(orgUser1.Id, OrganizationUserStatusType.Confirmed);
|
|
||||||
await organizationUserRepository
|
|
||||||
.Received(1)
|
|
||||||
.RestoreAsync(orgUser2.Id, OrganizationUserStatusType.Confirmed);
|
|
||||||
await eventService.Received(1)
|
|
||||||
.LogOrganizationUserEventAsync(orgUser1, EventType.OrganizationUser_Restored);
|
|
||||||
await eventService.Received(1)
|
|
||||||
.LogOrganizationUserEventAsync(orgUser2, EventType.OrganizationUser_Restored);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
|
||||||
public async Task RestoreUsers_With2FAPolicy_BlocksNonCompliantUser(Organization organization,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser1,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser2,
|
|
||||||
[OrganizationUser(OrganizationUserStatusType.Revoked)] OrganizationUser orgUser3,
|
|
||||||
SutProvider<OrganizationService> sutProvider)
|
|
||||||
{
|
|
||||||
// Arrange
|
|
||||||
RestoreRevokeUser_Setup(organization, owner, orgUser1, sutProvider);
|
|
||||||
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
|
|
||||||
var userRepository = sutProvider.GetDependency<IUserRepository>();
|
|
||||||
var policyService = sutProvider.GetDependency<IPolicyService>();
|
|
||||||
var userService = Substitute.For<IUserService>();
|
|
||||||
|
|
||||||
orgUser1.Email = orgUser2.Email = null;
|
|
||||||
orgUser3.UserId = null;
|
|
||||||
orgUser3.Key = null;
|
|
||||||
orgUser1.OrganizationId = orgUser2.OrganizationId = orgUser3.OrganizationId = organization.Id;
|
|
||||||
organizationUserRepository
|
|
||||||
.GetManyAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.Id) && ids.Contains(orgUser2.Id) && ids.Contains(orgUser3.Id)))
|
|
||||||
.Returns(new[] { orgUser1, orgUser2, orgUser3 });
|
|
||||||
|
|
||||||
userRepository.GetByIdAsync(orgUser2.UserId!.Value).Returns(new User { Email = "test@example.com" });
|
|
||||||
|
|
||||||
// Setup 2FA policy
|
|
||||||
policyService.GetPoliciesApplicableToUserAsync(Arg.Any<Guid>(), PolicyType.TwoFactorAuthentication, Arg.Any<OrganizationUserStatusType>())
|
|
||||||
.Returns(new[] { new OrganizationUserPolicyDetails { OrganizationId = organization.Id, PolicyType = PolicyType.TwoFactorAuthentication } });
|
|
||||||
|
|
||||||
// User1 has 2FA, User2 doesn't
|
|
||||||
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
|
|
||||||
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUser1.UserId!.Value) && ids.Contains(orgUser2.UserId!.Value)))
|
|
||||||
.Returns(new List<(Guid userId, bool twoFactorIsEnabled)>
|
|
||||||
{
|
|
||||||
(orgUser1.UserId!.Value, true),
|
|
||||||
(orgUser2.UserId!.Value, false)
|
|
||||||
});
|
|
||||||
|
|
||||||
// Act
|
|
||||||
var result = await sutProvider.Sut.RestoreUsersAsync(organization.Id, new[] { orgUser1.Id, orgUser2.Id, orgUser3.Id }, owner.Id, userService);
|
|
||||||
|
|
||||||
// Assert
|
|
||||||
Assert.Equal(3, result.Count);
|
|
||||||
Assert.Empty(result[0].Item2); // First user should succeed
|
|
||||||
Assert.Contains("two-step login", result[1].Item2); // Second user should fail
|
|
||||||
Assert.Empty(result[2].Item2); // Third user should succeed
|
|
||||||
await organizationUserRepository
|
|
||||||
.Received(1)
|
|
||||||
.RestoreAsync(orgUser1.Id, OrganizationUserStatusType.Confirmed);
|
|
||||||
await organizationUserRepository
|
|
||||||
.DidNotReceive()
|
|
||||||
.RestoreAsync(orgUser2.Id, Arg.Any<OrganizationUserStatusType>());
|
|
||||||
await organizationUserRepository
|
|
||||||
.Received(1)
|
|
||||||
.RestoreAsync(orgUser3.Id, OrganizationUserStatusType.Invited);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
using Bit.Core.AdminConsole.Enums;
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
using Bit.Core.AdminConsole.Services;
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Exceptions;
|
using Bit.Core.Exceptions;
|
||||||
using Bit.Core.Platform.Push;
|
using Bit.Core.Platform.Push;
|
||||||
using Bit.Core.Repositories;
|
using Bit.Core.Repositories;
|
||||||
|
using Bit.Core.Services;
|
||||||
using Bit.Core.Test.AutoFixture.CipherFixtures;
|
using Bit.Core.Test.AutoFixture.CipherFixtures;
|
||||||
using Bit.Core.Tools.Enums;
|
using Bit.Core.Tools.Enums;
|
||||||
using Bit.Core.Tools.ImportFeatures;
|
using Bit.Core.Tools.ImportFeatures;
|
||||||
@ -18,7 +21,6 @@ using Bit.Test.Common.AutoFixture.Attributes;
|
|||||||
using NSubstitute;
|
using NSubstitute;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
|
|
||||||
namespace Bit.Core.Test.Tools.ImportFeatures;
|
namespace Bit.Core.Test.Tools.ImportFeatures;
|
||||||
|
|
||||||
[UserCipherCustomize]
|
[UserCipherCustomize]
|
||||||
@ -51,6 +53,34 @@ public class ImportCiphersAsyncCommandTests
|
|||||||
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ImportIntoIndividualVaultAsync_WithPolicyRequirementsEnabled_WithDisablePersonalOwnershipPolicyDisabled_Success(
|
||||||
|
Guid importingUserId,
|
||||||
|
List<CipherDetails> ciphers,
|
||||||
|
SutProvider<ImportCiphersCommand> sutProvider)
|
||||||
|
{
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||||
|
.GetAsync<PersonalOwnershipPolicyRequirement>(importingUserId)
|
||||||
|
.Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = false });
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFolderRepository>()
|
||||||
|
.GetManyByUserIdAsync(importingUserId)
|
||||||
|
.Returns(new List<Folder>());
|
||||||
|
|
||||||
|
var folders = new List<Folder> { new Folder { UserId = importingUserId } };
|
||||||
|
|
||||||
|
var folderRelationships = new List<KeyValuePair<int, int>>();
|
||||||
|
|
||||||
|
await sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, importingUserId);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<ICipherRepository>().Received(1).CreateAsync(ciphers, Arg.Any<List<Folder>>());
|
||||||
|
await sutProvider.GetDependency<IPushNotificationService>().Received(1).PushSyncVaultAsync(importingUserId);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task ImportIntoIndividualVaultAsync_ThrowsBadRequestException(
|
public async Task ImportIntoIndividualVaultAsync_ThrowsBadRequestException(
|
||||||
List<Folder> folders,
|
List<Folder> folders,
|
||||||
@ -73,6 +103,32 @@ public class ImportCiphersAsyncCommandTests
|
|||||||
Assert.Equal("You cannot import items into your personal vault because you are a member of an organization which forbids it.", exception.Message);
|
Assert.Equal("You cannot import items into your personal vault because you are a member of an organization which forbids it.", exception.Message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory, BitAutoData]
|
||||||
|
public async Task ImportIntoIndividualVaultAsync_WithPolicyRequirementsEnabled_WithDisablePersonalOwnershipPolicyEnabled_ThrowsBadRequestException(
|
||||||
|
List<Folder> folders,
|
||||||
|
List<CipherDetails> ciphers,
|
||||||
|
SutProvider<ImportCiphersCommand> sutProvider)
|
||||||
|
{
|
||||||
|
var userId = Guid.NewGuid();
|
||||||
|
folders.ForEach(f => f.UserId = userId);
|
||||||
|
ciphers.ForEach(c => c.UserId = userId);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||||
|
.GetAsync<PersonalOwnershipPolicyRequirement>(userId)
|
||||||
|
.Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = true });
|
||||||
|
|
||||||
|
var folderRelationships = new List<KeyValuePair<int, int>>();
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(() =>
|
||||||
|
sutProvider.Sut.ImportIntoIndividualVaultAsync(folders, ciphers, folderRelationships, userId));
|
||||||
|
|
||||||
|
Assert.Equal("You cannot import items into your personal vault because you are a member of an organization which forbids it.", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory, BitAutoData]
|
[Theory, BitAutoData]
|
||||||
public async Task ImportIntoOrganizationalVaultAsync_Success(
|
public async Task ImportIntoOrganizationalVaultAsync_Success(
|
||||||
Organization organization,
|
Organization organization,
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using Bit.Core.AdminConsole.Entities;
|
using Bit.Core.AdminConsole.Entities;
|
||||||
|
using Bit.Core.AdminConsole.Enums;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies;
|
||||||
|
using Bit.Core.AdminConsole.OrganizationFeatures.Policies.PolicyRequirements;
|
||||||
|
using Bit.Core.AdminConsole.Services;
|
||||||
using Bit.Core.Billing.Enums;
|
using Bit.Core.Billing.Enums;
|
||||||
using Bit.Core.Entities;
|
using Bit.Core.Entities;
|
||||||
using Bit.Core.Enums;
|
using Bit.Core.Enums;
|
||||||
@ -107,6 +111,98 @@ public class CipherServiceTests
|
|||||||
await sutProvider.GetDependency<ICipherRepository>().Received(1).ReplaceAsync(cipherDetails);
|
await sutProvider.GetDependency<ICipherRepository>().Received(1).ReplaceAsync(cipherDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_PersonalVault_WithDisablePersonalOwnershipPolicyEnabled_Throws(
|
||||||
|
SutProvider<CipherService> sutProvider,
|
||||||
|
CipherDetails cipher,
|
||||||
|
Guid savingUserId)
|
||||||
|
{
|
||||||
|
cipher.Id = default;
|
||||||
|
cipher.UserId = savingUserId;
|
||||||
|
cipher.OrganizationId = null;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null));
|
||||||
|
Assert.Contains("restricted from saving items to your personal vault", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_PersonalVault_WithDisablePersonalOwnershipPolicyDisabled_Succeeds(
|
||||||
|
SutProvider<CipherService> sutProvider,
|
||||||
|
CipherDetails cipher,
|
||||||
|
Guid savingUserId)
|
||||||
|
{
|
||||||
|
cipher.Id = default;
|
||||||
|
cipher.UserId = savingUserId;
|
||||||
|
cipher.OrganizationId = null;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyService>()
|
||||||
|
.AnyPoliciesApplicableToUserAsync(savingUserId, PolicyType.PersonalOwnership)
|
||||||
|
.Returns(false);
|
||||||
|
|
||||||
|
await sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<ICipherRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.CreateAsync(cipher);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_PersonalVault_WithPolicyRequirementsEnabled_WithDisablePersonalOwnershipPolicyEnabled_Throws(
|
||||||
|
SutProvider<CipherService> sutProvider,
|
||||||
|
CipherDetails cipher,
|
||||||
|
Guid savingUserId)
|
||||||
|
{
|
||||||
|
cipher.Id = default;
|
||||||
|
cipher.UserId = savingUserId;
|
||||||
|
cipher.OrganizationId = null;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||||
|
.GetAsync<PersonalOwnershipPolicyRequirement>(savingUserId)
|
||||||
|
.Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = true });
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<BadRequestException>(
|
||||||
|
() => sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null));
|
||||||
|
Assert.Contains("restricted from saving items to your personal vault", exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[BitAutoData]
|
||||||
|
public async Task SaveDetailsAsync_PersonalVault_WithPolicyRequirementsEnabled_WithDisablePersonalOwnershipPolicyDisabled_Succeeds(
|
||||||
|
SutProvider<CipherService> sutProvider,
|
||||||
|
CipherDetails cipher,
|
||||||
|
Guid savingUserId)
|
||||||
|
{
|
||||||
|
cipher.Id = default;
|
||||||
|
cipher.UserId = savingUserId;
|
||||||
|
cipher.OrganizationId = null;
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IFeatureService>()
|
||||||
|
.IsEnabled(FeatureFlagKeys.PolicyRequirements)
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
sutProvider.GetDependency<IPolicyRequirementQuery>()
|
||||||
|
.GetAsync<PersonalOwnershipPolicyRequirement>(savingUserId)
|
||||||
|
.Returns(new PersonalOwnershipPolicyRequirement { DisablePersonalOwnership = false });
|
||||||
|
|
||||||
|
await sutProvider.Sut.SaveDetailsAsync(cipher, savingUserId, null);
|
||||||
|
|
||||||
|
await sutProvider.GetDependency<ICipherRepository>()
|
||||||
|
.Received(1)
|
||||||
|
.CreateAsync(cipher);
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[BitAutoData("")]
|
[BitAutoData("")]
|
||||||
[BitAutoData("Correct Time")]
|
[BitAutoData("Correct Time")]
|
||||||
|
@ -196,4 +196,43 @@ public class OrganizationRepositoryTests
|
|||||||
Assert.Single(sqlResult);
|
Assert.Single(sqlResult);
|
||||||
Assert.True(sqlResult.All(o => o.Name == org.Name));
|
Assert.True(sqlResult.All(o => o.Name == org.Name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[CiSkippedTheory, EfOrganizationAutoData]
|
||||||
|
public async Task GetManyByIdsAsync_Works_DataMatches(List<Organization> organizations,
|
||||||
|
SqlRepo.OrganizationRepository sqlOrganizationRepo,
|
||||||
|
List<EfRepo.OrganizationRepository> suts)
|
||||||
|
{
|
||||||
|
var returnedOrgs = new List<Organization>();
|
||||||
|
|
||||||
|
foreach (var sut in suts)
|
||||||
|
{
|
||||||
|
_ = await sut.CreateMany(organizations);
|
||||||
|
sut.ClearChangeTracking();
|
||||||
|
|
||||||
|
var efReturnedOrgs = await sut.GetManyByIdsAsync(organizations.Select(o => o.Id).ToList());
|
||||||
|
returnedOrgs.AddRange(efReturnedOrgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var organization in organizations)
|
||||||
|
{
|
||||||
|
var postSqlOrg = await sqlOrganizationRepo.CreateAsync(organization);
|
||||||
|
returnedOrgs.Add(await sqlOrganizationRepo.GetByIdAsync(postSqlOrg.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
var orgIds = organizations.Select(o => o.Id).ToList();
|
||||||
|
var distinctReturnedOrgIds = returnedOrgs.Select(o => o.Id).Distinct().ToList();
|
||||||
|
|
||||||
|
Assert.Equal(orgIds.Count, distinctReturnedOrgIds.Count);
|
||||||
|
Assert.Equivalent(orgIds, distinctReturnedOrgIds);
|
||||||
|
|
||||||
|
// clean up
|
||||||
|
foreach (var organization in organizations)
|
||||||
|
{
|
||||||
|
await sqlOrganizationRepo.DeleteAsync(organization);
|
||||||
|
foreach (var sut in suts)
|
||||||
|
{
|
||||||
|
await sut.DeleteAsync(organization);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -253,4 +253,37 @@ public class OrganizationRepositoryTests
|
|||||||
|
|
||||||
Assert.Empty(result);
|
Assert.Empty(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[DatabaseTheory, DatabaseData]
|
||||||
|
public async Task GetManyByIdsAsync_ExistingOrganizations_ReturnsOrganizations(IOrganizationRepository organizationRepository)
|
||||||
|
{
|
||||||
|
var email = "test@email.com";
|
||||||
|
|
||||||
|
var organization1 = await organizationRepository.CreateAsync(new Organization
|
||||||
|
{
|
||||||
|
Name = $"Test Org 1",
|
||||||
|
BillingEmail = email,
|
||||||
|
Plan = "Test",
|
||||||
|
PrivateKey = "privatekey1"
|
||||||
|
});
|
||||||
|
|
||||||
|
var organization2 = await organizationRepository.CreateAsync(new Organization
|
||||||
|
{
|
||||||
|
Name = $"Test Org 2",
|
||||||
|
BillingEmail = email,
|
||||||
|
Plan = "Test",
|
||||||
|
PrivateKey = "privatekey2"
|
||||||
|
});
|
||||||
|
|
||||||
|
var result = await organizationRepository.GetManyByIdsAsync([organization1.Id, organization2.Id]);
|
||||||
|
|
||||||
|
Assert.Equal(2, result.Count);
|
||||||
|
Assert.Contains(result, org => org.Id == organization1.Id);
|
||||||
|
Assert.Contains(result, org => org.Id == organization2.Id);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
await organizationRepository.DeleteAsync(organization1);
|
||||||
|
await organizationRepository.DeleteAsync(organization2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,66 @@
|
|||||||
|
CREATE OR ALTER PROCEDURE [dbo].[Organization_ReadManyByIds] @OrganizationIds AS [dbo].[GuidIdArray] READONLY
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT o.[Id],
|
||||||
|
o.[Identifier],
|
||||||
|
o.[Name],
|
||||||
|
o.[BusinessName],
|
||||||
|
o.[BusinessAddress1],
|
||||||
|
o.[BusinessAddress2],
|
||||||
|
o.[BusinessAddress3],
|
||||||
|
o.[BusinessCountry],
|
||||||
|
o.[BusinessTaxNumber],
|
||||||
|
o.[BillingEmail],
|
||||||
|
o.[Plan],
|
||||||
|
o.[PlanType],
|
||||||
|
o.[Seats],
|
||||||
|
o.[MaxCollections],
|
||||||
|
o.[UsePolicies],
|
||||||
|
o.[UseSso],
|
||||||
|
o.[UseGroups],
|
||||||
|
o.[UseDirectory],
|
||||||
|
o.[UseEvents],
|
||||||
|
o.[UseTotp],
|
||||||
|
o.[Use2fa],
|
||||||
|
o.[UseApi],
|
||||||
|
o.[UseResetPassword],
|
||||||
|
o.[SelfHost],
|
||||||
|
o.[UsersGetPremium],
|
||||||
|
o.[Storage],
|
||||||
|
o.[MaxStorageGb],
|
||||||
|
o.[Gateway],
|
||||||
|
o.[GatewayCustomerId],
|
||||||
|
o.[GatewaySubscriptionId],
|
||||||
|
o.[ReferenceData],
|
||||||
|
o.[Enabled],
|
||||||
|
o.[LicenseKey],
|
||||||
|
o.[PublicKey],
|
||||||
|
o.[PrivateKey],
|
||||||
|
o.[TwoFactorProviders],
|
||||||
|
o.[ExpirationDate],
|
||||||
|
o.[CreationDate],
|
||||||
|
o.[RevisionDate],
|
||||||
|
o.[OwnersNotifiedOfAutoscaling],
|
||||||
|
o.[MaxAutoscaleSeats],
|
||||||
|
o.[UseKeyConnector],
|
||||||
|
o.[UseScim],
|
||||||
|
o.[UseCustomPermissions],
|
||||||
|
o.[UseSecretsManager],
|
||||||
|
o.[Status],
|
||||||
|
o.[UsePasswordManager],
|
||||||
|
o.[SmSeats],
|
||||||
|
o.[SmServiceAccounts],
|
||||||
|
o.[MaxAutoscaleSmSeats],
|
||||||
|
o.[MaxAutoscaleSmServiceAccounts],
|
||||||
|
o.[SecretsManagerBeta],
|
||||||
|
o.[LimitCollectionCreation],
|
||||||
|
o.[LimitCollectionDeletion],
|
||||||
|
o.[LimitItemDeletion],
|
||||||
|
o.[AllowAdminAccessToAllCollectionItems],
|
||||||
|
o.[UseRiskInsights]
|
||||||
|
FROM [dbo].[OrganizationView] o
|
||||||
|
INNER JOIN @OrganizationIds ids ON o.[Id] = ids.[Id]
|
||||||
|
END
|
||||||
|
|
@ -0,0 +1,101 @@
|
|||||||
|
-- OrganizationIntegration
|
||||||
|
|
||||||
|
-- Table
|
||||||
|
IF OBJECT_ID('[dbo].[OrganizationIntegration]') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE [dbo].[OrganizationIntegration]
|
||||||
|
(
|
||||||
|
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
[OrganizationId] UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
[Type] SMALLINT NOT NULL,
|
||||||
|
[Configuration] VARCHAR (MAX) NULL,
|
||||||
|
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||||
|
[RevisionDate] DATETIME2 (7) NOT NULL,
|
||||||
|
CONSTRAINT [PK_OrganizationIntegration] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||||
|
CONSTRAINT [FK_OrganizationIntegration_Organization] FOREIGN KEY ([OrganizationId]) REFERENCES [dbo].[Organization] ([Id])
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE NONCLUSTERED INDEX [IX_OrganizationIntegration_OrganizationId]
|
||||||
|
ON [dbo].[OrganizationIntegration]([OrganizationId] ASC);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX [IX_OrganizationIntegration_Organization_Type]
|
||||||
|
ON [dbo].[OrganizationIntegration]([OrganizationId], [Type]);
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- View
|
||||||
|
IF EXISTS(SELECT *
|
||||||
|
FROM sys.views
|
||||||
|
WHERE [Name] = 'OrganizationIntegrationView')
|
||||||
|
BEGIN
|
||||||
|
DROP VIEW [dbo].[OrganizationIntegrationView];
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE VIEW [dbo].[OrganizationIntegrationView]
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationIntegration]
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- OrganizationIntegrationConfiguration
|
||||||
|
|
||||||
|
-- Table
|
||||||
|
IF OBJECT_ID('[dbo].[OrganizationIntegrationConfiguration]') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE [dbo].[OrganizationIntegrationConfiguration]
|
||||||
|
(
|
||||||
|
[Id] UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
[OrganizationIntegrationId] UNIQUEIDENTIFIER NOT NULL,
|
||||||
|
[EventType] SMALLINT NOT NULL,
|
||||||
|
[Configuration] VARCHAR (MAX) NULL,
|
||||||
|
[Template] VARCHAR (MAX) NULL,
|
||||||
|
[CreationDate] DATETIME2 (7) NOT NULL,
|
||||||
|
[RevisionDate] DATETIME2 (7) NOT NULL,
|
||||||
|
CONSTRAINT [PK_OrganizationIntegrationConfiguration] PRIMARY KEY CLUSTERED ([Id] ASC),
|
||||||
|
CONSTRAINT [FK_OrganizationIntegrationConfiguration_OrganizationIntegration] FOREIGN KEY ([OrganizationIntegrationId]) REFERENCES [dbo].[OrganizationIntegration] ([Id])
|
||||||
|
);
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- View
|
||||||
|
IF EXISTS(SELECT *
|
||||||
|
FROM sys.views
|
||||||
|
WHERE [Name] = 'OrganizationIntegrationConfigurationView')
|
||||||
|
BEGIN
|
||||||
|
DROP VIEW [dbo].[OrganizationIntegrationConfigurationView];
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE VIEW [dbo].[OrganizationIntegrationConfigurationView]
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
*
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationIntegrationConfiguration]
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[OrganizationIntegrationConfiguration_ReadManyByEventTypeOrganizationIdIntegrationType]
|
||||||
|
@EventType SMALLINT,
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER,
|
||||||
|
@IntegrationType SMALLINT
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
oic.*
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationIntegrationConfigurationView] oic
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[OrganizationIntegration] oi ON oi.[Id] = oic.[OrganizationIntegrationId]
|
||||||
|
WHERE
|
||||||
|
oic.[EventType] = @EventType
|
||||||
|
AND
|
||||||
|
oi.[OrganizationId] = @OrganizationId
|
||||||
|
AND
|
||||||
|
oi.[Type] = @IntegrationType
|
||||||
|
END
|
||||||
|
GO
|
@ -0,0 +1,49 @@
|
|||||||
|
IF EXISTS(SELECT *
|
||||||
|
FROM sys.views
|
||||||
|
WHERE [Name] = 'OrganizationIntegrationConfigurationDetailsView')
|
||||||
|
BEGIN
|
||||||
|
DROP VIEW [dbo].[OrganizationIntegrationConfigurationDetailsView];
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE VIEW [dbo].[OrganizationIntegrationConfigurationDetailsView]
|
||||||
|
AS
|
||||||
|
SELECT
|
||||||
|
oi.[OrganizationId],
|
||||||
|
oi.[Type] AS [IntegrationType],
|
||||||
|
oic.[EventType],
|
||||||
|
oic.[Configuration],
|
||||||
|
oi.[Configuration] AS [IntegrationConfiguration],
|
||||||
|
oic.[Template]
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationIntegrationConfiguration] oic
|
||||||
|
INNER JOIN
|
||||||
|
[dbo].[OrganizationIntegration] oi ON oi.[Id] = oic.[OrganizationIntegrationId]
|
||||||
|
GO
|
||||||
|
|
||||||
|
IF OBJECT_ID('[dbo].[OrganizationIntegrationConfiguration_ReadManyByEventTypeOrganizationIdIntegrationType]') IS NOT NULL
|
||||||
|
BEGIN
|
||||||
|
DROP PROCEDURE [dbo].[OrganizationIntegrationConfiguration_ReadManyByEventTypeOrganizationIdIntegrationType]
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE OR ALTER PROCEDURE [dbo].[OrganizationIntegrationConfigurationDetails_ReadManyByEventTypeOrganizationIdIntegrationType]
|
||||||
|
@EventType SMALLINT,
|
||||||
|
@OrganizationId UNIQUEIDENTIFIER,
|
||||||
|
@IntegrationType SMALLINT
|
||||||
|
AS
|
||||||
|
BEGIN
|
||||||
|
SET NOCOUNT ON
|
||||||
|
|
||||||
|
SELECT
|
||||||
|
oic.*
|
||||||
|
FROM
|
||||||
|
[dbo].[OrganizationIntegrationConfigurationDetailsView] oic
|
||||||
|
WHERE
|
||||||
|
oic.[EventType] = @EventType
|
||||||
|
AND
|
||||||
|
oic.[OrganizationId] = @OrganizationId
|
||||||
|
AND
|
||||||
|
oic.[IntegrationType] = @IntegrationType
|
||||||
|
END
|
||||||
|
GO
|
3101
util/MySqlMigrations/Migrations/20250325231708_OrganizationIntegrations.Designer.cs
generated
Normal file
3101
util/MySqlMigrations/Migrations/20250325231708_OrganizationIntegrations.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,89 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.MySqlMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class OrganizationIntegrations : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "OrganizationIntegration",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
OrganizationId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
Type = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Configuration = table.Column<string>(type: "longtext", nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
CreationDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
RevisionDate = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_OrganizationIntegration", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_OrganizationIntegration_Organization_OrganizationId",
|
||||||
|
column: x => x.OrganizationId,
|
||||||
|
principalTable: "Organization",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "OrganizationIntegrationConfiguration",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
OrganizationIntegrationId = table.Column<Guid>(type: "char(36)", nullable: false, collation: "ascii_general_ci"),
|
||||||
|
EventType = table.Column<int>(type: "int", nullable: false),
|
||||||
|
Configuration = table.Column<string>(type: "longtext", nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
Template = table.Column<string>(type: "longtext", nullable: true)
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4"),
|
||||||
|
CreationDate = table.Column<DateTime>(type: "datetime(6)", nullable: false),
|
||||||
|
RevisionDate = table.Column<DateTime>(type: "datetime(6)", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_OrganizationIntegrationConfiguration", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_OrganizationIntegrationConfiguration_OrganizationIntegration~",
|
||||||
|
column: x => x.OrganizationIntegrationId,
|
||||||
|
principalTable: "OrganizationIntegration",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
})
|
||||||
|
.Annotation("MySql:CharSet", "utf8mb4");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_OrganizationIntegration_OrganizationId",
|
||||||
|
table: "OrganizationIntegration",
|
||||||
|
column: "OrganizationId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_OrganizationIntegration_OrganizationId_Type",
|
||||||
|
table: "OrganizationIntegration",
|
||||||
|
columns: new[] { "OrganizationId", "Type" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_OrganizationIntegrationConfiguration_OrganizationIntegration~",
|
||||||
|
table: "OrganizationIntegrationConfiguration",
|
||||||
|
column: "OrganizationIntegrationId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "OrganizationIntegrationConfiguration");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "OrganizationIntegration");
|
||||||
|
}
|
||||||
|
}
|
@ -217,6 +217,68 @@ namespace Bit.MySqlMigrations.Migrations
|
|||||||
b.ToTable("Organization", (string)null);
|
b.ToTable("Organization", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("Configuration")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreationDate")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<Guid>("OrganizationId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("RevisionDate")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OrganizationId")
|
||||||
|
.HasAnnotation("SqlServer:Clustered", false);
|
||||||
|
|
||||||
|
b.HasIndex("OrganizationId", "Type")
|
||||||
|
.IsUnique()
|
||||||
|
.HasAnnotation("SqlServer:Clustered", false);
|
||||||
|
|
||||||
|
b.ToTable("OrganizationIntegration", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<string>("Configuration")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreationDate")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<int>("EventType")
|
||||||
|
.HasColumnType("int");
|
||||||
|
|
||||||
|
b.Property<Guid>("OrganizationIntegrationId")
|
||||||
|
.HasColumnType("char(36)");
|
||||||
|
|
||||||
|
b.Property<DateTime>("RevisionDate")
|
||||||
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
|
b.Property<string>("Template")
|
||||||
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OrganizationIntegrationId");
|
||||||
|
|
||||||
|
b.ToTable("OrganizationIntegrationConfiguration", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b =>
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@ -407,10 +469,6 @@ namespace Bit.MySqlMigrations.Migrations
|
|||||||
b.Property<DateTime?>("AuthenticationDate")
|
b.Property<DateTime?>("AuthenticationDate")
|
||||||
.HasColumnType("datetime(6)");
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
b.Property<string>("RequestCountryName")
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("varchar(200)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreationDate")
|
b.Property<DateTime>("CreationDate")
|
||||||
.HasColumnType("datetime(6)");
|
.HasColumnType("datetime(6)");
|
||||||
|
|
||||||
@ -426,6 +484,10 @@ namespace Bit.MySqlMigrations.Migrations
|
|||||||
b.Property<string>("PublicKey")
|
b.Property<string>("PublicKey")
|
||||||
.HasColumnType("longtext");
|
.HasColumnType("longtext");
|
||||||
|
|
||||||
|
b.Property<string>("RequestCountryName")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("varchar(200)");
|
||||||
|
|
||||||
b.Property<string>("RequestDeviceIdentifier")
|
b.Property<string>("RequestDeviceIdentifier")
|
||||||
.HasMaxLength(50)
|
.HasMaxLength(50)
|
||||||
.HasColumnType("varchar(50)");
|
.HasColumnType("varchar(50)");
|
||||||
@ -2259,6 +2321,28 @@ namespace Bit.MySqlMigrations.Migrations
|
|||||||
b.HasDiscriminator().HasValue("user_service_account");
|
b.HasDiscriminator().HasValue("user_service_account");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("OrganizationId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Organization");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("OrganizationIntegrationId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("OrganizationIntegration");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b =>
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
||||||
|
3107
util/PostgresMigrations/Migrations/20250325231701_OrganizationIntegrations.Designer.cs
generated
Normal file
3107
util/PostgresMigrations/Migrations/20250325231701_OrganizationIntegrations.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,84 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.PostgresMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class OrganizationIntegrations : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "OrganizationIntegration",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
OrganizationId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
Type = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Configuration = table.Column<string>(type: "text", nullable: true),
|
||||||
|
CreationDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
RevisionDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_OrganizationIntegration", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_OrganizationIntegration_Organization_OrganizationId",
|
||||||
|
column: x => x.OrganizationId,
|
||||||
|
principalTable: "Organization",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "OrganizationIntegrationConfiguration",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
OrganizationIntegrationId = table.Column<Guid>(type: "uuid", nullable: false),
|
||||||
|
EventType = table.Column<int>(type: "integer", nullable: false),
|
||||||
|
Configuration = table.Column<string>(type: "text", nullable: true),
|
||||||
|
Template = table.Column<string>(type: "text", nullable: true),
|
||||||
|
CreationDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
|
||||||
|
RevisionDate = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_OrganizationIntegrationConfiguration", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_OrganizationIntegrationConfiguration_OrganizationIntegratio~",
|
||||||
|
column: x => x.OrganizationIntegrationId,
|
||||||
|
principalTable: "OrganizationIntegration",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_OrganizationIntegration_OrganizationId",
|
||||||
|
table: "OrganizationIntegration",
|
||||||
|
column: "OrganizationId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_OrganizationIntegration_OrganizationId_Type",
|
||||||
|
table: "OrganizationIntegration",
|
||||||
|
columns: new[] { "OrganizationId", "Type" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_OrganizationIntegrationConfiguration_OrganizationIntegratio~",
|
||||||
|
table: "OrganizationIntegrationConfiguration",
|
||||||
|
column: "OrganizationIntegrationId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "OrganizationIntegrationConfiguration");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "OrganizationIntegration");
|
||||||
|
}
|
||||||
|
}
|
@ -220,6 +220,68 @@ namespace Bit.PostgresMigrations.Migrations
|
|||||||
b.ToTable("Organization", (string)null);
|
b.ToTable("Organization", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Configuration")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreationDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<Guid>("OrganizationId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("RevisionDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OrganizationId")
|
||||||
|
.HasAnnotation("SqlServer:Clustered", false);
|
||||||
|
|
||||||
|
b.HasIndex("OrganizationId", "Type")
|
||||||
|
.IsUnique()
|
||||||
|
.HasAnnotation("SqlServer:Clustered", false);
|
||||||
|
|
||||||
|
b.ToTable("OrganizationIntegration", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<string>("Configuration")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreationDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<int>("EventType")
|
||||||
|
.HasColumnType("integer");
|
||||||
|
|
||||||
|
b.Property<Guid>("OrganizationIntegrationId")
|
||||||
|
.HasColumnType("uuid");
|
||||||
|
|
||||||
|
b.Property<DateTime>("RevisionDate")
|
||||||
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
|
b.Property<string>("Template")
|
||||||
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OrganizationIntegrationId");
|
||||||
|
|
||||||
|
b.ToTable("OrganizationIntegrationConfiguration", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b =>
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@ -410,10 +472,6 @@ namespace Bit.PostgresMigrations.Migrations
|
|||||||
b.Property<DateTime?>("AuthenticationDate")
|
b.Property<DateTime?>("AuthenticationDate")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
b.Property<string>("RequestCountryName")
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("character varying(200)");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreationDate")
|
b.Property<DateTime>("CreationDate")
|
||||||
.HasColumnType("timestamp with time zone");
|
.HasColumnType("timestamp with time zone");
|
||||||
|
|
||||||
@ -429,6 +487,10 @@ namespace Bit.PostgresMigrations.Migrations
|
|||||||
b.Property<string>("PublicKey")
|
b.Property<string>("PublicKey")
|
||||||
.HasColumnType("text");
|
.HasColumnType("text");
|
||||||
|
|
||||||
|
b.Property<string>("RequestCountryName")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("character varying(200)");
|
||||||
|
|
||||||
b.Property<string>("RequestDeviceIdentifier")
|
b.Property<string>("RequestDeviceIdentifier")
|
||||||
.HasMaxLength(50)
|
.HasMaxLength(50)
|
||||||
.HasColumnType("character varying(50)");
|
.HasColumnType("character varying(50)");
|
||||||
@ -2265,6 +2327,28 @@ namespace Bit.PostgresMigrations.Migrations
|
|||||||
b.HasDiscriminator().HasValue("user_service_account");
|
b.HasDiscriminator().HasValue("user_service_account");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("OrganizationId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Organization");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("OrganizationIntegrationId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("OrganizationIntegration");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b =>
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
||||||
|
3090
util/SqliteMigrations/Migrations/20250325231714_OrganizationIntegrations.Designer.cs
generated
Normal file
3090
util/SqliteMigrations/Migrations/20250325231714_OrganizationIntegrations.Designer.cs
generated
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,84 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore.Migrations;
|
||||||
|
|
||||||
|
#nullable disable
|
||||||
|
|
||||||
|
namespace Bit.SqliteMigrations.Migrations;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public partial class OrganizationIntegrations : Migration
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Up(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "OrganizationIntegration",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
OrganizationId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
Type = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Configuration = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
CreationDate = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
RevisionDate = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_OrganizationIntegration", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_OrganizationIntegration_Organization_OrganizationId",
|
||||||
|
column: x => x.OrganizationId,
|
||||||
|
principalTable: "Organization",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateTable(
|
||||||
|
name: "OrganizationIntegrationConfiguration",
|
||||||
|
columns: table => new
|
||||||
|
{
|
||||||
|
Id = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
OrganizationIntegrationId = table.Column<Guid>(type: "TEXT", nullable: false),
|
||||||
|
EventType = table.Column<int>(type: "INTEGER", nullable: false),
|
||||||
|
Configuration = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
Template = table.Column<string>(type: "TEXT", nullable: true),
|
||||||
|
CreationDate = table.Column<DateTime>(type: "TEXT", nullable: false),
|
||||||
|
RevisionDate = table.Column<DateTime>(type: "TEXT", nullable: false)
|
||||||
|
},
|
||||||
|
constraints: table =>
|
||||||
|
{
|
||||||
|
table.PrimaryKey("PK_OrganizationIntegrationConfiguration", x => x.Id);
|
||||||
|
table.ForeignKey(
|
||||||
|
name: "FK_OrganizationIntegrationConfiguration_OrganizationIntegration_OrganizationIntegrationId",
|
||||||
|
column: x => x.OrganizationIntegrationId,
|
||||||
|
principalTable: "OrganizationIntegration",
|
||||||
|
principalColumn: "Id",
|
||||||
|
onDelete: ReferentialAction.Cascade);
|
||||||
|
});
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_OrganizationIntegration_OrganizationId",
|
||||||
|
table: "OrganizationIntegration",
|
||||||
|
column: "OrganizationId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_OrganizationIntegration_OrganizationId_Type",
|
||||||
|
table: "OrganizationIntegration",
|
||||||
|
columns: new[] { "OrganizationId", "Type" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_OrganizationIntegrationConfiguration_OrganizationIntegrationId",
|
||||||
|
table: "OrganizationIntegrationConfiguration",
|
||||||
|
column: "OrganizationIntegrationId");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Down(MigrationBuilder migrationBuilder)
|
||||||
|
{
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "OrganizationIntegrationConfiguration");
|
||||||
|
|
||||||
|
migrationBuilder.DropTable(
|
||||||
|
name: "OrganizationIntegration");
|
||||||
|
}
|
||||||
|
}
|
@ -212,6 +212,68 @@ namespace Bit.SqliteMigrations.Migrations
|
|||||||
b.ToTable("Organization", (string)null);
|
b.ToTable("Organization", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Configuration")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreationDate")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<Guid>("OrganizationId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("RevisionDate")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("Type")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OrganizationId")
|
||||||
|
.HasAnnotation("SqlServer:Clustered", false);
|
||||||
|
|
||||||
|
b.HasIndex("OrganizationId", "Type")
|
||||||
|
.IsUnique()
|
||||||
|
.HasAnnotation("SqlServer:Clustered", false);
|
||||||
|
|
||||||
|
b.ToTable("OrganizationIntegration", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b =>
|
||||||
|
{
|
||||||
|
b.Property<Guid>("Id")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Configuration")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("CreationDate")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<int>("EventType")
|
||||||
|
.HasColumnType("INTEGER");
|
||||||
|
|
||||||
|
b.Property<Guid>("OrganizationIntegrationId")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<DateTime>("RevisionDate")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("Template")
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("OrganizationIntegrationId");
|
||||||
|
|
||||||
|
b.ToTable("OrganizationIntegrationConfiguration", (string)null);
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b =>
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b =>
|
||||||
{
|
{
|
||||||
b.Property<Guid>("Id")
|
b.Property<Guid>("Id")
|
||||||
@ -402,10 +464,6 @@ namespace Bit.SqliteMigrations.Migrations
|
|||||||
b.Property<DateTime?>("AuthenticationDate")
|
b.Property<DateTime?>("AuthenticationDate")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("RequestCountryName")
|
|
||||||
.HasMaxLength(200)
|
|
||||||
.HasColumnType("TEXT");
|
|
||||||
|
|
||||||
b.Property<DateTime>("CreationDate")
|
b.Property<DateTime>("CreationDate")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
@ -421,6 +479,10 @@ namespace Bit.SqliteMigrations.Migrations
|
|||||||
b.Property<string>("PublicKey")
|
b.Property<string>("PublicKey")
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
|
b.Property<string>("RequestCountryName")
|
||||||
|
.HasMaxLength(200)
|
||||||
|
.HasColumnType("TEXT");
|
||||||
|
|
||||||
b.Property<string>("RequestDeviceIdentifier")
|
b.Property<string>("RequestDeviceIdentifier")
|
||||||
.HasMaxLength(50)
|
.HasMaxLength(50)
|
||||||
.HasColumnType("TEXT");
|
.HasColumnType("TEXT");
|
||||||
@ -2248,6 +2310,28 @@ namespace Bit.SqliteMigrations.Migrations
|
|||||||
b.HasDiscriminator().HasValue("user_service_account");
|
b.HasDiscriminator().HasValue("user_service_account");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("OrganizationId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("Organization");
|
||||||
|
});
|
||||||
|
|
||||||
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegrationConfiguration", b =>
|
||||||
|
{
|
||||||
|
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.OrganizationIntegration", "OrganizationIntegration")
|
||||||
|
.WithMany()
|
||||||
|
.HasForeignKey("OrganizationIntegrationId")
|
||||||
|
.OnDelete(DeleteBehavior.Cascade)
|
||||||
|
.IsRequired();
|
||||||
|
|
||||||
|
b.Navigation("OrganizationIntegration");
|
||||||
|
});
|
||||||
|
|
||||||
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b =>
|
modelBuilder.Entity("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Policy", b =>
|
||||||
{
|
{
|
||||||
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
b.HasOne("Bit.Infrastructure.EntityFramework.AdminConsole.Models.Organization", "Organization")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user