1
0
mirror of https://github.com/bitwarden/server.git synced 2025-01-20 21:31:23 +01:00

Merge branch 'BRE-291-ephemeral-env-cleanup-workflow' into ephem-test-01

This commit is contained in:
MtnBurrit0 2024-10-21 15:17:22 -06:00
commit 71f2d806ab
77 changed files with 1395 additions and 385 deletions

View File

@ -1,4 +1,3 @@
---
name: _move_finalization_db_scripts
run-name: Move finalization database scripts
@ -30,7 +29,7 @@ jobs:
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
- name: Check out branch
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
token: ${{ steps.retrieve-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
@ -54,7 +53,7 @@ jobs:
if: ${{ needs.setup.outputs.copy_finalization_scripts == 'true' }}
steps:
- name: Checkout
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
fetch-depth: 0

View File

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

View File

@ -1,4 +1,3 @@
---
name: Build
on:
@ -19,7 +18,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Set up .NET
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
@ -68,7 +67,7 @@ jobs:
node: true
steps:
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Set up .NET
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
@ -110,7 +109,7 @@ jobs:
ls -atlh ../../../
- name: Upload project artifact
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: ${{ matrix.project_name }}.zip
path: ${{ matrix.base_path }}/${{ matrix.project_name }}/${{ matrix.project_name }}.zip
@ -173,7 +172,7 @@ jobs:
dotnet: true
steps:
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Check branch to publish
env:
@ -263,7 +262,7 @@ jobs:
-d ${{ matrix.base_path }}/${{ matrix.project_name }}/obj/build-output/publish
- name: Build Docker image
uses: docker/build-push-action@32945a339266b759abcbdc89316275140b0fc960 # v6.8.0
uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 # v6.9.0
with:
context: ${{ matrix.base_path }}/${{ matrix.project_name }}
file: ${{ matrix.base_path }}/${{ matrix.project_name }}/Dockerfile
@ -275,14 +274,14 @@ jobs:
- name: Scan Docker image
id: container-scan
uses: anchore/scan-action@64a33b277ea7a1215a3c142735a1091341939ff5 # v4.1.2
uses: anchore/scan-action@49e50b215b647c5ec97abb66f69af73c46a4ca08 # v5.0.1
with:
image: ${{ steps.image-tags.outputs.primary_tag }}
fail-build: false
output-format: sarif
- name: Upload Grype results to GitHub
uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9
uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
with:
sarif_file: ${{ steps.container-scan.outputs.sarif }}
@ -292,7 +291,7 @@ jobs:
needs: build-docker
steps:
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Set up .NET
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
@ -311,7 +310,7 @@ jobs:
github.ref == 'refs/heads/hotfix-rc'
run: |
# Set proper setup image based on branch
case "${{ github.ref }}" in
case "$GITHUB_REF" in
"refs/heads/main")
SETUP_IMAGE="$_AZ_REGISTRY/setup:dev"
;;
@ -355,7 +354,7 @@ jobs:
- name: Upload Docker stub US artifact
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: docker-stub-US.zip
path: docker-stub-US.zip
@ -363,7 +362,7 @@ jobs:
- name: Upload Docker stub EU artifact
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: docker-stub-EU.zip
path: docker-stub-EU.zip
@ -371,7 +370,7 @@ jobs:
- name: Upload Docker stub US checksum artifact
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: docker-stub-US-sha256.txt
path: docker-stub-US-sha256.txt
@ -379,7 +378,7 @@ jobs:
- name: Upload Docker stub EU checksum artifact
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/rc' || github.ref == 'refs/heads/hotfix-rc'
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: docker-stub-EU-sha256.txt
path: docker-stub-EU-sha256.txt
@ -403,12 +402,12 @@ jobs:
GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder"
- name: Upload Public API Swagger artifact
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: swagger.json
path: swagger.json
if-no-files-found: error
- name: Build Internal API Swagger
run: |
cd ./src/Api
@ -416,17 +415,17 @@ jobs:
dotnet tool restore
echo "Publish API"
dotnet publish -c "Release" -o obj/build-output/publish
dotnet swagger tofile --output ../../internal.json --host https://api.bitwarden.com \
./obj/build-output/publish/Api.dll internal
cd ../Identity
echo "Restore Identity tools"
dotnet tool restore
echo "Publish Identity"
dotnet publish -c "Release" -o obj/build-output/publish
dotnet swagger tofile --output ../../identity.json --host https://identity.bitwarden.com \
./obj/build-output/publish/Identity.dll v1
cd ../..
@ -437,18 +436,18 @@ jobs:
GLOBALSETTINGS__SQLSERVER__CONNECTIONSTRING: "placeholder"
- name: Upload Internal API Swagger artifact
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: internal.json
path: internal.json
if-no-files-found: error
- name: Upload Identity Swagger artifact
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: identity.json
path: identity.json
if-no-files-found: error
if-no-files-found: error
build-mssqlmigratorutility:
name: Build MSSQL migrator utility
@ -467,7 +466,7 @@ jobs:
- win-x64
steps:
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Set up .NET
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
@ -486,7 +485,7 @@ jobs:
- name: Upload project artifact for Windows
if: ${{ contains(matrix.target, 'win') == true }}
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: MsSqlMigratorUtility-${{ matrix.target }}
path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility.exe
@ -494,7 +493,7 @@ jobs:
- name: Upload project artifact
if: ${{ contains(matrix.target, 'win') == false }}
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: MsSqlMigratorUtility-${{ matrix.target }}
path: util/MsSqlMigratorUtility/obj/build-output/publish/MsSqlMigratorUtility
@ -528,9 +527,9 @@ jobs:
workflow_id: 'build-unified.yml',
ref: 'main',
inputs: {
server_branch: '${{ github.ref }}'
server_branch: process.env.GITHUB_REF
}
})
});
trigger-k8s-deploy:
name: Trigger k8s deploy
@ -565,7 +564,7 @@ jobs:
tag: 'main'
}
})
trigger-ee-updates:
name: Trigger Ephemeral Environment updates
if: github.ref != 'refs/heads/main' && contains(github.event.pull_request.labels.*.name, 'ephemeral-environment')

View File

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

View File

@ -0,0 +1,40 @@
name: Ephemeral environment cleanup
on:
pull_request:
types: [unlabeled]
jobs:
cleanup-config:
name: Cleanup ephemeral environment
runs-on: ubuntu-24.04
if: ${{ contains(github.event.pull_request.labels.*.name, 'ephemeral-environment') }}
steps:
- name: Log in to Azure - CI subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve GitHub PAT secrets
id: retrieve-secret-pat
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
- name: Trigger Ephemeral Environment cleanup
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
with:
github-token: ${{ steps.retrieve-secret-pat.outputs.github-pat-bitwarden-devops-bot-repo-scope }}
script: |
await github.rest.actions.createWorkflowDispatch({
owner: 'bitwarden',
repo: 'devops',
workflow_id: '_ephemeral_environment_pr_manager.yml',
ref: 'BRE-291-ephemeral-pr-manager',
inputs: {
ephemeral_env_branch: '$GITHUB_HEAD_REF',
cleanup_config: true,
project: 'server'
}
})

View File

@ -1,4 +1,3 @@
---
name: Cleanup RC Branch
on:
@ -24,7 +23,7 @@ jobs:
secrets: "github-pat-bitwarden-devops-bot-repo-scope"
- name: Checkout main
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
ref: main
token: ${{ steps.retrieve-bot-secrets.outputs.github-pat-bitwarden-devops-bot-repo-scope }}

View File

@ -33,7 +33,7 @@ jobs:
steps:
- name: Check out repository
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Collect
id: collect

View File

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

View File

@ -1,7 +1,6 @@
# Runs if there are changes to the paths: list.
# Starts a matrix job to check for modified files, then sets output based on the results.
# The input decides if the label job is ran, adding a label to the PR.
---
name: Protect files
on:
@ -29,7 +28,7 @@ jobs:
label: "DB-migrations-changed"
steps:
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
fetch-depth: 2

View File

@ -1,4 +1,3 @@
---
name: Publish
run-name: Publish ${{ inputs.publish_type }}
@ -99,7 +98,7 @@ jobs:
echo "Github Release Option: $RELEASE_OPTION"
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Set up project name
id: setup

View File

@ -1,4 +1,3 @@
---
name: Release
run-name: Release ${{ inputs.release_type }}
@ -37,7 +36,7 @@ jobs:
fi
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Check release version
id: version

View File

@ -27,7 +27,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Check out target ref
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
ref: ${{ inputs.target_ref }}
@ -62,7 +62,7 @@ jobs:
version: ${{ inputs.version_number_override }}
- name: Check out branch
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
ref: main
@ -150,7 +150,7 @@ jobs:
needs: bump_version
steps:
- name: Check out main branch
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
ref: main

View File

@ -26,12 +26,12 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Scan with Checkmarx
uses: checkmarx/ast-github-action@9fda5a4a2c297608117a5a56af424502a9192e57 # 2.0.34
uses: checkmarx/ast-github-action@f0869bd1a37fddc06499a096101e6c900e815d81 # 2.0.36
env:
INCREMENTAL: "${{ contains(github.event_name, 'pull_request') && '--sast-incremental' || '' }}"
with:
@ -46,7 +46,7 @@ jobs:
--output-path . ${{ env.INCREMENTAL }}
- name: Upload Checkmarx results to GitHub
uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9
uses: github/codeql-action/upload-sarif@f779452ac5af1c261dce0346a8f964149f49322b # v3.26.13
with:
sarif_file: cx_result.sarif
@ -66,7 +66,7 @@ jobs:
distribution: "zulu"
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}

View File

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

View File

@ -1,4 +1,3 @@
---
name: Database testing
on:
@ -36,7 +35,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Set up .NET
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
@ -55,7 +54,7 @@ jobs:
# I've seen the SQL Server container not be ready for commands right after starting up and just needing a bit longer to be ready
- name: Sleep
run: sleep 15s
- name: Checking pending model changes (MySQL)
working-directory: "util/MySqlMigrations"
run: 'dotnet ef migrations has-pending-model-changes -- --GlobalSettings:MySql:ConnectionString="$CONN_STR"'
@ -114,7 +113,7 @@ jobs:
BW_TEST_DATABASES__3__CONNECTIONSTRING: "Data Source=${{ runner.temp }}/test.db"
run: dotnet test --logger "trx;LogFileName=infrastructure-test-results.trx"
shell: pwsh
- name: Print MySQL Logs
if: failure()
run: 'docker logs $(docker ps --quiet --filter "name=mysql")'
@ -147,7 +146,7 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Set up .NET
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
@ -164,7 +163,7 @@ jobs:
shell: pwsh
- name: Upload DACPAC
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: sql.dacpac
path: Sql.dacpac
@ -190,7 +189,7 @@ jobs:
shell: pwsh
- name: Report validation results
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3
with:
name: report.xml
path: |

View File

@ -46,7 +46,7 @@ jobs:
steps:
- name: Check out repo
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1
- name: Set up .NET
uses: actions/setup-dotnet@6bd8b7f7774af54e05809fcc5431931b3eb1ddee # v4.0.1
@ -77,7 +77,7 @@ jobs:
fail-on-error: true
- name: Upload to codecov.io
uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 # v4.5.0
uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4.6.0
if: ${{ needs.check-test-secrets.outputs.available == 'true' }}
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@ -57,17 +57,15 @@ public class UsersController : Controller
[HttpGet("")]
public async Task<IActionResult> Get(
Guid organizationId,
[FromQuery] string filter,
[FromQuery] int? count,
[FromQuery] int? startIndex)
[FromQuery] GetUsersQueryParamModel model)
{
var usersListQueryResult = await _getUsersListQuery.GetUsersListAsync(organizationId, filter, count, startIndex);
var usersListQueryResult = await _getUsersListQuery.GetUsersListAsync(organizationId, model);
var scimListResponseModel = new ScimListResponseModel<ScimUserResponseModel>
{
Resources = usersListQueryResult.userList.Select(u => new ScimUserResponseModel(u)).ToList(),
ItemsPerPage = count.GetValueOrDefault(usersListQueryResult.userList.Count()),
ItemsPerPage = model.Count,
TotalResults = usersListQueryResult.totalResults,
StartIndex = startIndex.GetValueOrDefault(1),
StartIndex = model.StartIndex,
};
return Ok(scimListResponseModel);
}

View File

@ -0,0 +1,12 @@
using System.ComponentModel.DataAnnotations;
public class GetUsersQueryParamModel
{
public string Filter { get; init; } = string.Empty;
[Range(1, int.MaxValue)]
public int Count { get; init; } = 50;
[Range(1, int.MaxValue)]
public int StartIndex { get; init; } = 1;
}

View File

@ -13,11 +13,16 @@ public class GetUsersListQuery : IGetUsersListQuery
_organizationUserRepository = organizationUserRepository;
}
public async Task<(IEnumerable<OrganizationUserUserDetails> userList, int totalResults)> GetUsersListAsync(Guid organizationId, string filter, int? count, int? startIndex)
public async Task<(IEnumerable<OrganizationUserUserDetails> userList, int totalResults)> GetUsersListAsync(Guid organizationId, GetUsersQueryParamModel userQueryParams)
{
string emailFilter = null;
string usernameFilter = null;
string externalIdFilter = null;
int count = userQueryParams.Count;
int startIndex = userQueryParams.StartIndex;
string filter = userQueryParams.Filter;
if (!string.IsNullOrWhiteSpace(filter))
{
var filterLower = filter.ToLowerInvariant();
@ -56,11 +61,11 @@ public class GetUsersListQuery : IGetUsersListQuery
}
totalResults = userList.Count;
}
else if (string.IsNullOrWhiteSpace(filter) && startIndex.HasValue && count.HasValue)
else if (string.IsNullOrWhiteSpace(filter))
{
userList = orgUsers.OrderBy(ou => ou.Email)
.Skip(startIndex.Value - 1)
.Take(count.Value)
.Skip(startIndex - 1)
.Take(count)
.ToList();
totalResults = orgUsers.Count;
}

View File

@ -4,5 +4,5 @@ namespace Bit.Scim.Users.Interfaces;
public interface IGetUsersListQuery
{
Task<(IEnumerable<OrganizationUserUserDetails> userList, int totalResults)> GetUsersListAsync(Guid organizationId, string filter, int? count, int? startIndex);
Task<(IEnumerable<OrganizationUserUserDetails> userList, int totalResults)> GetUsersListAsync(Guid organizationId, GetUsersQueryParamModel userQueryParams);
}

View File

@ -236,6 +236,46 @@ public class UsersControllerTests : IClassFixture<ScimApplicationFactory>, IAsyn
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);
}
[Fact]
public async Task GetList_SearchUserNameWithoutOptionalParameters_Success()
{
string filter = "userName eq user2@example.com";
int? itemsPerPage = null;
int? startIndex = null;
var expectedResponse = new ScimListResponseModel<ScimUserResponseModel>
{
ItemsPerPage = 50, //default value
TotalResults = 1,
StartIndex = 1, //default value
Resources = new List<ScimUserResponseModel>
{
new ScimUserResponseModel
{
Id = ScimApplicationFactory.TestOrganizationUserId2,
DisplayName = "Test User 2",
ExternalId = "UB",
Active = true,
Emails = new List<BaseScimUserModel.EmailModel>
{
new BaseScimUserModel.EmailModel { Primary = true, Type = "work", Value = "user2@example.com" }
},
Groups = new List<string>(),
Name = new BaseScimUserModel.NameModel("Test User 2"),
UserName = "user2@example.com",
Schemas = new List<string> { ScimConstants.Scim2SchemaUser }
}
},
Schemas = new List<string> { ScimConstants.Scim2SchemaListResponse }
};
var context = await _factory.UsersGetListAsync(ScimApplicationFactory.TestOrganizationId1, filter, itemsPerPage, startIndex);
Assert.Equal(StatusCodes.Status200OK, context.Response.StatusCode);
var responseModel = JsonSerializer.Deserialize<ScimListResponseModel<ScimUserResponseModel>>(context.Response.Body, new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.CamelCase });
AssertHelper.AssertPropertyEqual(expectedResponse, responseModel);
}
[Fact]
public async Task Post_Success()
{

View File

@ -24,7 +24,7 @@ public class GetUsersListQueryTests
.GetManyDetailsByOrganizationAsync(organizationId)
.Returns(organizationUserUserDetails);
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, null, count, startIndex);
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, new GetUsersQueryParamModel { Count = count, StartIndex = startIndex });
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyDetailsByOrganizationAsync(organizationId);
@ -49,7 +49,7 @@ public class GetUsersListQueryTests
.GetManyDetailsByOrganizationAsync(organizationId)
.Returns(organizationUserUserDetails);
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, filter, null, null);
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, new GetUsersQueryParamModel { Filter = filter });
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyDetailsByOrganizationAsync(organizationId);
@ -71,7 +71,7 @@ public class GetUsersListQueryTests
.GetManyDetailsByOrganizationAsync(organizationId)
.Returns(organizationUserUserDetails);
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, filter, null, null);
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, new GetUsersQueryParamModel { Filter = filter });
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyDetailsByOrganizationAsync(organizationId);
@ -96,7 +96,7 @@ public class GetUsersListQueryTests
.GetManyDetailsByOrganizationAsync(organizationId)
.Returns(organizationUserUserDetails);
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, filter, null, null);
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, new GetUsersQueryParamModel { Filter = filter });
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyDetailsByOrganizationAsync(organizationId);
@ -120,7 +120,7 @@ public class GetUsersListQueryTests
.GetManyDetailsByOrganizationAsync(organizationId)
.Returns(organizationUserUserDetails);
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, filter, null, null);
var result = await sutProvider.Sut.GetUsersListAsync(organizationId, new GetUsersQueryParamModel { Filter = filter });
await sutProvider.GetDependency<IOrganizationUserRepository>().Received(1).GetManyDetailsByOrganizationAsync(organizationId);

1
dev/.gitignore vendored
View File

@ -5,7 +5,6 @@ secrets.json
# Docker container configurations
.env
authsources.php
directory.ldif
# Development certificates
identity_server_dev.crt

View File

@ -59,7 +59,7 @@ services:
container_name: bw-mysql
ports:
- "3306:3306"
command:
command:
- --default-authentication-plugin=mysql_native_password
- --innodb-print-all-deadlocks=ON
environment:
@ -84,20 +84,6 @@ services:
profiles:
- idp
open-ldap:
image: osixia/openldap:1.5.0
command: --copy-service
environment:
LDAP_ORGANISATION: "Bitwarden"
LDAP_DOMAIN: "bitwarden.com"
volumes:
- ./directory.ldif:/container/service/slapd/assets/config/bootstrap/ldif/output.ldif
ports:
- "389:389"
- "636:636"
profiles:
- ldap
reverse-proxy:
image: nginx:alpine
container_name: reverse-proxy

View File

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

View File

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

View File

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

View File

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

View File

@ -124,7 +124,11 @@ public class OrganizationsController : Controller
var userId = _userService.GetProperUserId(User).Value;
var organizations = await _organizationUserRepository.GetManyDetailsByUserAsync(userId,
OrganizationUserStatusType.Confirmed);
var responses = organizations.Select(o => new ProfileOrganizationResponseModel(o));
var organizationManagingActiveUser = await _userService.GetOrganizationsManagingUserAsync(userId);
var organizationIdsManagingActiveUser = organizationManagingActiveUser.Select(o => o.Id);
var responses = organizations.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsManagingActiveUser));
return new ListResponseModel<ProfileOrganizationResponseModel>(responses);
}
@ -516,9 +520,16 @@ public class OrganizationsController : Controller
}
[HttpPut("{id}/collection-management")]
[SelfHosted(NotSelfHostedOnly = true)]
public async Task<OrganizationResponseModel> PutCollectionManagement(Guid id, [FromBody] OrganizationCollectionManagementUpdateRequestModel model)
{
if (
_globalSettings.SelfHosted &&
!_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit)
)
{
throw new BadRequestException("Only allowed when not self hosted.");
}
var organization = await _organizationRepository.GetByIdAsync(id);
if (organization == null)
{
@ -530,7 +541,7 @@ public class OrganizationsController : Controller
throw new NotFoundException();
}
await _organizationService.UpdateAsync(model.ToOrganization(organization), eventType: EventType.Organization_CollectionManagement_Updated);
await _organizationService.UpdateAsync(model.ToOrganization(organization, _featureService), eventType: EventType.Organization_CollectionManagement_Updated);
return new OrganizationResponseModel(organization);
}
}

View File

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

View File

@ -15,7 +15,10 @@ public class ProfileOrganizationResponseModel : ResponseModel
{
public ProfileOrganizationResponseModel(string str) : base(str) { }
public ProfileOrganizationResponseModel(OrganizationUserOrganizationDetails organization) : this("profileOrganization")
public ProfileOrganizationResponseModel(
OrganizationUserOrganizationDetails organization,
IEnumerable<Guid> organizationIdsManagingUser)
: this("profileOrganization")
{
Id = organization.OrganizationId;
Name = organization.Name;
@ -62,8 +65,12 @@ public class ProfileOrganizationResponseModel : ResponseModel
FamilySponsorshipToDelete = organization.FamilySponsorshipToDelete;
FamilySponsorshipValidUntil = organization.FamilySponsorshipValidUntil;
AccessSecretsManager = organization.AccessSecretsManager;
LimitCollectionCreation = organization.LimitCollectionCreation;
LimitCollectionDeletion = organization.LimitCollectionDeletion;
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
LimitCollectionCreationDeletion = organization.LimitCollectionCreationDeletion;
AllowAdminAccessToAllCollectionItems = organization.AllowAdminAccessToAllCollectionItems;
UserIsManagedByOrganization = organizationIdsManagingUser.Contains(organization.OrganizationId);
if (organization.SsoConfig != null)
{
@ -120,6 +127,20 @@ public class ProfileOrganizationResponseModel : ResponseModel
public DateTime? FamilySponsorshipValidUntil { get; set; }
public bool? FamilySponsorshipToDelete { get; set; }
public bool AccessSecretsManager { get; set; }
public bool LimitCollectionCreation { get; set; }
public bool LimitCollectionDeletion { get; set; }
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
public bool LimitCollectionCreationDeletion { get; set; }
public bool AllowAdminAccessToAllCollectionItems { get; set; }
/// <summary>
/// Indicates if the organization manages the user.
/// </summary>
/// <remarks>
/// An organization manages a user if the user's email domain is verified by the organization and the user is a member of it.
/// The organization must be enabled and able to have verified domains.
/// </remarks>
/// <returns>
/// False if the Account Deprovisioning feature flag is disabled.
/// </returns>
public bool UserIsManagedByOrganization { get; set; }
}

View File

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

View File

@ -443,11 +443,11 @@ public class AccountsController : Controller
var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user);
var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id);
var response = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
providerUserOrganizationDetails, twoFactorEnabled,
hasPremiumFromOrg, managedByOrganizationId);
hasPremiumFromOrg, organizationIdsManagingActiveUser);
return response;
}
@ -457,7 +457,9 @@ public class AccountsController : Controller
var userId = _userService.GetProperUserId(User);
var organizationUserDetails = await _organizationUserRepository.GetManyDetailsByUserAsync(userId.Value,
OrganizationUserStatusType.Confirmed);
var responseData = organizationUserDetails.Select(o => new ProfileOrganizationResponseModel(o));
var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(userId.Value);
var responseData = organizationUserDetails.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsManagingActiveUser));
return new ListResponseModel<ProfileOrganizationResponseModel>(responseData);
}
@ -475,9 +477,9 @@ public class AccountsController : Controller
var twoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
var hasPremiumFromOrg = await _userService.HasPremiumFromOrganization(user);
var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user);
var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id);
var response = new ProfileResponseModel(user, null, null, null, twoFactorEnabled, hasPremiumFromOrg, managedByOrganizationId);
var response = new ProfileResponseModel(user, null, null, null, twoFactorEnabled, hasPremiumFromOrg, organizationIdsManagingActiveUser);
return response;
}
@ -494,9 +496,9 @@ public class AccountsController : Controller
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user);
var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id);
var response = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, managedByOrganizationId);
var response = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsManagingActiveUser);
return response;
}
@ -647,9 +649,9 @@ public class AccountsController : Controller
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user);
var organizationIdsManagingActiveUser = await GetOrganizationIdsManagingUserAsync(user.Id);
var profile = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, managedByOrganizationId);
var profile = new ProfileResponseModel(user, null, null, null, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsManagingActiveUser);
return new PaymentResponseModel
{
UserProfile = profile,
@ -937,14 +939,9 @@ public class AccountsController : Controller
}
}
private async Task<Guid?> GetManagedByOrganizationIdAsync(User user)
private async Task<IEnumerable<Guid>> GetOrganizationIdsManagingUserAsync(Guid userId)
{
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
{
return null;
}
var organizationManagingUser = await _userService.GetOrganizationManagingUserAsync(user.Id);
return organizationManagingUser?.Id;
var organizationManagingUser = await _userService.GetOrganizationsManagingUserAsync(userId);
return organizationManagingUser.Select(o => o.Id);
}
}

View File

@ -201,7 +201,10 @@ public class OrganizationsController(
var organizationDetails = await organizationUserRepository.GetDetailsByUserAsync(userId, organization.Id,
OrganizationUserStatusType.Confirmed);
return new ProfileOrganizationResponseModel(organizationDetails);
var organizationManagingActiveUser = await userService.GetOrganizationsManagingUserAsync(userId);
var organizationIdsManagingActiveUser = organizationManagingActiveUser.Select(o => o.Id);
return new ProfileOrganizationResponseModel(organizationDetails, organizationIdsManagingActiveUser);
}
[HttpPost("{id:guid}/seat")]

View File

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

View File

@ -15,7 +15,7 @@ public class ProfileResponseModel : ResponseModel
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
bool twoFactorEnabled,
bool premiumFromOrganization,
Guid? managedByOrganizationId) : base("profile")
IEnumerable<Guid> organizationIdsManagingUser) : base("profile")
{
if (user == null)
{
@ -37,11 +37,10 @@ public class ProfileResponseModel : ResponseModel
UsesKeyConnector = user.UsesKeyConnector;
AvatarColor = user.AvatarColor;
CreationDate = user.CreationDate;
Organizations = organizationsUserDetails?.Select(o => new ProfileOrganizationResponseModel(o));
Organizations = organizationsUserDetails?.Select(o => new ProfileOrganizationResponseModel(o, organizationIdsManagingUser));
Providers = providerUserDetails?.Select(p => new ProfileProviderResponseModel(p));
ProviderOrganizations =
providerUserOrganizationDetails?.Select(po => new ProfileProviderOrganizationResponseModel(po));
ManagedByOrganizationId = managedByOrganizationId;
}
public ProfileResponseModel() : base("profile")
@ -63,7 +62,6 @@ public class ProfileResponseModel : ResponseModel
public bool UsesKeyConnector { get; set; }
public string AvatarColor { get; set; }
public DateTime CreationDate { get; set; }
public Guid? ManagedByOrganizationId { get; set; }
public IEnumerable<ProfileOrganizationResponseModel> Organizations { get; set; }
public IEnumerable<ProfileProviderResponseModel> Providers { get; set; }
public IEnumerable<ProfileProviderOrganizationResponseModel> ProviderOrganizations { get; set; }

View File

@ -1,5 +1,6 @@
#nullable enable
using System.Diagnostics;
using Bit.Core;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
@ -101,7 +102,7 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
break;
case null:
// requirement isn't actually nullable but since we use the
// requirement isn't actually nullable but since we use the
// not null when trick it makes the compiler think that requirement
// could actually be nullable.
throw new UnreachableException();
@ -123,10 +124,24 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
return true;
}
// If the limit collection management setting is disabled, allow any user to create collections
if (await GetOrganizationAbilityAsync(org) is { LimitCollectionCreationDeletion: false })
if (_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
{
return true;
var userIsMemberOfOrg = org is not null;
var limitCollectionCreationEnabled = await GetOrganizationAbilityAsync(org) is { LimitCollectionCreation: true };
var userIsOrgOwnerOrAdmin = org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin };
// If the limit collection management setting is disabled, allow any user to create collections
if (userIsMemberOfOrg && (!limitCollectionCreationEnabled || userIsOrgOwnerOrAdmin))
{
return true;
}
}
else
{
// If the limit collection management setting is disabled, allow any user to create collections
if (await GetOrganizationAbilityAsync(org) is { LimitCollectionCreationDeletion: false })
{
return true;
}
}
// Allow provider users to create collections if they are a provider for the target organization
@ -246,21 +261,35 @@ public class BulkCollectionAuthorizationHandler : BulkAuthorizationHandler<BulkC
return true;
}
// If AllowAdminAccessToAllCollectionItems is true, Owners and Admins can delete any collection, regardless of LimitCollectionCreationDeletion setting
// If AllowAdminAccessToAllCollectionItems is true, Owners and Admins can delete any collection, regardless of LimitCollectionDeletion setting
if (await AllowAdminAccessToAllCollectionItems(org) && org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin })
{
return true;
}
// If LimitCollectionCreationDeletion is false, AllowAdminAccessToAllCollectionItems setting is irrelevant.
// Ensure acting user has manage permissions for all collections being deleted
// If LimitCollectionCreationDeletion is true, only Owners and Admins can delete collections they manage
var organizationAbility = await GetOrganizationAbilityAsync(org);
var canDeleteManagedCollections = organizationAbility is { LimitCollectionCreationDeletion: false } ||
org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin };
if (canDeleteManagedCollections && await CanManageCollectionsAsync(resources, org))
if (_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
{
return true;
var userIsMemberOfOrg = org is not null;
var limitCollectionDeletionEnabled = await GetOrganizationAbilityAsync(org) is { LimitCollectionDeletion: true };
var userIsOrgOwnerOrAdmin = org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin };
// If the limit collection management setting is disabled, allow any user to delete collections
if (userIsMemberOfOrg && (!limitCollectionDeletionEnabled || userIsOrgOwnerOrAdmin) && await CanManageCollectionsAsync(resources, org))
{
return true;
}
}
else
{
// If LimitCollectionCreationDeletion is false, AllowAdminAccessToAllCollectionItems setting is irrelevant.
// Ensure acting user has manage permissions for all collections being deleted
// If LimitCollectionCreationDeletion is true, only Owners and Admins can delete collections they manage
var organizationAbility = await GetOrganizationAbilityAsync(org);
var canDeleteManagedCollections = organizationAbility is { LimitCollectionCreationDeletion: false } ||
org is { Type: OrganizationUserType.Owner or OrganizationUserType.Admin };
if (canDeleteManagedCollections && await CanManageCollectionsAsync(resources, org))
{
return true;
}
}
// Allow providers to delete collections if they are a provider for the target organization

View File

@ -910,6 +910,13 @@ public class CiphersController : Controller
throw new BadRequestException(ModelState);
}
// If Account Deprovisioning is enabled, we need to check if the user is managed by any organization.
if (_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
&& await _userService.IsManagedByAnyOrganizationAsync(user.Id))
{
throw new BadRequestException("Cannot purge accounts owned by an organization. Contact your organization administrator for additional details.");
}
if (string.IsNullOrWhiteSpace(organizationId))
{
await _cipherRepository.DeleteByUserIdAsync(user.Id);

View File

@ -1,5 +1,4 @@
using Bit.Api.Vault.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Repositories;
@ -7,7 +6,6 @@ using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
@ -95,23 +93,12 @@ public class SyncController : Controller
var userTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
var userHasPremiumFromOrganization = await _userService.HasPremiumFromOrganization(user);
var managedByOrganizationId = await GetManagedByOrganizationIdAsync(user, organizationUserDetails);
var organizationManagingActiveUser = await _userService.GetOrganizationsManagingUserAsync(user.Id);
var organizationIdsManagingActiveUser = organizationManagingActiveUser.Select(o => o.Id);
var response = new SyncResponseModel(_globalSettings, user, userTwoFactorEnabled, userHasPremiumFromOrganization,
managedByOrganizationId, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails,
organizationIdsManagingActiveUser, organizationUserDetails, providerUserDetails, providerUserOrganizationDetails,
folders, collections, ciphers, collectionCiphersGroupDict, excludeDomains, policies, sends);
return response;
}
private async Task<Guid?> GetManagedByOrganizationIdAsync(User user, IEnumerable<OrganizationUserOrganizationDetails> organizationUserDetails)
{
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning) ||
!organizationUserDetails.Any(o => o.Enabled && o.UseSso))
{
return null;
}
var organizationManagingUser = await _userService.GetOrganizationManagingUserAsync(user.Id);
return organizationManagingUser?.Id;
}
}

View File

@ -21,7 +21,7 @@ public class SyncResponseModel : ResponseModel
User user,
bool userTwoFactorEnabled,
bool userHasPremiumFromOrganization,
Guid? managedByOrganizationId,
IEnumerable<Guid> organizationIdsManagingUser,
IEnumerable<OrganizationUserOrganizationDetails> organizationUserDetails,
IEnumerable<ProviderUserProviderDetails> providerUserDetails,
IEnumerable<ProviderUserOrganizationDetails> providerUserOrganizationDetails,
@ -35,7 +35,7 @@ public class SyncResponseModel : ResponseModel
: base("sync")
{
Profile = new ProfileResponseModel(user, organizationUserDetails, providerUserDetails,
providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, managedByOrganizationId);
providerUserOrganizationDetails, userTwoFactorEnabled, userHasPremiumFromOrganization, organizationIdsManagingUser);
Folders = folders.Select(f => new FolderResponseModel(f));
Ciphers = ciphers.Select(c => new CipherDetailsResponseModel(c, globalSettings, collectionCiphersDict));
Collections = collections?.Select(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,7 +19,7 @@ public interface IOrganizationRepository : IRepository<Organization, Guid>
Task<IEnumerable<string>> GetOwnerEmailAddressesById(Guid organizationId);
/// <summary>
/// Gets the organization that has a claimed domain matching the user's email domain.
/// Gets the organizations that have a verified domain matching the user's email domain.
/// </summary>
Task<Organization> GetByClaimedUserDomainAsync(Guid userId);
Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId);
}

View File

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

View File

@ -708,10 +708,16 @@ public class OrganizationService : IOrganizationService
UseSecretsManager = license.UseSecretsManager,
SmSeats = license.SmSeats,
SmServiceAccounts = license.SmServiceAccounts,
LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion,
AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems,
};
// These fields are being removed from consideration when processing
// licenses.
if (!_featureService.IsEnabled(FeatureFlagKeys.LimitCollectionCreationDeletionSplit))
{
organization.LimitCollectionCreationDeletion = license.LimitCollectionCreationDeletion;
organization.AllowAdminAccessToAllCollectionItems = license.AllowAdminAccessToAllCollectionItems;
}
var result = await SignUpAsync(organization, owner.Id, ownerKey, collectionName, false);
var dir = $"{_globalSettings.LicenseDirectory}/organization";

View File

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

View File

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

View File

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

View File

@ -53,8 +53,11 @@ public class OrganizationLicense : ILicense
UseSecretsManager = org.UseSecretsManager;
SmSeats = org.SmSeats;
SmServiceAccounts = org.SmServiceAccounts;
// Deprecated. Left for backwards compatibility with old license versions.
LimitCollectionCreationDeletion = org.LimitCollectionCreationDeletion;
AllowAdminAccessToAllCollectionItems = org.AllowAdminAccessToAllCollectionItems;
//
if (subscriptionInfo?.Subscription == null)
{
@ -138,8 +141,12 @@ public class OrganizationLicense : ILicense
public bool UseSecretsManager { get; set; }
public int? SmSeats { get; set; }
public int? SmServiceAccounts { get; set; }
// Deprecated. Left for backwards compatibility with old license versions.
public bool LimitCollectionCreationDeletion { get; set; } = true;
public bool AllowAdminAccessToAllCollectionItems { get; set; } = true;
//
public bool Trial { get; set; }
public LicenseType? LicenseType { get; set; }
public string Hash { get; set; }
@ -150,7 +157,8 @@ public class OrganizationLicense : ILicense
/// Represents the current version of the license format. Should be updated whenever new fields are added.
/// </summary>
/// <remarks>Intentionally set one version behind to allow self hosted users some time to update before
/// getting out of date license errors</remarks>
/// getting out of date license errors
/// </remarks>
public const int CurrentLicenseFileVersion = 14;
private bool ValidLicenseVersion
{
@ -368,10 +376,11 @@ public class OrganizationLicense : ILicense
}
/*
* Version 14 added LimitCollectionCreationDeletion and Version 15 added AllowAdminAccessToAllCollectionItems,
* however these are just user settings and it is not worth failing validation if they mismatch.
* They are intentionally excluded.
*/
* Version 14 added LimitCollectionCreationDeletion and Version
* 15 added AllowAdminAccessToAllCollectionItems, however they
* are no longer used and are intentionally excluded from
* validation.
*/
return valid;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -90,14 +90,20 @@ public interface IUserService
/// Indicates if the user is managed by any organization.
/// </summary>
/// <remarks>
/// A managed user is a user whose email domain matches one of the Organization's verified domains.
/// The organization must be enabled and be on an Enterprise plan.
/// A user is considered managed by an organization if their email domain matches one of the verified domains of that organization, and the user is a member of it.
/// The organization must be enabled and able to have verified domains.
/// </remarks>
/// <returns>
/// False if the Account Deprovisioning feature flag is disabled.
/// </returns>
Task<bool> IsManagedByAnyOrganizationAsync(Guid userId);
/// <summary>
/// Gets the organization that manages the user.
/// Gets the organizations that manage the user.
/// </summary>
/// <returns>
/// An empty collection if the Account Deprovisioning feature flag is disabled.
/// </returns>
/// <inheritdoc cref="IsManagedByAnyOrganizationAsync(Guid)"/>
Task<Organization> GetOrganizationManagingUserAsync(Guid userId);
Task<IEnumerable<Organization>> GetOrganizationsManagingUserAsync(Guid userId);
}

View File

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

View File

@ -1267,18 +1267,24 @@ public class UserService : UserManager<User>, IUserService, IDisposable
public async Task<bool> IsManagedByAnyOrganizationAsync(Guid userId)
{
var managingOrganization = await GetOrganizationManagingUserAsync(userId);
return managingOrganization != null;
var managingOrganizations = await GetOrganizationsManagingUserAsync(userId);
return managingOrganizations.Any();
}
public async Task<Organization> GetOrganizationManagingUserAsync(Guid userId)
public async Task<IEnumerable<Organization>> GetOrganizationsManagingUserAsync(Guid userId)
{
// Users can only be managed by an Organization that is enabled and can have organization domains
var organization = await _organizationRepository.GetByClaimedUserDomainAsync(userId);
if (!_featureService.IsEnabled(FeatureFlagKeys.AccountDeprovisioning))
{
return Enumerable.Empty<Organization>();
}
// Get all organizations that have verified the user's email domain.
var organizationsWithVerifiedUserEmailDomain = await _organizationRepository.GetByVerifiedUserEmailDomainAsync(userId);
// Organizations must be enabled and able to have verified domains.
// TODO: Replace "UseSso" with a new organization ability like "UseOrganizationDomains" (PM-11622).
// Verified domains were tied to SSO, so we currently check the "UseSso" organization ability.
return (organization is { Enabled: true, UseSso: true }) ? organization : null;
return organizationsWithVerifiedUserEmailDomain.Where(organization => organization is { Enabled: true, UseSso: true });
}
/// <inheritdoc cref="IsLegacyUser(string)"/>

View File

@ -12,8 +12,4 @@
<ProjectReference Include="..\Core\Core.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="MessagePack" Version="2.5.172" />
</ItemGroup>
</Project>

View File

@ -168,7 +168,7 @@ public class OrganizationRepository : Repository<Organization, Guid>, IOrganizat
commandType: CommandType.StoredProcedure);
}
public async Task<Organization> GetByClaimedUserDomainAsync(Guid userId)
public async Task<ICollection<Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId)
{
using (var connection = new SqlConnection(ConnectionString))
{
@ -177,7 +177,7 @@ public class OrganizationRepository : Repository<Organization, Guid>, IOrganizat
new { UserId = userId },
commandType: CommandType.StoredProcedure);
return result.SingleOrDefault();
return result.ToList();
}
}
}

View File

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

View File

@ -99,6 +99,9 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
UseScim = e.UseScim,
UseCustomPermissions = e.UseCustomPermissions,
UsePolicies = e.UsePolicies,
LimitCollectionCreation = e.LimitCollectionCreation,
LimitCollectionDeletion = e.LimitCollectionDeletion,
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
LimitCollectionCreationDeletion = e.LimitCollectionCreationDeletion,
AllowAdminAccessToAllCollectionItems = e.AllowAdminAccessToAllCollectionItems
}).ToListAsync();
@ -276,7 +279,7 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
return await query.ToListAsync();
}
public async Task<Core.AdminConsole.Entities.Organization> GetByClaimedUserDomainAsync(Guid userId)
public async Task<ICollection<Core.AdminConsole.Entities.Organization>> GetByVerifiedUserEmailDomainAsync(Guid userId)
{
using (var scope = ServiceScopeFactory.CreateScope())
{
@ -291,7 +294,7 @@ public class OrganizationRepository : Repository<Core.AdminConsole.Entities.Orga
&& u.Email.ToLower().EndsWith("@" + od.DomainName.ToLower())
select o;
return await query.FirstOrDefaultAsync();
return await query.ToArrayAsync();
}
}

View File

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

View File

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

View File

@ -4,6 +4,21 @@ AS
BEGIN
SET NOCOUNT ON
SELECT
CC.*
FROM
[dbo].[CollectionCipher] CC
INNER JOIN
[dbo].[Collection] S ON S.[Id] = CC.[CollectionId]
INNER JOIN
[dbo].[OrganizationUser] OU ON OU.[OrganizationId] = S.[OrganizationId] AND OU.[UserId] = @UserId
INNER JOIN
[dbo].[CollectionUser] CU ON CU.[CollectionId] = S.[Id] AND CU.[OrganizationUserId] = OU.[Id]
WHERE
OU.[Status] = 2
UNION ALL
SELECT
CC.*
FROM
@ -12,18 +27,13 @@ BEGIN
[dbo].[Collection] S ON S.[Id] = CC.[CollectionId]
INNER JOIN
[dbo].[OrganizationUser] OU ON OU.[OrganizationId] = S.[OrganizationId] AND OU.[UserId] = @UserId
INNER JOIN
[dbo].[GroupUser] GU ON GU.[OrganizationUserId] = OU.[Id]
INNER JOIN
[dbo].[CollectionGroup] CG ON CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId]
LEFT JOIN
[dbo].[CollectionUser] CU ON CU.[CollectionId] = S.[Id] AND CU.[OrganizationUserId] = OU.[Id]
LEFT JOIN
[dbo].[GroupUser] GU ON CU.[CollectionId] IS NULL AND GU.[OrganizationUserId] = OU.[Id]
LEFT JOIN
[dbo].[Group] G ON G.[Id] = GU.[GroupId]
LEFT JOIN
[dbo].[CollectionGroup] CG ON CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId]
WHERE
OU.[Status] = 2 -- Confirmed
AND (
CU.[CollectionId] IS NOT NULL
OR CG.[CollectionId] IS NOT NULL
)
OU.[Status] = 2
AND CU.[CollectionId] IS NULL
END

View File

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

View File

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

View File

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

View File

@ -1,8 +1,7 @@
using Bit.Core.AdminConsole.Services.Implementations;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.AdminConsole.Services.Implementations;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
@ -36,18 +35,14 @@ public class OrganizationDomainServiceTests
Txt = "btw+6789"
}
};
sutProvider.GetDependency<IOrganizationDomainRepository>().GetManyByNextRunDateAsync(default)
.ReturnsForAnyArgs(domains);
await sutProvider.Sut.ValidateOrganizationsDomainAsync();
await sutProvider.GetDependency<IDnsResolverService>().ReceivedWithAnyArgs(2)
.ResolveAsync(default, default);
await sutProvider.GetDependency<IOrganizationDomainRepository>().ReceivedWithAnyArgs(2)
.ReplaceAsync(default);
await sutProvider.GetDependency<IEventService>().ReceivedWithAnyArgs(2)
.LogOrganizationDomainEventAsync(default, EventType.OrganizationDomain_NotVerified,
EventSystemUser.DomainVerification);
await sutProvider.GetDependency<IVerifyOrganizationDomainCommand>().ReceivedWithAnyArgs(2)
.SystemVerifyOrganizationDomainAsync(default);
}
[Theory, BitAutoData]

View File

@ -27,7 +27,6 @@ using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NSubstitute;
using NSubstitute.ReceivedExtensions;
using Xunit;
namespace Bit.Core.Test.Services;
@ -282,45 +281,69 @@ public class UserServiceTests
}
[Theory, BitAutoData]
public async Task IsManagedByAnyOrganizationAsync_WithManagingEnabledOrganization_ReturnsTrue(
SutProvider<UserService> sutProvider, Guid userId, Organization organization)
public async Task IsManagedByAnyOrganizationAsync_WithAccountDeprovisioningDisabled_ReturnsFalse(
SutProvider<UserService> sutProvider, Guid userId)
{
organization.Enabled = true;
organization.UseSso = true;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByClaimedUserDomainAsync(userId)
.Returns(organization);
var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId);
Assert.True(result);
}
[Theory, BitAutoData]
public async Task IsManagedByAnyOrganizationAsync_WithManagingDisabledOrganization_ReturnsFalse(
SutProvider<UserService> sutProvider, Guid userId, Organization organization)
{
organization.Enabled = false;
organization.UseSso = true;
sutProvider.GetDependency<IOrganizationRepository>()
.GetByClaimedUserDomainAsync(userId)
.Returns(organization);
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(false);
var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId);
Assert.False(result);
}
[Theory, BitAutoData]
public async Task IsManagedByAnyOrganizationAsync_WithOrganizationUseSsoFalse_ReturnsFalse(
public async Task IsManagedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithManagingEnabledOrganization_ReturnsTrue(
SutProvider<UserService> sutProvider, Guid userId, Organization organization)
{
organization.Enabled = true;
organization.UseSso = true;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByVerifiedUserEmailDomainAsync(userId)
.Returns(new[] { organization });
var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId);
Assert.True(result);
}
[Theory, BitAutoData]
public async Task IsManagedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithManagingDisabledOrganization_ReturnsFalse(
SutProvider<UserService> sutProvider, Guid userId, Organization organization)
{
organization.Enabled = false;
organization.UseSso = true;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByVerifiedUserEmailDomainAsync(userId)
.Returns(new[] { organization });
var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId);
Assert.False(result);
}
[Theory, BitAutoData]
public async Task IsManagedByAnyOrganizationAsync_WithAccountDeprovisioningEnabled_WithOrganizationUseSsoFalse_ReturnsFalse(
SutProvider<UserService> sutProvider, Guid userId, Organization organization)
{
organization.Enabled = true;
organization.UseSso = false;
sutProvider.GetDependency<IFeatureService>()
.IsEnabled(FeatureFlagKeys.AccountDeprovisioning)
.Returns(true);
sutProvider.GetDependency<IOrganizationRepository>()
.GetByClaimedUserDomainAsync(userId)
.Returns(organization);
.GetByVerifiedUserEmailDomainAsync(userId)
.Returns(new[] { organization });
var result = await sutProvider.Sut.IsManagedByAnyOrganizationAsync(userId);
Assert.False(result);

View File

@ -97,13 +97,160 @@ public class OrganizationRepositoryTests
ResetPasswordKey = "resetpasswordkey1",
});
var user1Response = await organizationRepository.GetByClaimedUserDomainAsync(user1.Id);
var user2Response = await organizationRepository.GetByClaimedUserDomainAsync(user2.Id);
var user3Response = await organizationRepository.GetByClaimedUserDomainAsync(user3.Id);
var user1Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user1.Id);
var user2Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user2.Id);
var user3Response = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user3.Id);
Assert.NotNull(user1Response);
Assert.Equal(organization.Id, user1Response.Id);
Assert.Null(user2Response);
Assert.Null(user3Response);
Assert.NotEmpty(user1Response);
Assert.Equal(organization.Id, user1Response.First().Id);
Assert.Empty(user2Response);
Assert.Empty(user3Response);
}
[DatabaseTheory, DatabaseData]
public async Task GetByVerifiedUserEmailDomainAsync_WithUnverifiedDomains_ReturnsEmpty(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IOrganizationDomainRepository organizationDomainRepository)
{
var id = Guid.NewGuid();
var domainName = $"{id}.example.com";
var user = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{id}@{domainName}",
ApiKey = "TEST",
SecurityStamp = "stamp",
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = 1,
KdfMemory = 2,
KdfParallelism = 3
});
var organization = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org {id}",
BillingEmail = user.Email,
Plan = "Test",
PrivateKey = "privatekey",
});
var organizationDomain = new OrganizationDomain
{
OrganizationId = organization.Id,
DomainName = domainName,
Txt = "btw+12345",
};
organizationDomain.SetNextRunDate(12);
organizationDomain.SetJobRunCount();
await organizationDomainRepository.CreateAsync(organizationDomain);
await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization.Id,
UserId = user.Id,
Status = OrganizationUserStatusType.Confirmed,
ResetPasswordKey = "resetpasswordkey",
});
var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user.Id);
Assert.Empty(result);
}
[DatabaseTheory, DatabaseData]
public async Task GetByVerifiedUserEmailDomainAsync_WithMultipleVerifiedDomains_ReturnsAllMatchingOrganizations(
IUserRepository userRepository,
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
IOrganizationDomainRepository organizationDomainRepository)
{
var id = Guid.NewGuid();
var domainName = $"{id}.example.com";
var user = await userRepository.CreateAsync(new User
{
Name = "Test User",
Email = $"test+{id}@{domainName}",
ApiKey = "TEST",
SecurityStamp = "stamp",
Kdf = KdfType.PBKDF2_SHA256,
KdfIterations = 1,
KdfMemory = 2,
KdfParallelism = 3
});
var organization1 = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org 1 {id}",
BillingEmail = user.Email,
Plan = "Test",
PrivateKey = "privatekey1",
});
var organization2 = await organizationRepository.CreateAsync(new Organization
{
Name = $"Test Org 2 {id}",
BillingEmail = user.Email,
Plan = "Test",
PrivateKey = "privatekey2",
});
var organizationDomain1 = new OrganizationDomain
{
OrganizationId = organization1.Id,
DomainName = domainName,
Txt = "btw+12345",
};
organizationDomain1.SetNextRunDate(12);
organizationDomain1.SetJobRunCount();
organizationDomain1.SetVerifiedDate();
await organizationDomainRepository.CreateAsync(organizationDomain1);
var organizationDomain2 = new OrganizationDomain
{
OrganizationId = organization2.Id,
DomainName = domainName,
Txt = "btw+67890",
};
organizationDomain2.SetNextRunDate(12);
organizationDomain2.SetJobRunCount();
organizationDomain2.SetVerifiedDate();
await organizationDomainRepository.CreateAsync(organizationDomain2);
await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization1.Id,
UserId = user.Id,
Status = OrganizationUserStatusType.Confirmed,
ResetPasswordKey = "resetpasswordkey1",
});
await organizationUserRepository.CreateAsync(new OrganizationUser
{
OrganizationId = organization2.Id,
UserId = user.Id,
Status = OrganizationUserStatusType.Confirmed,
ResetPasswordKey = "resetpasswordkey2",
});
var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(user.Id);
Assert.Equal(2, result.Count);
Assert.Contains(result, org => org.Id == organization1.Id);
Assert.Contains(result, org => org.Id == organization2.Id);
}
[DatabaseTheory, DatabaseData]
public async Task GetByVerifiedUserEmailDomainAsync_WithNonExistentUser_ReturnsEmpty(
IOrganizationRepository organizationRepository)
{
var nonExistentUserId = Guid.NewGuid();
var result = await organizationRepository.GetByVerifiedUserEmailDomainAsync(nonExistentUserId);
Assert.Empty(result);
}
}

View File

@ -253,6 +253,9 @@ public class OrganizationUserRepositoryTests
Assert.Equal(orgUser1.Permissions, result.Permissions);
Assert.Equal(organization.SmSeats, result.SmSeats);
Assert.Equal(organization.SmServiceAccounts, result.SmServiceAccounts);
Assert.Equal(organization.LimitCollectionCreation, result.LimitCollectionCreation);
Assert.Equal(organization.LimitCollectionDeletion, result.LimitCollectionDeletion);
// Deprecated: https://bitwarden.atlassian.net/browse/PM-10863
Assert.Equal(organization.LimitCollectionCreationDeletion, result.LimitCollectionCreationDeletion);
Assert.Equal(organization.AllowAdminAccessToAllCollectionItems, result.AllowAdminAccessToAllCollectionItems);
}

View File

@ -0,0 +1,39 @@
CREATE OR ALTER PROCEDURE [dbo].[CollectionCipher_ReadByUserId]
@UserId UNIQUEIDENTIFIER
AS
BEGIN
SET NOCOUNT ON
SELECT
CC.*
FROM
[dbo].[CollectionCipher] CC
INNER JOIN
[dbo].[Collection] S ON S.[Id] = CC.[CollectionId]
INNER JOIN
[dbo].[OrganizationUser] OU ON OU.[OrganizationId] = S.[OrganizationId] AND OU.[UserId] = @UserId
INNER JOIN
[dbo].[CollectionUser] CU ON CU.[CollectionId] = S.[Id] AND CU.[OrganizationUserId] = OU.[Id]
WHERE
OU.[Status] = 2
UNION ALL
SELECT
CC.*
FROM
[dbo].[CollectionCipher] CC
INNER JOIN
[dbo].[Collection] S ON S.[Id] = CC.[CollectionId]
INNER JOIN
[dbo].[OrganizationUser] OU ON OU.[OrganizationId] = S.[OrganizationId] AND OU.[UserId] = @UserId
INNER JOIN
[dbo].[GroupUser] GU ON GU.[OrganizationUserId] = OU.[Id]
INNER JOIN
[dbo].[CollectionGroup] CG ON CG.[CollectionId] = CC.[CollectionId] AND CG.[GroupId] = GU.[GroupId]
LEFT JOIN
[dbo].[CollectionUser] CU ON CU.[CollectionId] = S.[Id] AND CU.[OrganizationUserId] = OU.[Id]
WHERE
OU.[Status] = 2
AND CU.[CollectionId] IS NULL
END