1
0
mirror of https://github.com/bitwarden/server.git synced 2024-11-25 12:45:18 +01:00

Merge branch 'main' into km/pm-10563

This commit is contained in:
Maciej Zieniuk 2024-10-17 11:07:44 +01:00
commit a07e51fe24
No known key found for this signature in database
GPG Key ID: 9CACE59F1272ACD9
87 changed files with 2719 additions and 2114 deletions

View File

@ -3,7 +3,7 @@
"isRoot": true,
"tools": {
"swashbuckle.aspnetcore.cli": {
"version": "6.8.0",
"version": "6.8.1",
"commands": ["swagger"]
},
"dotnet-ef": {

3
.github/CODEOWNERS vendored
View File

@ -26,6 +26,9 @@ util/SqliteMigrations/** @bitwarden/dept-dbops
bitwarden_license/src/Sso @bitwarden/team-auth-dev
src/Identity @bitwarden/team-auth-dev
# Key Management team
**/KeyManagement @bitwarden/team-key-management-dev
**/SecretsManager @bitwarden/team-secrets-manager-dev
**/Tools @bitwarden/team-tools-dev

View File

@ -565,6 +565,39 @@ 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')
runs-on: ubuntu-24.04
needs: build-docker
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 update
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: '_update_ephemeral_tags.yml',
ref: 'main',
inputs: {
ephemeral_env_branch: '${{ github.head_ref }}'
}
})
check-failures:
name: Check for failures

View File

@ -0,0 +1,249 @@
name: Repository management
on:
workflow_dispatch:
inputs:
branch_to_cut:
default: "rc"
description: "Branch to cut"
options:
- "rc"
- "hotfix-rc"
required: true
type: choice
target_ref:
default: "main"
description: "Branch/Tag to target for cut"
required: true
type: string
version_number_override:
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
required: false
type: string
jobs:
cut_branch:
name: Cut branch
runs-on: ubuntu-22.04
steps:
- name: Check out target ref
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
with:
ref: ${{ inputs.target_ref }}
- name: Check if ${{ inputs.branch_to_cut }} branch exists
env:
BRANCH_NAME: ${{ inputs.branch_to_cut }}
run: |
if [[ $(git ls-remote --heads origin $BRANCH_NAME) ]]; then
echo "$BRANCH_NAME already exists! Please delete $BRANCH_NAME before running again." >> $GITHUB_STEP_SUMMARY
exit 1
fi
- name: Cut branch
env:
BRANCH_NAME: ${{ inputs.branch_to_cut }}
run: |
git switch --quiet --create $BRANCH_NAME
git push --quiet --set-upstream origin $BRANCH_NAME
bump_version:
name: Bump Version
runs-on: ubuntu-22.04
needs: cut_branch
outputs:
version: ${{ steps.set-final-version-output.outputs.version }}
steps:
- name: Validate version input format
if: ${{ inputs.version_number_override != '' }}
uses: bitwarden/gh-actions/version-check@main
with:
version: ${{ inputs.version_number_override }}
- name: Check out branch
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
with:
ref: main
- name: Install xmllint
run: |
sudo apt-get update
sudo apt-get install -y libxml2-utils
- name: Get current version
id: current-version
run: |
CURRENT_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
- name: Verify input version
if: ${{ inputs.version_number_override != '' }}
env:
CURRENT_VERSION: ${{ steps.current-version.outputs.version }}
NEW_VERSION: ${{ inputs.version_number_override }}
run: |
# Error if version has not changed.
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
echo "Specified override version is the same as the current version." >> $GITHUB_STEP_SUMMARY
exit 1
fi
# Check if version is newer.
printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V
if [ $? -eq 0 ]; then
echo "Version is newer than the current version."
else
echo "Version is older than the current version." >> $GITHUB_STEP_SUMMARY
exit 1
fi
- name: Calculate next release version
if: ${{ inputs.version_number_override == '' }}
id: calculate-next-version
uses: bitwarden/gh-actions/version-next@main
with:
version: ${{ steps.current-version.outputs.version }}
- name: Bump version props - Version Override
if: ${{ inputs.version_number_override != '' }}
id: bump-version-override
uses: bitwarden/gh-actions/version-bump@main
with:
file_path: "Directory.Build.props"
version: ${{ inputs.version_number_override }}
- name: Bump version props - Automatic Calculation
if: ${{ inputs.version_number_override == '' }}
id: bump-version-automatic
uses: bitwarden/gh-actions/version-bump@main
with:
file_path: "Directory.Build.props"
version: ${{ steps.calculate-next-version.outputs.version }}
- name: Set final version output
id: set-final-version-output
run: |
if [[ "${{ steps.bump-version-override.outcome }}" = "success" ]]; then
echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-version-automatic.outcome }}" = "success" ]]; then
echo "version=${{ steps.calculate-next-version.outputs.version }}" >> $GITHUB_OUTPUT
fi
- name: Configure Git
run: |
git config --local user.email "actions@github.com"
git config --local user.name "Github Actions"
- name: Commit files
run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a
- name: Push changes
run: |
git pull -pt
git push
cherry_pick:
name: Cherry-Pick Commit(s)
runs-on: ubuntu-22.04
needs: bump_version
steps:
- name: Check out main branch
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
with:
ref: main
- name: Install xmllint
run: |
sudo apt-get update
sudo apt-get install -y libxml2-utils
- name: Verify version has been updated
env:
NEW_VERSION: ${{ needs.bump_version.outputs.version }}
run: |
# Wait for version to change.
while : ; do
echo "Waiting for version to be updated..."
git pull --force
CURRENT_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
# If the versions don't match we continue the loop, otherwise we break out of the loop.
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break
sleep 10
done
- name: Get last version commit(s)
id: get-commits
run: |
git switch main
MAIN_COMMIT=$(git log --reverse --pretty=format:"%H" --max-count=1 Directory.Build.props)
echo "main_commit=$MAIN_COMMIT" >> $GITHUB_OUTPUT
if [[ $(git ls-remote --heads origin rc) ]]; then
git switch rc
RC_COMMIT=$(git log --reverse --pretty=format:"%H" --max-count=1 Directory.Build.props)
echo "rc_commit=$RC_COMMIT" >> $GITHUB_OUTPUT
RC_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
echo "rc_version=$RC_VERSION" >> $GITHUB_OUTPUT
fi
- name: Configure Git
run: |
git config --local user.email "actions@github.com"
git config --local user.name "Github Actions"
- name: Perform cherry-pick(s)
env:
CUT_BRANCH: ${{ inputs.branch_to_cut }}
MAIN_COMMIT: ${{ steps.get-commits.outputs.main_commit }}
RC_COMMIT: ${{ steps.get-commits.outputs.rc_commit }}
RC_VERSION: ${{ steps.get-commits.outputs.rc_version }}
run: |
# If we are cutting 'hotfix-rc':
if [[ "$CUT_BRANCH" == "hotfix-rc" ]]; then
# If the 'rc' branch exists:
if [[ $(git ls-remote --heads origin rc) ]]; then
# Chery-pick from 'rc' into 'hotfix-rc'
git switch hotfix-rc
HOTFIX_RC_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
if [[ "$HOTFIX_RC_VERSION" != "$RC_VERSION" ]]; then
git cherry-pick --strategy-option=theirs -x $RC_COMMIT
git push -u origin hotfix-rc
fi
# Cherry-pick from 'main' into 'rc'
git switch rc
git cherry-pick --strategy-option=theirs -x $MAIN_COMMIT
git push -u origin rc
# If the 'rc' branch does not exist:
else
# Cherry-pick from 'main' into 'hotfix-rc'
git switch hotfix-rc
git cherry-pick --strategy-option=theirs -x $MAIN_COMMIT
git push -u origin hotfix-rc
fi
# If we are cutting 'rc':
elif [[ "$CUT_BRANCH" == "rc" ]]; then
# Cherry-pick from 'main' into 'rc'
git switch rc
git cherry-pick --strategy-option=theirs -x $MAIN_COMMIT
git push -u origin rc
fi
move_future_db_scripts:
name: Move finalization database scripts
needs: cherry_pick
uses: ./.github/workflows/_move_finalization_db_scripts.yml
secrets: inherit

View File

@ -1,266 +0,0 @@
---
name: Version Bump
on:
workflow_dispatch:
inputs:
version_number_override:
description: "New version override (leave blank for automatic calculation, example: '2024.1.0')"
required: false
type: string
cut_rc_branch:
description: "Cut RC branch?"
default: true
type: boolean
enable_slack_notification:
description: "Enable Slack notifications for upcoming release?"
default: false
type: boolean
jobs:
bump_version:
name: Bump Version
runs-on: ubuntu-22.04
outputs:
version: ${{ steps.set-final-version-output.outputs.version }}
steps:
- name: Validate version input
if: ${{ inputs.version_number_override != '' }}
uses: bitwarden/gh-actions/version-check@main
with:
version: ${{ inputs.version_number_override }}
- name: Slack Notification Check
run: |
if [[ "${{ inputs.enable_slack_notification }}" == true ]]; then
echo "Slack notifications enabled."
else
echo "Slack notifications disabled."
fi
- name: Check out branch
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
- name: Check if RC branch exists
if: ${{ inputs.cut_rc_branch == true }}
run: |
remote_rc_branch_check=$(git ls-remote --heads origin rc | wc -l)
if [[ "${remote_rc_branch_check}" -gt 0 ]]; then
echo "Remote RC branch exists."
echo "Please delete current RC branch before running again."
exit 1
fi
- name: Log in to Azure - CI subscription
uses: Azure/login@e15b166166a8746d1a47596803bd8c1b595455cf # v1.6.0
with:
creds: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
- name: Retrieve secrets
id: retrieve-secrets
uses: bitwarden/gh-actions/get-keyvault-secrets@main
with:
keyvault: "bitwarden-ci"
secrets: "github-gpg-private-key,
github-gpg-private-key-passphrase"
- name: Import GPG key
uses: crazy-max/ghaction-import-gpg@01dd5d3ca463c7f10f7f4f7b4f177225ac661ee4 # v6.1.0
with:
gpg_private_key: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key }}
passphrase: ${{ steps.retrieve-secrets.outputs.github-gpg-private-key-passphrase }}
git_user_signingkey: true
git_commit_gpgsign: true
- name: Set up Git
run: |
git config --local user.email "106330231+bitwarden-devops-bot@users.noreply.github.com"
git config --local user.name "bitwarden-devops-bot"
- name: Create version branch
id: create-branch
run: |
NAME=version_bump_${{ github.ref_name }}_$(date +"%Y-%m-%d")
git switch -c $NAME
echo "name=$NAME" >> $GITHUB_OUTPUT
- name: Install xmllint
run: |
sudo apt-get update
sudo apt-get install -y libxml2-utils
- name: Get current version
id: current-version
run: |
CURRENT_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
echo "version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
- name: Verify input version
if: ${{ inputs.version_number_override != '' }}
env:
CURRENT_VERSION: ${{ steps.current-version.outputs.version }}
NEW_VERSION: ${{ inputs.version_number_override }}
run: |
# Error if version has not changed.
if [[ "$NEW_VERSION" == "$CURRENT_VERSION" ]]; then
echo "Version has not changed."
exit 1
fi
# Check if version is newer.
printf '%s\n' "${CURRENT_VERSION}" "${NEW_VERSION}" | sort -C -V
if [ $? -eq 0 ]; then
echo "Version check successful."
else
echo "Version check failed."
exit 1
fi
- name: Calculate next release version
if: ${{ inputs.version_number_override == '' }}
id: calculate-next-version
uses: bitwarden/gh-actions/version-next@main
with:
version: ${{ steps.current-version.outputs.version }}
- name: Bump version props - Version Override
if: ${{ inputs.version_number_override != '' }}
id: bump-version-override
uses: bitwarden/gh-actions/version-bump@main
with:
file_path: "Directory.Build.props"
version: ${{ inputs.version_number_override }}
- name: Bump version props - Automatic Calculation
if: ${{ inputs.version_number_override == '' }}
id: bump-version-automatic
uses: bitwarden/gh-actions/version-bump@main
with:
file_path: "Directory.Build.props"
version: ${{ steps.calculate-next-version.outputs.version }}
- name: Set final version output
id: set-final-version-output
run: |
if [[ "${{ steps.bump-version-override.outcome }}" = "success" ]]; then
echo "version=${{ inputs.version_number_override }}" >> $GITHUB_OUTPUT
elif [[ "${{ steps.bump-version-automatic.outcome }}" = "success" ]]; then
echo "version=${{ steps.calculate-next-version.outputs.version }}" >> $GITHUB_OUTPUT
fi
- name: Check if version changed
id: version-changed
run: |
if [ -n "$(git status --porcelain)" ]; then
echo "changes_to_commit=TRUE" >> $GITHUB_OUTPUT
else
echo "changes_to_commit=FALSE" >> $GITHUB_OUTPUT
echo "No changes to commit!";
fi
- name: Commit files
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
run: git commit -m "Bumped version to ${{ steps.set-final-version-output.outputs.version }}" -a
- name: Push changes
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
run: git push -u origin $PR_BRANCH
- name: Generate GH App token
uses: actions/create-github-app-token@5d869da34e18e7287c1daad50e0b8ea0f506ce69 # v1.11.0
id: app-token
with:
app-id: ${{ secrets.BW_GHAPP_ID }}
private-key: ${{ secrets.BW_GHAPP_KEY }}
owner: ${{ github.repository_owner }}
- name: Create version PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
id: create-pr
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_BRANCH: ${{ steps.create-branch.outputs.name }}
TITLE: "Bump version to ${{ steps.set-final-version-output.outputs.version }}"
run: |
PR_URL=$(gh pr create --title "$TITLE" \
--base "main" \
--head "$PR_BRANCH" \
--label "version update" \
--label "automated pr" \
--body "
## Type of change
- [ ] Bug fix
- [ ] New feature development
- [ ] Tech debt (refactoring, code cleanup, dependency upgrades, etc)
- [ ] Build/deploy pipeline (DevOps)
- [X] Other
## Objective
Automated version bump to ${{ steps.set-final-version-output.outputs.version }}")
echo "pr_number=${PR_URL##*/}" >> $GITHUB_OUTPUT
- name: Approve PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr review $PR_NUMBER --approve
- name: Merge PR
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' }}
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
PR_NUMBER: ${{ steps.create-pr.outputs.pr_number }}
run: gh pr merge $PR_NUMBER --squash --auto --delete-branch
- name: Report upcoming release version to Slack
if: ${{ steps.version-changed.outputs.changes_to_commit == 'TRUE' && inputs.enable_slack_notification == true }}
uses: bitwarden/gh-actions/report-upcoming-release-version@main
with:
version: ${{ steps.set-final-version-output.outputs.version }}
project: ${{ github.repository }}
AZURE_KV_CI_SERVICE_PRINCIPAL: ${{ secrets.AZURE_KV_CI_SERVICE_PRINCIPAL }}
cut_rc:
name: Cut RC branch
if: ${{ inputs.cut_rc_branch == true }}
needs: bump_version
runs-on: ubuntu-22.04
steps:
- name: Check out branch
uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0
with:
ref: main
- name: Install xmllint
run: |
sudo apt-get update
sudo apt-get install -y libxml2-utils
- name: Verify version has been updated
env:
NEW_VERSION: ${{ needs.bump_version.outputs.version }}
run: |
# Wait for version to change.
while : ; do
echo "Waiting for version to be updated..."
git pull --force
CURRENT_VERSION=$(xmllint -xpath "/Project/PropertyGroup/Version/text()" Directory.Build.props)
# If the versions don't match we continue the loop, otherwise we break out of the loop.
[[ "$NEW_VERSION" != "$CURRENT_VERSION" ]] || break
sleep 10
done
- name: Cut RC branch
run: |
git switch --quiet --create rc
git push --quiet --set-upstream origin rc
move-future-db-scripts:
name: Move finalization database scripts
needs: cut_rc
uses: ./.github/workflows/_move_finalization_db_scripts.yml
secrets: inherit

View File

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

View File

@ -2,6 +2,7 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants;
@ -27,6 +28,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
private readonly IFeatureService _featureService;
private readonly IProviderBillingService _providerBillingService;
private readonly ISubscriberService _subscriberService;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
public RemoveOrganizationFromProviderCommand(
IEventService eventService,
@ -37,7 +39,8 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
IStripeAdapter stripeAdapter,
IFeatureService featureService,
IProviderBillingService providerBillingService,
ISubscriberService subscriberService)
ISubscriberService subscriberService,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery)
{
_eventService = eventService;
_mailService = mailService;
@ -48,6 +51,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
_featureService = featureService;
_providerBillingService = providerBillingService;
_subscriberService = subscriberService;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
}
public async Task RemoveOrganizationFromProvider(
@ -63,7 +67,7 @@ public class RemoveOrganizationFromProviderCommand : IRemoveOrganizationFromProv
throw new BadRequestException("Failed to remove organization. Please contact support.");
}
if (!await _organizationService.HasConfirmedOwnersExceptAsync(
if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId,
Array.Empty<Guid>(),
includeProvider: false))

View File

@ -17,7 +17,6 @@ namespace Bit.Scim.Controllers.v2;
[ExceptionHandlerFilter]
public class UsersController : Controller
{
private readonly IUserService _userService;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationService _organizationService;
private readonly IGetUsersListQuery _getUsersListQuery;
@ -27,7 +26,6 @@ public class UsersController : Controller
private readonly ILogger<UsersController> _logger;
public UsersController(
IUserService userService,
IOrganizationUserRepository organizationUserRepository,
IOrganizationService organizationService,
IGetUsersListQuery getUsersListQuery,
@ -36,7 +34,6 @@ public class UsersController : Controller
IPostUserCommand postUserCommand,
ILogger<UsersController> logger)
{
_userService = userService;
_organizationUserRepository = organizationUserRepository;
_organizationService = organizationService;
_getUsersListQuery = getUsersListQuery;
@ -98,7 +95,7 @@ public class UsersController : Controller
if (model.Active && orgUser.Status == OrganizationUserStatusType.Revoked)
{
await _organizationService.RestoreUserAsync(orgUser, EventSystemUser.SCIM, _userService);
await _organizationService.RestoreUserAsync(orgUser, EventSystemUser.SCIM);
}
else if (!model.Active && orgUser.Status != OrganizationUserStatusType.Revoked)
{

View File

@ -69,6 +69,7 @@ public class Startup
// Services
services.AddBaseServices(globalSettings);
services.AddDefaultServices(globalSettings);
services.AddDistributedCache(globalSettings);
services.AddBillingOperations();
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();

View File

@ -9,18 +9,15 @@ namespace Bit.Scim.Users;
public class PatchUserCommand : IPatchUserCommand
{
private readonly IUserService _userService;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationService _organizationService;
private readonly ILogger<PatchUserCommand> _logger;
public PatchUserCommand(
IUserService userService,
IOrganizationUserRepository organizationUserRepository,
IOrganizationService organizationService,
ILogger<PatchUserCommand> logger)
{
_userService = userService;
_organizationUserRepository = organizationUserRepository;
_organizationService = organizationService;
_logger = logger;
@ -74,7 +71,7 @@ public class PatchUserCommand : IPatchUserCommand
{
if (active && orgUser.Status == OrganizationUserStatusType.Revoked)
{
await _organizationService.RestoreUserAsync(orgUser, EventSystemUser.SCIM, _userService);
await _organizationService.RestoreUserAsync(orgUser, EventSystemUser.SCIM);
return true;
}
else if (!active && orgUser.Status != OrganizationUserStatusType.Revoked)

View File

@ -9,7 +9,7 @@
"version": "0.0.0",
"license": "-",
"dependencies": {
"bootstrap": "5.3.3",
"bootstrap": "4.6.2",
"font-awesome": "4.7.0",
"jquery": "3.7.1",
"popper.js": "1.16.1"
@ -18,9 +18,9 @@
"css-loader": "7.1.2",
"expose-loader": "5.0.0",
"mini-css-extract-plugin": "2.9.1",
"sass": "1.77.8",
"sass-loader": "16.0.1",
"webpack": "5.94.0",
"sass": "1.79.5",
"sass-loader": "16.0.2",
"webpack": "5.95.0",
"webpack-cli": "5.1.4"
}
},
@ -98,10 +98,296 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@parcel/watcher": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz",
"integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^1.0.3",
"is-glob": "^4.0.3",
"micromatch": "^4.0.5",
"node-addon-api": "^7.0.0"
},
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.4.1",
"@parcel/watcher-darwin-arm64": "2.4.1",
"@parcel/watcher-darwin-x64": "2.4.1",
"@parcel/watcher-freebsd-x64": "2.4.1",
"@parcel/watcher-linux-arm-glibc": "2.4.1",
"@parcel/watcher-linux-arm64-glibc": "2.4.1",
"@parcel/watcher-linux-arm64-musl": "2.4.1",
"@parcel/watcher-linux-x64-glibc": "2.4.1",
"@parcel/watcher-linux-x64-musl": "2.4.1",
"@parcel/watcher-win32-arm64": "2.4.1",
"@parcel/watcher-win32-ia32": "2.4.1",
"@parcel/watcher-win32-x64": "2.4.1"
}
},
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz",
"integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz",
"integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-x64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz",
"integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz",
"integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz",
"integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz",
"integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz",
"integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz",
"integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz",
"integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz",
"integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-ia32": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz",
"integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz",
"integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true,
"license": "MIT"
},
@ -113,13 +399,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.1.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz",
"integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==",
"version": "22.7.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
"integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.13.0"
"undici-types": "~6.19.2"
}
},
"node_modules/@webassemblyjs/ast": {
@ -415,35 +701,8 @@
"ajv": "^8.8.2"
}
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bootstrap": {
"version": "5.3.3",
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz",
"integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==",
"funding": [
@ -476,9 +735,9 @@
}
},
"node_modules/browserslist": {
"version": "4.23.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
"integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz",
"integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==",
"dev": true,
"funding": [
{
@ -496,8 +755,8 @@
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001646",
"electron-to-chromium": "^1.5.4",
"caniuse-lite": "^1.0.30001663",
"electron-to-chromium": "^1.5.28",
"node-releases": "^2.0.18",
"update-browserslist-db": "^1.1.0"
},
@ -516,9 +775,9 @@
"license": "MIT"
},
"node_modules/caniuse-lite": {
"version": "1.0.30001651",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
"integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
"version": "1.0.30001668",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz",
"integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==",
"dev": true,
"funding": [
{
@ -537,28 +796,19 @@
"license": "CC-BY-4.0"
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 8.10.0"
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/chrome-trace-event": {
@ -664,10 +914,23 @@
"node": ">=4"
}
},
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"detect-libc": "bin/detect-libc.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.5.tgz",
"integrity": "sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==",
"version": "1.5.36",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.36.tgz",
"integrity": "sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==",
"dev": true,
"license": "ISC"
},
@ -686,9 +949,9 @@
}
},
"node_modules/envinfo": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz",
"integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz",
"integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==",
"dev": true,
"license": "MIT",
"bin": {
@ -706,9 +969,9 @@
"license": "MIT"
},
"node_modules/escalade": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
@ -804,9 +1067,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz",
"integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz",
"integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==",
"dev": true,
"license": "MIT"
},
@ -866,21 +1129,6 @@
"node": ">=0.10.3"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -891,19 +1139,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
@ -991,23 +1226,10 @@
"node": ">=10.13.0"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-core-module": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz",
"integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==",
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1158,6 +1380,20 @@
"dev": true,
"license": "MIT"
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@ -1228,6 +1464,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true,
"license": "MIT"
},
"node_modules/node-releases": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
@ -1235,16 +1478,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
@ -1312,9 +1545,9 @@
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
"dev": true,
"license": "ISC"
},
@ -1356,9 +1589,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.41",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"dev": true,
"funding": [
{
@ -1377,8 +1610,8 @@
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.1",
"source-map-js": "^1.2.0"
"picocolors": "^1.1.0",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
@ -1448,9 +1681,9 @@
}
},
"node_modules/postcss-selector-parser": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz",
"integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==",
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1489,16 +1722,17 @@
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz",
"integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
"node": ">= 14.16.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/rechoir": {
@ -1587,13 +1821,14 @@
"license": "MIT"
},
"node_modules/sass": {
"version": "1.77.8",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz",
"integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==",
"version": "1.79.5",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.79.5.tgz",
"integrity": "sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"@parcel/watcher": "^2.4.1",
"chokidar": "^4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
@ -1605,9 +1840,9 @@
}
},
"node_modules/sass-loader": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.1.tgz",
"integrity": "sha512-xACl1ToTsKnL9Ce5yYpRxrLj9QUDCnwZNhzpC7tKiFyA8zXsd3Ap+HGVnbCgkdQcm43E+i6oKAWBsvGA6ZoiMw==",
"version": "16.0.2",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.2.tgz",
"integrity": "sha512-Ll6iXZ1EYwYT19SqW4mSBb76vSSi8JgzElmzIerhEGgzB5hRjDQIWsPmuk1UrAXkR16KJHqVY0eH+5/uw9Tmfw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1735,9 +1970,9 @@
}
},
"node_modules/source-map-js": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
@ -1795,9 +2030,9 @@
}
},
"node_modules/terser": {
"version": "5.31.5",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.31.5.tgz",
"integrity": "sha512-YPmas0L0rE1UyLL/llTWA0SiDOqIcAQYLeUj7cJYzXHlRTAnMSg9pPe4VJ5PlKvTrPQsdVFuiRiwyeNlYgwh2Q==",
"version": "5.34.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.34.1.tgz",
"integrity": "sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
@ -1915,16 +2150,16 @@
}
},
"node_modules/undici-types": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz",
"integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==",
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true,
"license": "MIT"
},
"node_modules/update-browserslist-db": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
"integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
"integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
"dev": true,
"funding": [
{
@ -1942,8 +2177,8 @@
],
"license": "MIT",
"dependencies": {
"escalade": "^3.1.2",
"picocolors": "^1.0.1"
"escalade": "^3.2.0",
"picocolors": "^1.1.0"
},
"bin": {
"update-browserslist-db": "cli.js"
@ -1970,9 +2205,9 @@
"license": "MIT"
},
"node_modules/watchpack": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",
"integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==",
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
"integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1984,9 +2219,9 @@
}
},
"node_modules/webpack": {
"version": "5.94.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
"integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
"version": "5.95.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz",
"integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@ -8,7 +8,7 @@
"build": "webpack"
},
"dependencies": {
"bootstrap": "5.3.3",
"bootstrap": "4.6.2",
"font-awesome": "4.7.0",
"jquery": "3.7.1",
"popper.js": "1.16.1"
@ -17,9 +17,9 @@
"css-loader": "7.1.2",
"expose-loader": "5.0.0",
"mini-css-extract-plugin": "2.9.1",
"sass": "1.77.8",
"sass-loader": "16.0.1",
"webpack": "5.94.0",
"sass": "1.79.5",
"sass-loader": "16.0.2",
"webpack": "5.95.0",
"webpack-cli": "5.1.4"
}
}

View File

@ -3,6 +3,7 @@ using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Billing.Constants;
using Bit.Core.Billing.Enums;
@ -75,7 +76,7 @@ public class RemoveOrganizationFromProviderCommandTests
{
providerOrganization.ProviderId = provider.Id;
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId,
[],
includeProvider: false)
@ -98,7 +99,7 @@ public class RemoveOrganizationFromProviderCommandTests
organization.GatewayCustomerId = null;
organization.GatewaySubscriptionId = null;
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId,
[],
includeProvider: false)
@ -141,7 +142,7 @@ public class RemoveOrganizationFromProviderCommandTests
{
providerOrganization.ProviderId = provider.Id;
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId,
[],
includeProvider: false)
@ -208,7 +209,7 @@ public class RemoveOrganizationFromProviderCommandTests
var teamsMonthlyPlan = StaticStore.GetPlan(PlanType.TeamsMonthly);
sutProvider.GetDependency<IOrganizationService>().HasConfirmedOwnersExceptAsync(
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().HasConfirmedOwnersExceptAsync(
providerOrganization.OrganizationId,
[],
includeProvider: false)

View File

@ -43,7 +43,7 @@ public class PatchUserCommandTests
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
await sutProvider.GetDependency<IOrganizationService>().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM, Arg.Any<IUserService>());
await sutProvider.GetDependency<IOrganizationService>().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM);
}
[Theory]
@ -71,7 +71,7 @@ public class PatchUserCommandTests
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
await sutProvider.GetDependency<IOrganizationService>().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM, Arg.Any<IUserService>());
await sutProvider.GetDependency<IOrganizationService>().Received(1).RestoreUserAsync(organizationUser, EventSystemUser.SCIM);
}
[Theory]
@ -147,7 +147,7 @@ public class PatchUserCommandTests
await sutProvider.Sut.PatchUserAsync(organizationUser.OrganizationId, organizationUser.Id, scimPatchModel);
await sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs().RestoreUserAsync(default, EventSystemUser.SCIM, default);
await sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs().RestoreUserAsync(default, EventSystemUser.SCIM);
await sutProvider.GetDependency<IOrganizationService>().DidNotReceiveWithAnyArgs().RevokeUserAsync(default, EventSystemUser.SCIM);
}

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

@ -7,7 +7,6 @@ using Bit.Core;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Providers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Billing.Extensions;
using Bit.Core.Billing.Services;
using Bit.Core.Context;
@ -57,7 +56,6 @@ public class OrganizationsController : Controller
private readonly IRemoveOrganizationFromProviderCommand _removeOrganizationFromProviderCommand;
private readonly IFeatureService _featureService;
private readonly IProviderBillingService _providerBillingService;
private readonly IPolicyService _policyService;
public OrganizationsController(
IOrganizationService organizationService,
@ -84,8 +82,7 @@ public class OrganizationsController : Controller
IProviderOrganizationRepository providerOrganizationRepository,
IRemoveOrganizationFromProviderCommand removeOrganizationFromProviderCommand,
IFeatureService featureService,
IProviderBillingService providerBillingService,
IPolicyService policyService)
IProviderBillingService providerBillingService)
{
_organizationService = organizationService;
_organizationRepository = organizationRepository;
@ -112,7 +109,6 @@ public class OrganizationsController : Controller
_removeOrganizationFromProviderCommand = removeOrganizationFromProviderCommand;
_featureService = featureService;
_providerBillingService = providerBillingService;
_policyService = policyService;
}
[RequirePermission(Permission.Org_List_View)]
@ -440,13 +436,6 @@ public class OrganizationsController : Controller
organization.MaxAutoscaleSmServiceAccounts = model.MaxAutoscaleSmServiceAccounts;
}
var plan = StaticStore.GetPlan(organization.PlanType);
if (!organization.UsePolicies || !plan.HasPolicies)
{
await DisableOrganizationPoliciesAsync(organization.Id);
}
if (_accessControlService.UserHasPermission(Permission.Org_Licensing_Edit))
{
organization.LicenseKey = model.LicenseKey;
@ -463,18 +452,4 @@ public class OrganizationsController : Controller
return organization;
}
private async Task DisableOrganizationPoliciesAsync(Guid organizationId)
{
var policies = await _policyRepository.GetManyByOrganizationIdAsync(organizationId);
if (policies.Count != 0)
{
await Task.WhenAll(policies.Select(async policy =>
{
policy.Enabled = false;
await _policyService.SaveAsync(policy, _userService, _organizationService, null);
}));
}
}
}

View File

@ -4,7 +4,6 @@ using Bit.Admin.Enums;
using Bit.Admin.Models;
using Bit.Admin.Services;
using Bit.Admin.Utilities;
using Bit.Core;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Repositories;
using Bit.Core.Services;
@ -25,8 +24,6 @@ public class UsersController : Controller
private readonly GlobalSettings _globalSettings;
private readonly IAccessControlService _accessControlService;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IFeatureService _featureService;
private readonly IUserService _userService;
public UsersController(
IUserRepository userRepository,
@ -34,9 +31,7 @@ public class UsersController : Controller
IPaymentService paymentService,
GlobalSettings globalSettings,
IAccessControlService accessControlService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IFeatureService featureService,
IUserService userService)
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
{
_userRepository = userRepository;
_cipherRepository = cipherRepository;
@ -44,8 +39,6 @@ public class UsersController : Controller
_globalSettings = globalSettings;
_accessControlService = accessControlService;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_featureService = featureService;
_userService = userService;
}
[RequirePermission(Permission.User_List_View)]
@ -64,22 +57,8 @@ public class UsersController : Controller
var skip = (page - 1) * count;
var users = await _userRepository.SearchAsync(email, skip, count);
var userModels = new List<UserViewModel>();
if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization))
{
var twoFactorAuthLookup = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(users.Select(u => u.Id))).ToList();
userModels = UserViewModel.MapViewModels(users, twoFactorAuthLookup).ToList();
}
else
{
foreach (var user in users)
{
var isTwoFactorEnabled = await _userService.TwoFactorIsEnabledAsync(user);
userModels.Add(UserViewModel.MapViewModel(user, isTwoFactorEnabled));
}
}
var twoFactorAuthLookup = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(users.Select(u => u.Id))).ToList();
var userModels = UserViewModel.MapViewModels(users, twoFactorAuthLookup).ToList();
return View(new UsersModel
{

View File

@ -9,7 +9,7 @@
"version": "0.0.0",
"license": "GPL-3.0",
"dependencies": {
"bootstrap": "5.3.3",
"bootstrap": "4.6.2",
"font-awesome": "4.7.0",
"jquery": "3.7.1",
"popper.js": "1.16.1",
@ -19,9 +19,9 @@
"css-loader": "7.1.2",
"expose-loader": "5.0.0",
"mini-css-extract-plugin": "2.9.1",
"sass": "1.77.8",
"sass-loader": "16.0.1",
"webpack": "5.94.0",
"sass": "1.79.5",
"sass-loader": "16.0.2",
"webpack": "5.95.0",
"webpack-cli": "5.1.4"
}
},
@ -99,10 +99,296 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@parcel/watcher": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz",
"integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^1.0.3",
"is-glob": "^4.0.3",
"micromatch": "^4.0.5",
"node-addon-api": "^7.0.0"
},
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
},
"optionalDependencies": {
"@parcel/watcher-android-arm64": "2.4.1",
"@parcel/watcher-darwin-arm64": "2.4.1",
"@parcel/watcher-darwin-x64": "2.4.1",
"@parcel/watcher-freebsd-x64": "2.4.1",
"@parcel/watcher-linux-arm-glibc": "2.4.1",
"@parcel/watcher-linux-arm64-glibc": "2.4.1",
"@parcel/watcher-linux-arm64-musl": "2.4.1",
"@parcel/watcher-linux-x64-glibc": "2.4.1",
"@parcel/watcher-linux-x64-musl": "2.4.1",
"@parcel/watcher-win32-arm64": "2.4.1",
"@parcel/watcher-win32-ia32": "2.4.1",
"@parcel/watcher-win32-x64": "2.4.1"
}
},
"node_modules/@parcel/watcher-android-arm64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz",
"integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-arm64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz",
"integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-darwin-x64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz",
"integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-freebsd-x64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz",
"integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm-glibc": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz",
"integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-glibc": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz",
"integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-arm64-musl": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz",
"integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-glibc": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz",
"integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-linux-x64-musl": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz",
"integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-arm64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz",
"integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-ia32": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz",
"integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@parcel/watcher-win32-x64": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz",
"integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/parcel"
}
},
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true,
"license": "MIT"
},
@ -114,13 +400,13 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.1.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz",
"integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==",
"version": "22.7.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
"integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.13.0"
"undici-types": "~6.19.2"
}
},
"node_modules/@webassemblyjs/ast": {
@ -416,35 +702,8 @@
"ajv": "^8.8.2"
}
},
"node_modules/anymatch": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz",
"integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==",
"dev": true,
"license": "ISC",
"dependencies": {
"normalize-path": "^3.0.0",
"picomatch": "^2.0.4"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bootstrap": {
"version": "5.3.3",
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.6.2.tgz",
"integrity": "sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==",
"funding": [
@ -477,9 +736,9 @@
}
},
"node_modules/browserslist": {
"version": "4.23.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
"integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz",
"integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==",
"dev": true,
"funding": [
{
@ -497,8 +756,8 @@
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001646",
"electron-to-chromium": "^1.5.4",
"caniuse-lite": "^1.0.30001663",
"electron-to-chromium": "^1.5.28",
"node-releases": "^2.0.18",
"update-browserslist-db": "^1.1.0"
},
@ -517,9 +776,9 @@
"license": "MIT"
},
"node_modules/caniuse-lite": {
"version": "1.0.30001651",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
"integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
"version": "1.0.30001668",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001668.tgz",
"integrity": "sha512-nWLrdxqCdblixUO+27JtGJJE/txpJlyUy5YN1u53wLZkP0emYCo5zgS6QYft7VUYR42LGgi/S5hdLZTrnyIddw==",
"dev": true,
"funding": [
{
@ -538,28 +797,19 @@
"license": "CC-BY-4.0"
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz",
"integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
"readdirp": "^4.0.1"
},
"engines": {
"node": ">= 8.10.0"
"node": ">= 14.16.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/chrome-trace-event": {
@ -665,10 +915,23 @@
"node": ">=4"
}
},
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
"integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"detect-libc": "bin/detect-libc.js"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.5.tgz",
"integrity": "sha512-QR7/A7ZkMS8tZuoftC/jfqNkZLQO779SSW3YuZHP4eXpj3EffGLFcB/Xu9AAZQzLccTiCV+EmUo3ha4mQ9wnlA==",
"version": "1.5.36",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.36.tgz",
"integrity": "sha512-HYTX8tKge/VNp6FGO+f/uVDmUkq+cEfcxYhKf15Akc4M5yxt5YmorwlAitKWjWhWQnKcDRBAQKXkhqqXMqcrjw==",
"dev": true,
"license": "ISC"
},
@ -687,9 +950,9 @@
}
},
"node_modules/envinfo": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.13.0.tgz",
"integrity": "sha512-cvcaMr7KqXVh4nyzGTVqTum+gAiL265x5jUWQIDLq//zOGbW+gSW/C+OWLleY/rs9Qole6AZLMXPbtIFQbqu+Q==",
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.14.0.tgz",
"integrity": "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg==",
"dev": true,
"license": "MIT",
"bin": {
@ -707,9 +970,9 @@
"license": "MIT"
},
"node_modules/escalade": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
@ -805,9 +1068,9 @@
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz",
"integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.2.tgz",
"integrity": "sha512-GR6f0hD7XXyNJa25Tb9BuIdN0tdr+0BMi6/CJPH3wJO1JjNG3n/VsSw38AwRdKZABm8lGbPfakLRkYzx2V9row==",
"dev": true,
"license": "MIT"
},
@ -867,21 +1130,6 @@
"node": ">=0.10.3"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -892,19 +1140,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz",
@ -992,23 +1227,10 @@
"node": ">=10.13.0"
}
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-core-module": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz",
"integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==",
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1159,6 +1381,20 @@
"dev": true,
"license": "MIT"
},
"node_modules/micromatch": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
"node": ">=8.6"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
@ -1229,6 +1465,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/node-addon-api": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
"integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
"dev": true,
"license": "MIT"
},
"node_modules/node-releases": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
@ -1236,16 +1479,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/normalize-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
@ -1313,9 +1546,9 @@
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
"dev": true,
"license": "ISC"
},
@ -1357,9 +1590,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.41",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz",
"integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==",
"version": "8.4.47",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz",
"integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==",
"dev": true,
"funding": [
{
@ -1378,8 +1611,8 @@
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.1",
"source-map-js": "^1.2.0"
"picocolors": "^1.1.0",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
@ -1449,9 +1682,9 @@
}
},
"node_modules/postcss-selector-parser": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz",
"integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==",
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz",
"integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1490,16 +1723,17 @@
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz",
"integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
"node": ">= 14.16.0"
},
"funding": {
"type": "individual",
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/rechoir": {
@ -1588,13 +1822,14 @@
"license": "MIT"
},
"node_modules/sass": {
"version": "1.77.8",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz",
"integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==",
"version": "1.79.5",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.79.5.tgz",
"integrity": "sha512-W1h5kp6bdhqFh2tk3DsI771MoEJjvrSY/2ihJRJS4pjIyfJCw0nTsxqhnrUzaLMOJjFchj8rOvraI/YUVjtx5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": ">=3.0.0 <4.0.0",
"@parcel/watcher": "^2.4.1",
"chokidar": "^4.0.0",
"immutable": "^4.0.0",
"source-map-js": ">=0.6.2 <2.0.0"
},
@ -1606,9 +1841,9 @@
}
},
"node_modules/sass-loader": {
"version": "16.0.1",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.1.tgz",
"integrity": "sha512-xACl1ToTsKnL9Ce5yYpRxrLj9QUDCnwZNhzpC7tKiFyA8zXsd3Ap+HGVnbCgkdQcm43E+i6oKAWBsvGA6ZoiMw==",
"version": "16.0.2",
"resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.2.tgz",
"integrity": "sha512-Ll6iXZ1EYwYT19SqW4mSBb76vSSi8JgzElmzIerhEGgzB5hRjDQIWsPmuk1UrAXkR16KJHqVY0eH+5/uw9Tmfw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1736,9 +1971,9 @@
}
},
"node_modules/source-map-js": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
"integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
@ -1796,9 +2031,9 @@
}
},
"node_modules/terser": {
"version": "5.31.5",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.31.5.tgz",
"integrity": "sha512-YPmas0L0rE1UyLL/llTWA0SiDOqIcAQYLeUj7cJYzXHlRTAnMSg9pPe4VJ5PlKvTrPQsdVFuiRiwyeNlYgwh2Q==",
"version": "5.34.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.34.1.tgz",
"integrity": "sha512-FsJZ7iZLd/BXkz+4xrRTGJ26o/6VTjQytUk8b8OxkwcD2I+79VPJlz7qss1+zE7h8GNIScFqXcDyJ/KqBYZFVA==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
@ -1924,16 +2159,16 @@
}
},
"node_modules/undici-types": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz",
"integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==",
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true,
"license": "MIT"
},
"node_modules/update-browserslist-db": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
"integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
"integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
"dev": true,
"funding": [
{
@ -1951,8 +2186,8 @@
],
"license": "MIT",
"dependencies": {
"escalade": "^3.1.2",
"picocolors": "^1.0.1"
"escalade": "^3.2.0",
"picocolors": "^1.1.0"
},
"bin": {
"update-browserslist-db": "cli.js"
@ -1979,9 +2214,9 @@
"license": "MIT"
},
"node_modules/watchpack": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz",
"integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==",
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",
"integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1993,9 +2228,9 @@
}
},
"node_modules/webpack": {
"version": "5.94.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.94.0.tgz",
"integrity": "sha512-KcsGn50VT+06JH/iunZJedYGUJS5FGjow8wb9c0v5n1Om8O1g4L6LjtfxwlXIATopoQu+vOXXa7gYisWxCoPyg==",
"version": "5.95.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.95.0.tgz",
"integrity": "sha512-2t3XstrKULz41MNMBF+cJ97TyHdyQ8HCt//pqErqDvNjU9YQBnZxIHa11VXsi7F3mb5/aO2tuDxdeTPdU7xu9Q==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@ -8,7 +8,7 @@
"build": "webpack"
},
"dependencies": {
"bootstrap": "5.3.3",
"bootstrap": "4.6.2",
"font-awesome": "4.7.0",
"jquery": "3.7.1",
"popper.js": "1.16.1",
@ -18,9 +18,9 @@
"css-loader": "7.1.2",
"expose-loader": "5.0.0",
"mini-css-extract-plugin": "2.9.1",
"sass": "1.77.8",
"sass-loader": "16.0.1",
"webpack": "5.94.0",
"sass": "1.79.5",
"sass-loader": "16.0.2",
"webpack": "5.95.0",
"webpack-cli": "5.1.4"
}
}

View File

@ -2,11 +2,13 @@
using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.AdminConsole.Models.Response.Organizations;
using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationDomains.Interfaces;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Utilities;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
@ -133,6 +135,20 @@ public class OrganizationDomainController : Controller
return new OrganizationDomainSsoDetailsResponseModel(ssoResult);
}
[AllowAnonymous]
[HttpPost("domain/sso/verified")]
[RequireFeature(FeatureFlagKeys.VerifiedSsoDomainEndpoint)]
public async Task<VerifiedOrganizationDomainSsoDetailsResponseModel> GetVerifiedOrgDomainSsoDetailsAsync(
[FromBody] OrganizationDomainSsoDetailsRequestModel model)
{
var ssoResults = (await _organizationDomainRepository
.GetVerifiedOrganizationDomainSsoDetailsAsync(model.Email))
.ToList();
return new VerifiedOrganizationDomainSsoDetailsResponseModel(
ssoResults.Select(ssoResult => new VerifiedOrganizationDomainSsoDetailResponseModel(ssoResult)));
}
private async Task ValidateOrganizationAccessAsync(Guid orgIdGuid)
{
if (!await _currentContext.ManageSso(orgIdGuid))

View File

@ -48,13 +48,12 @@ public class OrganizationUsersController : Controller
private readonly IAcceptOrgUserCommand _acceptOrgUserCommand;
private readonly IAuthorizationService _authorizationService;
private readonly IApplicationCacheService _applicationCacheService;
private readonly IFeatureService _featureService;
private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly IOrganizationUserUserDetailsQuery _organizationUserUserDetailsQuery;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly IDeleteManagedOrganizationUserAccountCommand _deleteManagedOrganizationUserAccountCommand;
public OrganizationUsersController(
IOrganizationRepository organizationRepository,
IOrganizationUserRepository organizationUserRepository,
@ -70,10 +69,10 @@ public class OrganizationUsersController : Controller
IAcceptOrgUserCommand acceptOrgUserCommand,
IAuthorizationService authorizationService,
IApplicationCacheService applicationCacheService,
IFeatureService featureService,
ISsoConfigRepository ssoConfigRepository,
IOrganizationUserUserDetailsQuery organizationUserUserDetailsQuery,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IRemoveOrganizationUserCommand removeOrganizationUserCommand,
IDeleteManagedOrganizationUserAccountCommand deleteManagedOrganizationUserAccountCommand)
{
_organizationRepository = organizationRepository;
@ -90,10 +89,10 @@ public class OrganizationUsersController : Controller
_acceptOrgUserCommand = acceptOrgUserCommand;
_authorizationService = authorizationService;
_applicationCacheService = applicationCacheService;
_featureService = featureService;
_ssoConfigRepository = ssoConfigRepository;
_organizationUserUserDetailsQuery = organizationUserUserDetailsQuery;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
_deleteManagedOrganizationUserAccountCommand = deleteManagedOrganizationUserAccountCommand;
}
@ -142,11 +141,6 @@ public class OrganizationUsersController : Controller
throw new NotFoundException();
}
if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization))
{
return await Get_vNext(orgId, includeGroups, includeCollections);
}
var organizationUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails(
new OrganizationUserUserDetailsQueryRequest
{
@ -155,17 +149,15 @@ public class OrganizationUsersController : Controller
IncludeCollections = includeCollections
}
);
var responseTasks = organizationUsers
.Select(async o =>
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers);
var responses = organizationUsers
.Select(o =>
{
var orgUser = new OrganizationUserUserDetailsResponseModel(o,
await _userService.TwoFactorIsEnabledAsync(o));
var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == o.Id).twoFactorIsEnabled;
var orgUser = new OrganizationUserUserDetailsResponseModel(o, userTwoFactorEnabled);
return orgUser;
});
var responses = await Task.WhenAll(responseTasks);
return new ListResponseModel<OrganizationUserUserDetailsResponseModel>(responses);
}
@ -296,7 +288,7 @@ public class OrganizationUsersController : Controller
await _organizationService.InitPendingOrganization(user.Id, orgId, organizationUserId, model.Keys.PublicKey, model.Keys.EncryptedPrivateKey, model.CollectionName);
await _acceptOrgUserCommand.AcceptOrgUserByEmailTokenAsync(organizationUserId, user, model.Token, _userService);
await _organizationService.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id, _userService);
await _organizationService.ConfirmUserAsync(orgId, organizationUserId, model.Key, user.Id);
}
[HttpPost("{organizationUserId}/accept")]
@ -335,8 +327,7 @@ public class OrganizationUsersController : Controller
}
var userId = _userService.GetProperUserId(User);
var result = await _organizationService.ConfirmUserAsync(orgGuidId, new Guid(id), model.Key, userId.Value,
_userService);
var result = await _organizationService.ConfirmUserAsync(orgGuidId, new Guid(id), model.Key, userId.Value);
}
[HttpPost("confirm")]
@ -350,10 +341,7 @@ public class OrganizationUsersController : Controller
}
var userId = _userService.GetProperUserId(User);
var results = _featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization)
? await _organizationService.ConfirmUsersAsync_vNext(orgGuidId, model.ToDictionary(), userId.Value)
: await _organizationService.ConfirmUsersAsync(orgGuidId, model.ToDictionary(), userId.Value,
_userService);
var results = await _organizationService.ConfirmUsersAsync(orgGuidId, model.ToDictionary(), userId.Value);
return new ListResponseModel<OrganizationUserBulkResponseModel>(results.Select(r =>
new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2)));
@ -517,30 +505,28 @@ public class OrganizationUsersController : Controller
[HttpDelete("{id}")]
[HttpPost("{id}/remove")]
public async Task Remove(string orgId, string id)
public async Task Remove(Guid orgId, Guid id)
{
var orgGuidId = new Guid(orgId);
if (!await _currentContext.ManageUsers(orgGuidId))
if (!await _currentContext.ManageUsers(orgId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User);
await _organizationService.RemoveUserAsync(orgGuidId, new Guid(id), userId.Value);
await _removeOrganizationUserCommand.RemoveUserAsync(orgId, id, userId.Value);
}
[HttpDelete("")]
[HttpPost("remove")]
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRemove(string orgId, [FromBody] OrganizationUserBulkRequestModel model)
public async Task<ListResponseModel<OrganizationUserBulkResponseModel>> BulkRemove(Guid orgId, [FromBody] OrganizationUserBulkRequestModel model)
{
var orgGuidId = new Guid(orgId);
if (!await _currentContext.ManageUsers(orgGuidId))
if (!await _currentContext.ManageUsers(orgId))
{
throw new NotFoundException();
}
var userId = _userService.GetProperUserId(User);
var result = await _organizationService.RemoveUsersAsync(orgGuidId, model.Ids, userId.Value);
var result = await _removeOrganizationUserCommand.RemoveUsersAsync(orgId, model.Ids, userId.Value);
return new ListResponseModel<OrganizationUserBulkResponseModel>(result.Select(r =>
new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2)));
}
@ -616,7 +602,7 @@ public class OrganizationUsersController : Controller
[HttpPut("{id}/restore")]
public async Task RestoreAsync(Guid orgId, Guid id)
{
await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _organizationService.RestoreUserAsync(orgUser, userId, _userService));
await RestoreOrRevokeUserAsync(orgId, id, (orgUser, userId) => _organizationService.RestoreUserAsync(orgUser, userId));
}
[HttpPatch("restore")]
@ -696,27 +682,4 @@ public class OrganizationUsersController : Controller
return new ListResponseModel<OrganizationUserBulkResponseModel>(result.Select(r =>
new OrganizationUserBulkResponseModel(r.Item1.Id, r.Item2)));
}
private async Task<ListResponseModel<OrganizationUserUserDetailsResponseModel>> Get_vNext(Guid orgId,
bool includeGroups = false, bool includeCollections = false)
{
var organizationUsers = await _organizationUserUserDetailsQuery.GetOrganizationUserUserDetails(
new OrganizationUserUserDetailsQueryRequest
{
OrganizationId = orgId,
IncludeGroups = includeGroups,
IncludeCollections = includeCollections
}
);
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUsers);
var responses = organizationUsers
.Select(o =>
{
var userTwoFactorEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(u => u.user.Id == o.Id).twoFactorIsEnabled;
var orgUser = new OrganizationUserUserDetailsResponseModel(o, userTwoFactorEnabled);
return orgUser;
});
return new ListResponseModel<OrganizationUserUserDetailsResponseModel>(responses);
}
}

View File

@ -13,6 +13,7 @@ using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories;
@ -55,6 +56,7 @@ public class OrganizationsController : Controller
private readonly IProviderRepository _providerRepository;
private readonly IProviderBillingService _providerBillingService;
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
public OrganizationsController(
IOrganizationRepository organizationRepository,
@ -74,7 +76,8 @@ public class OrganizationsController : Controller
IPushNotificationService pushNotificationService,
IProviderRepository providerRepository,
IProviderBillingService providerBillingService,
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory)
IDataProtectorTokenFactory<OrgDeleteTokenable> orgDeleteTokenDataFactory,
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@ -94,6 +97,7 @@ public class OrganizationsController : Controller
_providerRepository = providerRepository;
_providerBillingService = providerBillingService;
_orgDeleteTokenDataFactory = orgDeleteTokenDataFactory;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
}
[HttpGet("{id}")]
@ -229,24 +233,22 @@ public class OrganizationsController : Controller
}
[HttpPost("{id}/leave")]
public async Task Leave(string id)
public async Task Leave(Guid id)
{
var orgGuidId = new Guid(id);
if (!await _currentContext.OrganizationUser(orgGuidId))
if (!await _currentContext.OrganizationUser(id))
{
throw new NotFoundException();
}
var user = await _userService.GetUserByPrincipalAsync(User);
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(orgGuidId);
var ssoConfig = await _ssoConfigRepository.GetByOrganizationIdAsync(id);
if (ssoConfig?.GetData()?.MemberDecryptionType == MemberDecryptionType.KeyConnector && user.UsesKeyConnector)
{
throw new BadRequestException("Your organization's Single Sign-On settings prevent you from leaving.");
}
await _organizationService.RemoveUserAsync(orgGuidId, user.Id);
await _removeOrganizationUserCommand.RemoveUserAsync(id, user.Id);
}
[HttpDelete("{id}")]

View File

@ -185,7 +185,7 @@ public class PoliciesController : Controller
}
var userId = _userService.GetProperUserId(User);
await _policyService.SaveAsync(policy, _userService, _organizationService, userId);
await _policyService.SaveAsync(policy, _organizationService, userId);
return new PolicyResponseModel(policy);
}
}

View File

@ -0,0 +1,23 @@
using Bit.Core.Models.Api;
using Bit.Core.Models.Data.Organizations;
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
public class VerifiedOrganizationDomainSsoDetailResponseModel : ResponseModel
{
public VerifiedOrganizationDomainSsoDetailResponseModel(VerifiedOrganizationDomainSsoDetail data)
: base("verifiedOrganizationDomainSsoDetails")
{
if (data is null)
{
throw new ArgumentNullException(nameof(data));
}
DomainName = data.DomainName;
OrganizationIdentifier = data.OrganizationIdentifier;
OrganizationName = data.OrganizationName;
}
public string DomainName { get; }
public string OrganizationIdentifier { get; }
public string OrganizationName { get; }
}

View File

@ -0,0 +1,8 @@
using Bit.Api.Models.Response;
namespace Bit.Api.AdminConsole.Models.Response.Organizations;
public class VerifiedOrganizationDomainSsoDetailsResponseModel(
IEnumerable<VerifiedOrganizationDomainSsoDetailResponseModel> data,
string continuationToken = null)
: ListResponseModel<VerifiedOrganizationDomainSsoDetailResponseModel>(data, continuationToken);

View File

@ -2,12 +2,10 @@
using Bit.Api.AdminConsole.Public.Models.Request;
using Bit.Api.AdminConsole.Public.Models.Response;
using Bit.Api.Models.Public.Response;
using Bit.Core;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Context;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Microsoft.AspNetCore.Authorization;
@ -29,8 +27,8 @@ public class MembersController : Controller
private readonly IApplicationCacheService _applicationCacheService;
private readonly IPaymentService _paymentService;
private readonly IOrganizationRepository _organizationRepository;
private readonly IFeatureService _featureService;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
public MembersController(
IOrganizationUserRepository organizationUserRepository,
@ -43,8 +41,8 @@ public class MembersController : Controller
IApplicationCacheService applicationCacheService,
IPaymentService paymentService,
IOrganizationRepository organizationRepository,
IFeatureService featureService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
{
_organizationUserRepository = organizationUserRepository;
_groupRepository = groupRepository;
@ -56,8 +54,8 @@ public class MembersController : Controller
_applicationCacheService = applicationCacheService;
_paymentService = paymentService;
_organizationRepository = organizationRepository;
_featureService = featureService;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
}
/// <summary>
@ -120,16 +118,11 @@ public class MembersController : Controller
var organizationUserUserDetails = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(_currentContext.OrganizationId.Value);
// TODO: Get all CollectionUser associations for the organization and marry them up here for the response.
if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization))
var orgUsersTwoFactorIsEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUserUserDetails);
var memberResponses = organizationUserUserDetails.Select(u =>
{
return await List_vNext(organizationUserUserDetails);
}
var memberResponsesTasks = organizationUserUserDetails.Select(async u =>
{
return new MemberResponseModel(u, await _userService.TwoFactorIsEnabledAsync(u), null);
return new MemberResponseModel(u, orgUsersTwoFactorIsEnabled.FirstOrDefault(tuple => tuple.user == u).twoFactorIsEnabled, null);
});
var memberResponses = await Task.WhenAll(memberResponsesTasks);
var response = new ListResponseModel<MemberResponseModel>(memberResponses);
return new JsonResult(response);
}
@ -243,7 +236,7 @@ public class MembersController : Controller
{
return new NotFoundResult();
}
await _organizationService.RemoveUserAsync(_currentContext.OrganizationId.Value, id, null);
await _removeOrganizationUserCommand.RemoveUserAsync(_currentContext.OrganizationId.Value, id, null);
return new OkResult();
}
@ -268,15 +261,4 @@ public class MembersController : Controller
await _organizationService.ResendInviteAsync(_currentContext.OrganizationId.Value, null, id);
return new OkResult();
}
private async Task<JsonResult> List_vNext(ICollection<OrganizationUserUserDetails> organizationUserUserDetails)
{
var orgUsersTwoFactorIsEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(organizationUserUserDetails);
var memberResponses = organizationUserUserDetails.Select(u =>
{
return new MemberResponseModel(u, orgUsersTwoFactorIsEnabled.FirstOrDefault(tuple => tuple.user == u).twoFactorIsEnabled, null);
});
var response = new ListResponseModel<MemberResponseModel>(memberResponses);
return new JsonResult(response);
}
}

View File

@ -18,20 +18,17 @@ public class PoliciesController : Controller
{
private readonly IPolicyRepository _policyRepository;
private readonly IPolicyService _policyService;
private readonly IUserService _userService;
private readonly IOrganizationService _organizationService;
private readonly ICurrentContext _currentContext;
public PoliciesController(
IPolicyRepository policyRepository,
IPolicyService policyService,
IUserService userService,
IOrganizationService organizationService,
ICurrentContext currentContext)
{
_policyRepository = policyRepository;
_policyService = policyService;
_userService = userService;
_organizationService = organizationService;
_currentContext = currentContext;
}
@ -99,7 +96,7 @@ public class PoliciesController : Controller
{
policy = model.ToPolicy(policy);
}
await _policyService.SaveAsync(policy, _userService, _organizationService, null);
await _policyService.SaveAsync(policy, _organizationService, null);
var response = new PolicyResponseModel(policy);
return new JsonResult(response);
}

View File

@ -35,7 +35,7 @@
<PackageReference Include="AspNetCore.HealthChecks.SqlServer" Version="8.0.2" />
<PackageReference Include="AspNetCore.HealthChecks.Uris" Version="8.0.1" />
<PackageReference Include="Azure.Messaging.EventGrid" Version="4.25.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.8.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.8.1" />
</ItemGroup>
</Project>

View File

@ -3,7 +3,6 @@ using Bit.Api.Auth.Models.Request.Accounts;
using Bit.Api.Auth.Models.Response.TwoFactor;
using Bit.Api.Models.Request;
using Bit.Api.Models.Response;
using Bit.Core;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.LoginFeatures.PasswordlessLogin.Interfaces;
using Bit.Core.Auth.Models.Business.Tokenables;
@ -37,7 +36,6 @@ public class TwoFactorController : Controller
private readonly IFeatureService _featureService;
private readonly IDataProtectorTokenFactory<TwoFactorAuthenticatorUserVerificationTokenable> _twoFactorAuthenticatorDataProtector;
private readonly IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> _ssoEmailTwoFactorSessionDataProtector;
private readonly bool _TwoFactorAuthenticatorTokenFeatureFlagEnabled;
public TwoFactorController(
IUserService userService,
@ -61,7 +59,6 @@ public class TwoFactorController : Controller
_featureService = featureService;
_twoFactorAuthenticatorDataProtector = twoFactorAuthenticatorDataProtector;
_ssoEmailTwoFactorSessionDataProtector = ssoEmailTwoFactorSessionDataProtector;
_TwoFactorAuthenticatorTokenFeatureFlagEnabled = _featureService.IsEnabled(FeatureFlagKeys.AuthenticatorTwoFactorToken);
}
[HttpGet("")]
@ -102,13 +99,10 @@ public class TwoFactorController : Controller
public async Task<TwoFactorAuthenticatorResponseModel> GetAuthenticator(
[FromBody] SecretVerificationRequestModel model)
{
var user = _TwoFactorAuthenticatorTokenFeatureFlagEnabled ? await CheckAsync(model, false) : await CheckAsync(model, false, true);
var user = await CheckAsync(model, false);
var response = new TwoFactorAuthenticatorResponseModel(user);
if (_TwoFactorAuthenticatorTokenFeatureFlagEnabled)
{
var tokenable = new TwoFactorAuthenticatorUserVerificationTokenable(user, response.Key);
response.UserVerificationToken = _twoFactorAuthenticatorDataProtector.Protect(tokenable);
}
var tokenable = new TwoFactorAuthenticatorUserVerificationTokenable(user, response.Key);
response.UserVerificationToken = _twoFactorAuthenticatorDataProtector.Protect(tokenable);
return response;
}
@ -117,20 +111,11 @@ public class TwoFactorController : Controller
public async Task<TwoFactorAuthenticatorResponseModel> PutAuthenticator(
[FromBody] UpdateTwoFactorAuthenticatorRequestModel model)
{
User user;
if (_TwoFactorAuthenticatorTokenFeatureFlagEnabled)
var user = model.ToUser(await _userService.GetUserByPrincipalAsync(User));
_twoFactorAuthenticatorDataProtector.TryUnprotect(model.UserVerificationToken, out var decryptedToken);
if (!decryptedToken.TokenIsValid(user, model.Key))
{
user = model.ToUser(await _userService.GetUserByPrincipalAsync(User));
_twoFactorAuthenticatorDataProtector.TryUnprotect(model.UserVerificationToken, out var decryptedToken);
if (!decryptedToken.TokenIsValid(user, model.Key))
{
throw new BadRequestException("UserVerificationToken", "User verification failed.");
}
}
else
{
user = await CheckAsync(model, false);
model.ToUser(user); // populates user obj with proper metadata for VerifyTwoFactorTokenAsync
throw new BadRequestException("UserVerificationToken", "User verification failed.");
}
if (!await _userManager.VerifyTwoFactorTokenAsync(user,
@ -145,7 +130,6 @@ public class TwoFactorController : Controller
return response;
}
[RequireFeature(FeatureFlagKeys.AuthenticatorTwoFactorToken)]
[HttpDelete("authenticator")]
public async Task<TwoFactorProviderResponseModel> DisableAuthenticator(
[FromBody] TwoFactorAuthenticatorDisableRequestModel model)
@ -157,7 +141,7 @@ public class TwoFactorController : Controller
throw new BadRequestException("UserVerificationToken", "User verification failed.");
}
await _userService.DisableTwoFactorProviderAsync(user, model.Type.Value, _organizationService);
await _userService.DisableTwoFactorProviderAsync(user, model.Type.Value);
return new TwoFactorProviderResponseModel(model.Type.Value, user);
}
@ -412,7 +396,7 @@ public class TwoFactorController : Controller
public async Task<TwoFactorProviderResponseModel> PutDisable([FromBody] TwoFactorProviderRequestModel model)
{
var user = await CheckAsync(model, false);
await _userService.DisableTwoFactorProviderAsync(user, model.Type.Value, _organizationService);
await _userService.DisableTwoFactorProviderAsync(user, model.Type.Value);
var response = new TwoFactorProviderResponseModel(model.Type.Value, user);
return response;
}
@ -453,8 +437,7 @@ public class TwoFactorController : Controller
[AllowAnonymous]
public async Task PostRecover([FromBody] TwoFactorRecoveryRequestModel model)
{
if (!await _userService.RecoverTwoFactorAsync(model.Email, model.MasterPasswordHash, model.RecoveryCode,
_organizationService))
if (!await _userService.RecoverTwoFactorAsync(model.Email, model.MasterPasswordHash, model.RecoveryCode))
{
await Task.Delay(2000);
throw new BadRequestException(string.Empty, "Invalid information. Try again.");

View File

@ -1,4 +1,5 @@
using Bit.Api.Billing.Models.Responses;
#nullable enable
using Bit.Api.Billing.Models.Responses;
using Bit.Core.Billing.Services;
using Bit.Core.Services;
using Bit.Core.Utilities;
@ -43,7 +44,7 @@ public class AccountsBillingController(
}
[HttpGet("invoices")]
public async Task<IResult> GetInvoicesAsync([FromQuery] string startAfter = null)
public async Task<IResult> GetInvoicesAsync([FromQuery] string? status = null, [FromQuery] string? startAfter = null)
{
var user = await userService.GetUserByPrincipalAsync(User);
if (user == null)
@ -54,6 +55,7 @@ public class AccountsBillingController(
var invoices = await paymentHistoryService.GetInvoiceHistoryAsync(
user,
5,
status,
startAfter);
return TypedResults.Ok(invoices);

View File

@ -1,4 +1,5 @@
using Bit.Api.Billing.Models.Requests;
#nullable enable
using Bit.Api.Billing.Models.Requests;
using Bit.Api.Billing.Models.Responses;
using Bit.Core;
using Bit.Core.Billing.Services;
@ -63,7 +64,7 @@ public class OrganizationBillingController(
}
[HttpGet("invoices")]
public async Task<IResult> GetInvoicesAsync([FromRoute] Guid organizationId, [FromQuery] string startAfter = null)
public async Task<IResult> GetInvoicesAsync([FromRoute] Guid organizationId, [FromQuery] string? status = null, [FromQuery] string? startAfter = null)
{
if (!await currentContext.ViewBillingHistory(organizationId))
{
@ -80,6 +81,7 @@ public class OrganizationBillingController(
var invoices = await paymentHistoryService.GetInvoiceHistoryAsync(
organization,
5,
status,
startAfter);
return TypedResults.Ok(invoices);

View File

@ -75,6 +75,7 @@ public class Startup
// Services
services.AddBaseServices(globalSettings);
services.AddDefaultServices(globalSettings);
services.AddDistributedCache(globalSettings);
services.AddBillingOperations();
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();

View File

@ -18,7 +18,8 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IUserRepository _userRepository;
private readonly ICurrentContext _currentContext;
private readonly IOrganizationService _organizationService;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
public DeleteManagedOrganizationUserAccountCommand(
IUserService userService,
IEventService eventService,
@ -26,7 +27,7 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
IOrganizationUserRepository organizationUserRepository,
IUserRepository userRepository,
ICurrentContext currentContext,
IOrganizationService organizationService)
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery)
{
_userService = userService;
_eventService = eventService;
@ -34,7 +35,7 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
_organizationUserRepository = organizationUserRepository;
_userRepository = userRepository;
_currentContext = currentContext;
_organizationService = organizationService;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
}
public async Task DeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)
@ -46,7 +47,7 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
}
var managementStatus = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(organizationId, new[] { organizationUserId });
var hasOtherConfirmedOwners = await _organizationService.HasConfirmedOwnersExceptAsync(organizationId, new[] { organizationUserId }, includeProvider: true);
var hasOtherConfirmedOwners = await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, new[] { organizationUserId }, includeProvider: true);
await ValidateDeleteUserAsync(organizationId, organizationUser, deletingUserId, managementStatus, hasOtherConfirmedOwners);
@ -67,7 +68,7 @@ public class DeleteManagedOrganizationUserAccountCommand : IDeleteManagedOrganiz
var users = await _userRepository.GetManyAsync(userIds);
var managementStatus = await _getOrganizationUsersManagementStatusQuery.GetUsersOrganizationManagementStatusAsync(organizationId, orgUserIds);
var hasOtherConfirmedOwners = await _organizationService.HasConfirmedOwnersExceptAsync(organizationId, orgUserIds, includeProvider: true);
var hasOtherConfirmedOwners = await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, orgUserIds, includeProvider: true);
var results = new List<(Guid OrganizationUserId, string? ErrorMessage)>();
foreach (var orgUserId in orgUserIds)

View File

@ -0,0 +1,41 @@
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
public class HasConfirmedOwnersExceptQuery : IHasConfirmedOwnersExceptQuery
{
private readonly IProviderUserRepository _providerUserRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
public HasConfirmedOwnersExceptQuery(
IProviderUserRepository providerUserRepository,
IOrganizationUserRepository organizationUserRepository)
{
_providerUserRepository = providerUserRepository;
_organizationUserRepository = organizationUserRepository;
}
public async Task<bool> HasConfirmedOwnersExceptAsync(Guid organizationId, IEnumerable<Guid> organizationUsersId, bool includeProvider = true)
{
var confirmedOwners = await GetConfirmedOwnersAsync(organizationId);
var confirmedOwnersIds = confirmedOwners.Select(u => u.Id);
bool hasOtherOwner = confirmedOwnersIds.Except(organizationUsersId).Any();
if (!hasOtherOwner && includeProvider)
{
return (await _providerUserRepository.GetManyByOrganizationAsync(organizationId, ProviderUserStatusType.Confirmed)).Any();
}
return hasOtherOwner;
}
private async Task<IEnumerable<OrganizationUser>> GetConfirmedOwnersAsync(Guid organizationId)
{
var owners = await _organizationUserRepository.GetManyByOrganizationAsync(organizationId,
OrganizationUserType.Owner);
return owners.Where(o => o.Status == OrganizationUserStatusType.Confirmed);
}
}

View File

@ -0,0 +1,12 @@
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
public interface IHasConfirmedOwnersExceptQuery
{
/// <summary>
/// Checks if an organization has any confirmed owners except for the ones in the <paramref name="organizationUsersId"/> list.
/// </summary>
/// <param name="organizationId">The organization ID.</param>
/// <param name="organizationUsersId">The organization user IDs to exclude.</param>
/// <param name="includeProvider">Whether to include the provider users in the count.</param>
Task<bool> HasConfirmedOwnersExceptAsync(Guid organizationId, IEnumerable<Guid> organizationUsersId, bool includeProvider = true);
}

View File

@ -1,4 +1,5 @@
using Bit.Core.Enums;
using Bit.Core.Entities;
using Bit.Core.Enums;
namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
@ -7,4 +8,7 @@ public interface IRemoveOrganizationUserCommand
Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId);
Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser);
Task RemoveUserAsync(Guid organizationId, Guid userId);
Task<List<Tuple<OrganizationUser, string>>> RemoveUsersAsync(Guid organizationId,
IEnumerable<Guid> organizationUserIds, Guid? deletingUserId);
}

View File

@ -1,4 +1,6 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
@ -8,38 +10,171 @@ namespace Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
public class RemoveOrganizationUserCommand : IRemoveOrganizationUserCommand
{
private readonly IDeviceRepository _deviceRepository;
private readonly IOrganizationUserRepository _organizationUserRepository;
private readonly IOrganizationService _organizationService;
private readonly IEventService _eventService;
private readonly IPushNotificationService _pushNotificationService;
private readonly IPushRegistrationService _pushRegistrationService;
private readonly ICurrentContext _currentContext;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
public RemoveOrganizationUserCommand(
IDeviceRepository deviceRepository,
IOrganizationUserRepository organizationUserRepository,
IOrganizationService organizationService
)
IEventService eventService,
IPushNotificationService pushNotificationService,
IPushRegistrationService pushRegistrationService,
ICurrentContext currentContext,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery)
{
_deviceRepository = deviceRepository;
_organizationUserRepository = organizationUserRepository;
_organizationService = organizationService;
_eventService = eventService;
_pushNotificationService = pushNotificationService;
_pushRegistrationService = pushRegistrationService;
_currentContext = currentContext;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
}
public async Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)
{
await ValidateDeleteUserAsync(organizationId, organizationUserId);
var organizationUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
ValidateDeleteUser(organizationId, organizationUser);
await _organizationService.RemoveUserAsync(organizationId, organizationUserId, deletingUserId);
await RepositoryDeleteUserAsync(organizationUser, deletingUserId);
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed);
}
public async Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser)
{
await ValidateDeleteUserAsync(organizationId, organizationUserId);
var organizationUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
ValidateDeleteUser(organizationId, organizationUser);
await _organizationService.RemoveUserAsync(organizationId, organizationUserId, eventSystemUser);
await RepositoryDeleteUserAsync(organizationUser, null);
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser);
}
private async Task ValidateDeleteUserAsync(Guid organizationId, Guid organizationUserId)
public async Task RemoveUserAsync(Guid organizationId, Guid userId)
{
var organizationUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId);
ValidateDeleteUser(organizationId, organizationUser);
await RepositoryDeleteUserAsync(organizationUser, null);
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed);
}
public async Task<List<Tuple<OrganizationUser, string>>> RemoveUsersAsync(Guid organizationId,
IEnumerable<Guid> organizationUsersId,
Guid? deletingUserId)
{
var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId);
var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId)
.ToList();
if (!filteredUsers.Any())
{
throw new BadRequestException("Users invalid.");
}
if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId))
{
throw new BadRequestException("Organization must have at least one confirmed owner.");
}
var deletingUserIsOwner = false;
if (deletingUserId.HasValue)
{
deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId);
}
var result = new List<Tuple<OrganizationUser, string>>();
var deletedUserIds = new List<Guid>();
foreach (var orgUser in filteredUsers)
{
try
{
if (deletingUserId.HasValue && orgUser.UserId == deletingUserId)
{
throw new BadRequestException("You cannot remove yourself.");
}
if (orgUser.Type == OrganizationUserType.Owner && deletingUserId.HasValue && !deletingUserIsOwner)
{
throw new BadRequestException("Only owners can delete other owners.");
}
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed);
if (orgUser.UserId.HasValue)
{
await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value);
}
result.Add(Tuple.Create(orgUser, ""));
deletedUserIds.Add(orgUser.Id);
}
catch (BadRequestException e)
{
result.Add(Tuple.Create(orgUser, e.Message));
}
await _organizationUserRepository.DeleteManyAsync(deletedUserIds);
}
return result;
}
private void ValidateDeleteUser(Guid organizationId, OrganizationUser orgUser)
{
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
if (orgUser == null || orgUser.OrganizationId != organizationId)
{
throw new NotFoundException("User not found.");
}
}
private async Task RepositoryDeleteUserAsync(OrganizationUser orgUser, Guid? deletingUserId)
{
if (deletingUserId.HasValue && orgUser.UserId == deletingUserId.Value)
{
throw new BadRequestException("You cannot remove yourself.");
}
if (orgUser.Type == OrganizationUserType.Owner)
{
if (deletingUserId.HasValue && !await _currentContext.OrganizationOwner(orgUser.OrganizationId))
{
throw new BadRequestException("Only owners can delete other owners.");
}
if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(orgUser.OrganizationId, new[] { orgUser.Id }, includeProvider: true))
{
throw new BadRequestException("Organization must have at least one confirmed owner.");
}
}
await _organizationUserRepository.DeleteAsync(orgUser);
if (orgUser.UserId.HasValue)
{
await DeleteAndPushUserRegistrationAsync(orgUser.OrganizationId, orgUser.UserId.Value);
}
}
private async Task<IEnumerable<KeyValuePair<string, DeviceType>>> GetUserDeviceIdsAsync(Guid userId)
{
var devices = await _deviceRepository.GetManyByUserIdAsync(userId);
return devices
.Where(d => !string.IsNullOrWhiteSpace(d.PushToken))
.Select(d => new KeyValuePair<string, DeviceType>(d.Id.ToString(), d.Type));
}
private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)
{
var devices = await GetUserDeviceIdsAsync(userId);
await _pushRegistrationService.DeleteUserRegistrationOrganizationAsync(devices,
organizationId.ToString());
await _pushNotificationService.PushSyncOrgKeysAsync(userId);
}
}

View File

@ -22,6 +22,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
private readonly IUpdateSecretsManagerSubscriptionCommand _updateSecretsManagerSubscriptionCommand;
private readonly ICollectionRepository _collectionRepository;
private readonly IGroupRepository _groupRepository;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
public UpdateOrganizationUserCommand(
IEventService eventService,
@ -31,7 +32,8 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
ICountNewSmSeatsRequiredQuery countNewSmSeatsRequiredQuery,
IUpdateSecretsManagerSubscriptionCommand updateSecretsManagerSubscriptionCommand,
ICollectionRepository collectionRepository,
IGroupRepository groupRepository)
IGroupRepository groupRepository,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery)
{
_eventService = eventService;
_organizationService = organizationService;
@ -41,6 +43,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
_updateSecretsManagerSubscriptionCommand = updateSecretsManagerSubscriptionCommand;
_collectionRepository = collectionRepository;
_groupRepository = groupRepository;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
}
/// <summary>
@ -87,7 +90,7 @@ public class UpdateOrganizationUserCommand : IUpdateOrganizationUserCommand
await _organizationService.ValidateOrganizationCustomPermissionsEnabledAsync(user.OrganizationId, user.Type);
if (user.Type != OrganizationUserType.Owner &&
!await _organizationService.HasConfirmedOwnersExceptAsync(user.OrganizationId, new[] { user.Id }))
!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(user.OrganizationId, new[] { user.Id }))
{
throw new BadRequestException("Organization must have at least one confirmed owner.");
}

View File

@ -49,32 +49,21 @@ public interface IOrganizationService
IEnumerable<(OrganizationUserInvite invite, string externalId)> invites);
Task<IEnumerable<Tuple<OrganizationUser, string>>> ResendInvitesAsync(Guid organizationId, Guid? invitingUserId, IEnumerable<Guid> organizationUsersId);
Task ResendInviteAsync(Guid organizationId, Guid? invitingUserId, Guid organizationUserId, bool initOrganization = false);
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
Guid confirmingUserId, IUserService userService);
Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key, Guid confirmingUserId);
Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys,
Guid confirmingUserId, IUserService userService);
Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync_vNext(Guid organizationId, Dictionary<Guid, string> keys,
Guid confirmingUserId);
[Obsolete("IRemoveOrganizationUserCommand should be used instead. To be removed by EC-607.")]
Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId);
[Obsolete("IRemoveOrganizationUserCommand should be used instead. To be removed by EC-607.")]
Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, EventSystemUser systemUser);
Task RemoveUserAsync(Guid organizationId, Guid userId);
Task<List<Tuple<OrganizationUser, string>>> RemoveUsersAsync(Guid organizationId,
IEnumerable<Guid> organizationUserIds, Guid? deletingUserId);
Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId);
Task ImportAsync(Guid organizationId, IEnumerable<ImportedGroup> groups,
IEnumerable<ImportedOrganizationUser> newUsers, IEnumerable<string> removeUserExternalIds,
bool overwriteExisting, EventSystemUser eventSystemUser);
Task DeleteSsoUserAsync(Guid userId, Guid? organizationId);
Task<Organization> UpdateOrganizationKeysAsync(Guid orgId, string publicKey, string privateKey);
Task<bool> HasConfirmedOwnersExceptAsync(Guid organizationId, IEnumerable<Guid> organizationUsersId, bool includeProvider = true);
Task RevokeUserAsync(OrganizationUser organizationUser, Guid? revokingUserId);
Task RevokeUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser);
Task<List<Tuple<OrganizationUser, string>>> RevokeUsersAsync(Guid organizationId,
IEnumerable<Guid> organizationUserIds, Guid? revokingUserId);
Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId, IUserService userService);
Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser, IUserService userService);
Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId);
Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser);
Task<List<Tuple<OrganizationUser, string>>> RestoreUsersAsync(Guid organizationId,
IEnumerable<Guid> organizationUserIds, Guid? restoringUserId, IUserService userService);
Task CreatePendingOrganization(Organization organization, string ownerEmail, ClaimsPrincipal user, IUserService userService, bool salesAssistedTrialStarted);

View File

@ -10,8 +10,7 @@ namespace Bit.Core.AdminConsole.Services;
public interface IPolicyService
{
Task SaveAsync(Policy policy, IUserService userService, IOrganizationService organizationService,
Guid? savingUserId);
Task SaveAsync(Policy policy, IOrganizationService organizationService, Guid? savingUserId);
/// <summary>
/// Get the combined master password policy options for the specified user.

View File

@ -73,6 +73,7 @@ public class OrganizationService : IOrganizationService
private readonly IFeatureService _featureService;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IOrganizationBillingService _organizationBillingService;
private readonly IHasConfirmedOwnersExceptQuery _hasConfirmedOwnersExceptQuery;
public OrganizationService(
IOrganizationRepository organizationRepository,
@ -107,7 +108,8 @@ public class OrganizationService : IOrganizationService
IProviderRepository providerRepository,
IFeatureService featureService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IOrganizationBillingService organizationBillingService)
IOrganizationBillingService organizationBillingService,
IHasConfirmedOwnersExceptQuery hasConfirmedOwnersExceptQuery)
{
_organizationRepository = organizationRepository;
_organizationUserRepository = organizationUserRepository;
@ -142,6 +144,7 @@ public class OrganizationService : IOrganizationService
_featureService = featureService;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_organizationBillingService = organizationBillingService;
_hasConfirmedOwnersExceptQuery = hasConfirmedOwnersExceptQuery;
}
public async Task ReplacePaymentMethodAsync(Guid organizationId, string paymentToken,
@ -1074,7 +1077,7 @@ public class OrganizationService : IOrganizationService
}
var invitedAreAllOwners = invites.All(i => i.invite.Type == OrganizationUserType.Owner);
if (!invitedAreAllOwners && !await HasConfirmedOwnersExceptAsync(organizationId, new Guid[] { }, includeProvider: true))
if (!invitedAreAllOwners && !await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, new Guid[] { }, includeProvider: true))
{
throw new BadRequestException("Organization must have at least one confirmed owner.");
}
@ -1323,13 +1326,12 @@ public class OrganizationService : IOrganizationService
}
public async Task<OrganizationUser> ConfirmUserAsync(Guid organizationId, Guid organizationUserId, string key,
Guid confirmingUserId, IUserService userService)
Guid confirmingUserId)
{
var result = _featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization)
? await ConfirmUsersAsync_vNext(organizationId, new Dictionary<Guid, string>() { { organizationUserId, key } },
confirmingUserId)
: await ConfirmUsersAsync(organizationId, new Dictionary<Guid, string>() { { organizationUserId, key } },
confirmingUserId, userService);
var result = await ConfirmUsersAsync(
organizationId,
new Dictionary<Guid, string>() { { organizationUserId, key } },
confirmingUserId);
if (!result.Any())
{
@ -1345,75 +1347,6 @@ public class OrganizationService : IOrganizationService
}
public async Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync(Guid organizationId, Dictionary<Guid, string> keys,
Guid confirmingUserId, IUserService userService)
{
var organizationUsers = await _organizationUserRepository.GetManyAsync(keys.Keys);
var validOrganizationUsers = organizationUsers
.Where(u => u.Status == OrganizationUserStatusType.Accepted && u.OrganizationId == organizationId && u.UserId != null)
.ToList();
if (!validOrganizationUsers.Any())
{
return new List<Tuple<OrganizationUser, string>>();
}
var validOrganizationUserIds = validOrganizationUsers.Select(u => u.UserId.Value).ToList();
var organization = await GetOrgById(organizationId);
var usersOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(validOrganizationUserIds);
var users = await _userRepository.GetManyAsync(validOrganizationUserIds);
var keyedFilteredUsers = validOrganizationUsers.ToDictionary(u => u.UserId.Value, u => u);
var keyedOrganizationUsers = usersOrgs.GroupBy(u => u.UserId.Value)
.ToDictionary(u => u.Key, u => u.ToList());
var succeededUsers = new List<OrganizationUser>();
var result = new List<Tuple<OrganizationUser, string>>();
foreach (var user in users)
{
if (!keyedFilteredUsers.ContainsKey(user.Id))
{
continue;
}
var orgUser = keyedFilteredUsers[user.Id];
var orgUsers = keyedOrganizationUsers.GetValueOrDefault(user.Id, new List<OrganizationUser>());
try
{
if (organization.PlanType == PlanType.Free && (orgUser.Type == OrganizationUserType.Admin
|| orgUser.Type == OrganizationUserType.Owner))
{
// Since free organizations only supports a few users there is not much point in avoiding N+1 queries for this.
var adminCount = await _organizationUserRepository.GetCountByFreeOrganizationAdminUserAsync(user.Id);
if (adminCount > 0)
{
throw new BadRequestException("User can only be an admin of one free organization.");
}
}
await CheckPolicies(organizationId, user, orgUsers, userService);
orgUser.Status = OrganizationUserStatusType.Confirmed;
orgUser.Key = keys[orgUser.Id];
orgUser.Email = null;
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Confirmed);
await _mailService.SendOrganizationConfirmedEmailAsync(organization.DisplayName(), user.Email, orgUser.AccessSecretsManager);
await DeleteAndPushUserRegistrationAsync(organizationId, user.Id);
succeededUsers.Add(orgUser);
result.Add(Tuple.Create(orgUser, ""));
}
catch (BadRequestException e)
{
result.Add(Tuple.Create(orgUser, e.Message));
}
}
await _organizationUserRepository.ReplaceManyAsync(succeededUsers);
return result;
}
public async Task<List<Tuple<OrganizationUser, string>>> ConfirmUsersAsync_vNext(Guid organizationId, Dictionary<Guid, string> keys,
Guid confirmingUserId)
{
var selectedOrganizationUsers = await _organizationUserRepository.GetManyAsync(keys.Keys);
@ -1430,7 +1363,7 @@ public class OrganizationService : IOrganizationService
var organization = await GetOrgById(organizationId);
var allUsersOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(validSelectedUserIds);
var users = await _userRepository.GetManyWithCalculatedPremiumAsync(validSelectedUserIds);
var users = await _userRepository.GetManyAsync(validSelectedUserIds);
var usersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(validSelectedUserIds);
var keyedFilteredUsers = validSelectedOrganizationUsers.ToDictionary(u => u.UserId.Value, u => u);
@ -1462,7 +1395,7 @@ public class OrganizationService : IOrganizationService
}
var twoFactorEnabled = usersTwoFactorEnabled.FirstOrDefault(tuple => tuple.userId == user.Id).twoFactorIsEnabled;
await CheckPolicies_vNext(organizationId, user, orgUsers, twoFactorEnabled);
await CheckPoliciesAsync(organizationId, user, orgUsers, twoFactorEnabled);
orgUser.Status = OrganizationUserStatusType.Confirmed;
orgUser.Key = keys[orgUser.Id];
orgUser.Email = null;
@ -1567,33 +1500,7 @@ public class OrganizationService : IOrganizationService
}
}
private async Task CheckPolicies(Guid organizationId, User user,
ICollection<OrganizationUser> userOrgs, IUserService userService)
{
// Enforce Two Factor Authentication Policy for this organization
var orgRequiresTwoFactor = (await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication)).Any(p => p.OrganizationId == organizationId);
if (orgRequiresTwoFactor && !await userService.TwoFactorIsEnabledAsync(user))
{
throw new BadRequestException("User does not have two-step login enabled.");
}
var hasOtherOrgs = userOrgs.Any(ou => ou.OrganizationId != organizationId);
var singleOrgPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.SingleOrg);
var otherSingleOrgPolicies =
singleOrgPolicies.Where(p => p.OrganizationId != organizationId);
// Enforce Single Organization Policy for this organization
if (hasOtherOrgs && singleOrgPolicies.Any(p => p.OrganizationId == organizationId))
{
throw new BadRequestException("Cannot confirm this member to the organization until they leave or remove all other organizations.");
}
// Enforce Single Organization Policy of other organizations user is a member of
if (otherSingleOrgPolicies.Any())
{
throw new BadRequestException("Cannot confirm this member to the organization because they are in another organization which forbids it.");
}
}
private async Task CheckPolicies_vNext(Guid organizationId, UserWithCalculatedPremium user,
private async Task CheckPoliciesAsync(Guid organizationId, User user,
ICollection<OrganizationUser> userOrgs, bool twoFactorEnabled)
{
// Enforce Two Factor Authentication Policy for this organization
@ -1620,149 +1527,6 @@ public class OrganizationService : IOrganizationService
}
}
[Obsolete("IRemoveOrganizationUserCommand should be used instead. To be removed by EC-607.")]
public async Task RemoveUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)
{
var orgUser = await RepositoryDeleteUserAsync(organizationId, organizationUserId, deletingUserId);
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed);
}
[Obsolete("IRemoveOrganizationUserCommand should be used instead. To be removed by EC-607.")]
public async Task RemoveUserAsync(Guid organizationId, Guid organizationUserId,
EventSystemUser systemUser)
{
var orgUser = await RepositoryDeleteUserAsync(organizationId, organizationUserId, null);
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed, systemUser);
}
private async Task<OrganizationUser> RepositoryDeleteUserAsync(Guid organizationId, Guid organizationUserId, Guid? deletingUserId)
{
var orgUser = await _organizationUserRepository.GetByIdAsync(organizationUserId);
if (orgUser == null || orgUser.OrganizationId != organizationId)
{
throw new BadRequestException("User not valid.");
}
if (deletingUserId.HasValue && orgUser.UserId == deletingUserId.Value)
{
throw new BadRequestException("You cannot remove yourself.");
}
if (orgUser.Type == OrganizationUserType.Owner && deletingUserId.HasValue &&
!await _currentContext.OrganizationOwner(organizationId))
{
throw new BadRequestException("Only owners can delete other owners.");
}
if (!await HasConfirmedOwnersExceptAsync(organizationId, new[] { organizationUserId }, includeProvider: true))
{
throw new BadRequestException("Organization must have at least one confirmed owner.");
}
await _organizationUserRepository.DeleteAsync(orgUser);
if (orgUser.UserId.HasValue)
{
await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value);
}
return orgUser;
}
public async Task RemoveUserAsync(Guid organizationId, Guid userId)
{
var orgUser = await _organizationUserRepository.GetByOrganizationAsync(organizationId, userId);
if (orgUser == null)
{
throw new NotFoundException();
}
if (!await HasConfirmedOwnersExceptAsync(organizationId, new[] { orgUser.Id }))
{
throw new BadRequestException("Organization must have at least one confirmed owner.");
}
await _organizationUserRepository.DeleteAsync(orgUser);
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed);
if (orgUser.UserId.HasValue)
{
await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value);
}
}
public async Task<List<Tuple<OrganizationUser, string>>> RemoveUsersAsync(Guid organizationId,
IEnumerable<Guid> organizationUsersId,
Guid? deletingUserId)
{
var orgUsers = await _organizationUserRepository.GetManyAsync(organizationUsersId);
var filteredUsers = orgUsers.Where(u => u.OrganizationId == organizationId)
.ToList();
if (!filteredUsers.Any())
{
throw new BadRequestException("Users invalid.");
}
if (!await HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId))
{
throw new BadRequestException("Organization must have at least one confirmed owner.");
}
var deletingUserIsOwner = false;
if (deletingUserId.HasValue)
{
deletingUserIsOwner = await _currentContext.OrganizationOwner(organizationId);
}
var result = new List<Tuple<OrganizationUser, string>>();
var deletedUserIds = new List<Guid>();
foreach (var orgUser in filteredUsers)
{
try
{
if (deletingUserId.HasValue && orgUser.UserId == deletingUserId)
{
throw new BadRequestException("You cannot remove yourself.");
}
if (orgUser.Type == OrganizationUserType.Owner && deletingUserId.HasValue && !deletingUserIsOwner)
{
throw new BadRequestException("Only owners can delete other owners.");
}
await _eventService.LogOrganizationUserEventAsync(orgUser, EventType.OrganizationUser_Removed);
if (orgUser.UserId.HasValue)
{
await DeleteAndPushUserRegistrationAsync(organizationId, orgUser.UserId.Value);
}
result.Add(Tuple.Create(orgUser, ""));
deletedUserIds.Add(orgUser.Id);
}
catch (BadRequestException e)
{
result.Add(Tuple.Create(orgUser, e.Message));
}
await _organizationUserRepository.DeleteManyAsync(deletedUserIds);
}
return result;
}
public async Task<bool> HasConfirmedOwnersExceptAsync(Guid organizationId, IEnumerable<Guid> organizationUsersId, bool includeProvider = true)
{
var confirmedOwners = await GetConfirmedOwnersAsync(organizationId);
var confirmedOwnersIds = confirmedOwners.Select(u => u.Id);
bool hasOtherOwner = confirmedOwnersIds.Except(organizationUsersId).Any();
if (!hasOtherOwner && includeProvider)
{
return (await _providerUserRepository.GetManyByOrganizationAsync(organizationId, ProviderUserStatusType.Confirmed)).Any();
}
return hasOtherOwner;
}
public async Task UpdateUserResetPasswordEnrollmentAsync(Guid organizationId, Guid userId, string resetPasswordKey, Guid? callingUserId)
{
// Org User must be the same as the calling user and the organization ID associated with the user must match passed org ID
@ -2059,13 +1823,6 @@ public class OrganizationService : IOrganizationService
await _groupRepository.UpdateUsersAsync(group.Id, users);
}
private async Task<IEnumerable<OrganizationUser>> GetConfirmedOwnersAsync(Guid organizationId)
{
var owners = await _organizationUserRepository.GetManyByOrganizationAsync(organizationId,
OrganizationUserType.Owner);
return owners.Where(o => o.Status == OrganizationUserStatusType.Confirmed);
}
private async Task DeleteAndPushUserRegistrationAsync(Guid organizationId, Guid userId)
{
var devices = await GetUserDeviceIdsAsync(userId);
@ -2370,7 +2127,7 @@ public class OrganizationService : IOrganizationService
throw new BadRequestException("Already revoked.");
}
if (!await HasConfirmedOwnersExceptAsync(organizationUser.OrganizationId, new[] { organizationUser.Id }, includeProvider: true))
if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationUser.OrganizationId, new[] { organizationUser.Id }, includeProvider: true))
{
throw new BadRequestException("Organization must have at least one confirmed owner.");
}
@ -2391,7 +2148,7 @@ public class OrganizationService : IOrganizationService
throw new BadRequestException("Users invalid.");
}
if (!await HasConfirmedOwnersExceptAsync(organizationId, organizationUserIds))
if (!await _hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(organizationId, organizationUserIds))
{
throw new BadRequestException("Organization must have at least one confirmed owner.");
}
@ -2438,8 +2195,7 @@ public class OrganizationService : IOrganizationService
return result;
}
public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId,
IUserService userService)
public async Task RestoreUserAsync(OrganizationUser organizationUser, Guid? restoringUserId)
{
if (restoringUserId.HasValue && organizationUser.UserId == restoringUserId.Value)
{
@ -2452,18 +2208,17 @@ public class OrganizationService : IOrganizationService
throw new BadRequestException("Only owners can restore other owners.");
}
await RepositoryRestoreUserAsync(organizationUser, userService);
await RepositoryRestoreUserAsync(organizationUser);
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored);
}
public async Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser,
IUserService userService)
public async Task RestoreUserAsync(OrganizationUser organizationUser, EventSystemUser systemUser)
{
await RepositoryRestoreUserAsync(organizationUser, userService);
await RepositoryRestoreUserAsync(organizationUser);
await _eventService.LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Restored, systemUser);
}
private async Task RepositoryRestoreUserAsync(OrganizationUser organizationUser, IUserService userService)
private async Task RepositoryRestoreUserAsync(OrganizationUser organizationUser)
{
if (organizationUser.Status != OrganizationUserStatusType.Revoked)
{
@ -2478,21 +2233,14 @@ public class OrganizationService : IOrganizationService
await AutoAddSeatsAsync(organization, 1);
}
if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization))
var userTwoFactorIsEnabled = false;
// Only check Two Factor Authentication status if the user is linked to a user account
if (organizationUser.UserId.HasValue)
{
var userTwoFactorIsEnabled = false;
// Only check Two Factor Authentication status if the user is linked to a user account
if (organizationUser.UserId.HasValue)
{
userTwoFactorIsEnabled = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(new[] { organizationUser.UserId.Value })).FirstOrDefault().twoFactorIsEnabled;
}
userTwoFactorIsEnabled = (await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(new[] { organizationUser.UserId.Value })).FirstOrDefault().twoFactorIsEnabled;
}
await CheckPoliciesBeforeRestoreAsync_vNext(organizationUser, userTwoFactorIsEnabled);
}
else
{
await CheckPoliciesBeforeRestoreAsync(organizationUser, userService);
}
await CheckPoliciesBeforeRestoreAsync(organizationUser, userTwoFactorIsEnabled);
var status = GetPriorActiveOrganizationUserStatusType(organizationUser);
@ -2526,11 +2274,7 @@ public class OrganizationService : IOrganizationService
// Query Two Factor Authentication status for all users in the organization
// This is an optimization to avoid querying the Two Factor Authentication status for each user individually
IEnumerable<(Guid userId, bool twoFactorIsEnabled)> organizationUsersTwoFactorEnabled = null;
if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization))
{
organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(filteredUsers.Select(ou => ou.UserId.Value));
}
var organizationUsersTwoFactorEnabled = await _twoFactorIsEnabledQuery.TwoFactorIsEnabledAsync(filteredUsers.Select(ou => ou.UserId.Value));
var result = new List<Tuple<OrganizationUser, string>>();
@ -2553,15 +2297,8 @@ public class OrganizationService : IOrganizationService
throw new BadRequestException("Only owners can restore other owners.");
}
if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization))
{
var twoFactorIsEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(ou => ou.userId == organizationUser.UserId.Value).twoFactorIsEnabled;
await CheckPoliciesBeforeRestoreAsync_vNext(organizationUser, twoFactorIsEnabled);
}
else
{
await CheckPoliciesBeforeRestoreAsync(organizationUser, userService);
}
var twoFactorIsEnabled = organizationUsersTwoFactorEnabled.FirstOrDefault(ou => ou.userId == organizationUser.UserId.Value).twoFactorIsEnabled;
await CheckPoliciesBeforeRestoreAsync(organizationUser, twoFactorIsEnabled);
var status = GetPriorActiveOrganizationUserStatusType(organizationUser);
@ -2580,54 +2317,7 @@ public class OrganizationService : IOrganizationService
return result;
}
private async Task CheckPoliciesBeforeRestoreAsync(OrganizationUser orgUser, IUserService userService)
{
// An invited OrganizationUser isn't linked with a user account yet, so these checks are irrelevant
// The user will be subject to the same checks when they try to accept the invite
if (GetPriorActiveOrganizationUserStatusType(orgUser) == OrganizationUserStatusType.Invited)
{
return;
}
var userId = orgUser.UserId.Value;
// Enforce Single Organization Policy of organization user is being restored to
var allOrgUsers = await _organizationUserRepository.GetManyByUserAsync(userId);
var hasOtherOrgs = allOrgUsers.Any(ou => ou.OrganizationId != orgUser.OrganizationId);
var singleOrgPoliciesApplyingToRevokedUsers = await _policyService.GetPoliciesApplicableToUserAsync(userId,
PolicyType.SingleOrg, OrganizationUserStatusType.Revoked);
var singleOrgPolicyApplies = singleOrgPoliciesApplyingToRevokedUsers.Any(p => p.OrganizationId == orgUser.OrganizationId);
if (hasOtherOrgs && singleOrgPolicyApplies)
{
throw new BadRequestException("You cannot restore this user until " +
"they leave or remove all other organizations.");
}
// Enforce Single Organization Policy of other organizations user is a member of
var anySingleOrgPolicies = await _policyService.AnyPoliciesApplicableToUserAsync(userId,
PolicyType.SingleOrg);
if (anySingleOrgPolicies)
{
throw new BadRequestException("You cannot restore this user because they are a member of " +
"another organization which forbids it");
}
// Enforce Two Factor Authentication Policy of organization user is trying to join
var user = await _userRepository.GetByIdAsync(userId);
if (!await userService.TwoFactorIsEnabledAsync(user))
{
var invitedTwoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(userId,
PolicyType.TwoFactorAuthentication, OrganizationUserStatusType.Invited);
if (invitedTwoFactorPolicies.Any(p => p.OrganizationId == orgUser.OrganizationId))
{
throw new BadRequestException("You cannot restore this user until they enable " +
"two-step login on their user account.");
}
}
}
private async Task CheckPoliciesBeforeRestoreAsync_vNext(OrganizationUser orgUser, bool userHasTwoFactorEnabled)
private async Task CheckPoliciesBeforeRestoreAsync(OrganizationUser orgUser, bool userHasTwoFactorEnabled)
{
// An invited OrganizationUser isn't linked with a user account yet, so these checks are irrelevant
// The user will be subject to the same checks when they try to accept the invite

View File

@ -1,6 +1,7 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Repositories;
@ -25,8 +26,8 @@ public class PolicyService : IPolicyService
private readonly ISsoConfigRepository _ssoConfigRepository;
private readonly IMailService _mailService;
private readonly GlobalSettings _globalSettings;
private readonly IFeatureService _featureService;
private readonly ITwoFactorIsEnabledQuery _twoFactorIsEnabledQuery;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
public PolicyService(
IApplicationCacheService applicationCacheService,
@ -37,8 +38,8 @@ public class PolicyService : IPolicyService
ISsoConfigRepository ssoConfigRepository,
IMailService mailService,
GlobalSettings globalSettings,
IFeatureService featureService,
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery)
ITwoFactorIsEnabledQuery twoFactorIsEnabledQuery,
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
{
_applicationCacheService = applicationCacheService;
_eventService = eventService;
@ -48,12 +49,11 @@ public class PolicyService : IPolicyService
_ssoConfigRepository = ssoConfigRepository;
_mailService = mailService;
_globalSettings = globalSettings;
_featureService = featureService;
_twoFactorIsEnabledQuery = twoFactorIsEnabledQuery;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
}
public async Task SaveAsync(Policy policy, IUserService userService, IOrganizationService organizationService,
Guid? savingUserId)
public async Task SaveAsync(Policy policy, IOrganizationService organizationService, Guid? savingUserId)
{
var org = await _organizationRepository.GetByIdAsync(policy.OrganizationId);
if (org == null)
@ -88,14 +88,7 @@ public class PolicyService : IPolicyService
return;
}
if (_featureService.IsEnabled(FeatureFlagKeys.MembersTwoFAQueryOptimization))
{
await EnablePolicy_vNext(policy, org, organizationService, savingUserId);
return;
}
await EnablePolicy(policy, org, userService, organizationService, savingUserId);
return;
await EnablePolicyAsync(policy, org, organizationService, savingUserId);
}
public async Task<MasterPasswordPolicyData> GetMasterPasswordPolicyForUserAsync(User user)
@ -269,62 +262,7 @@ public class PolicyService : IPolicyService
await _eventService.LogPolicyEventAsync(policy, EventType.Policy_Updated);
}
private async Task EnablePolicy(Policy policy, Organization org, IUserService userService, IOrganizationService organizationService, Guid? savingUserId)
{
var currentPolicy = await _policyRepository.GetByIdAsync(policy.Id);
if (!currentPolicy?.Enabled ?? true)
{
var orgUsers = await _organizationUserRepository.GetManyDetailsByOrganizationAsync(policy.OrganizationId);
var removableOrgUsers = orgUsers.Where(ou =>
ou.Status != OrganizationUserStatusType.Invited && ou.Status != OrganizationUserStatusType.Revoked &&
ou.Type != OrganizationUserType.Owner && ou.Type != OrganizationUserType.Admin &&
ou.UserId != savingUserId);
switch (policy.Type)
{
case PolicyType.TwoFactorAuthentication:
// Reorder by HasMasterPassword to prioritize checking users without a master if they have 2FA enabled
foreach (var orgUser in removableOrgUsers.OrderBy(ou => ou.HasMasterPassword))
{
if (!await userService.TwoFactorIsEnabledAsync(orgUser))
{
if (!orgUser.HasMasterPassword)
{
throw new BadRequestException(
"Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.");
}
await organizationService.RemoveUserAsync(policy.OrganizationId, orgUser.Id,
savingUserId);
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
org.DisplayName(), orgUser.Email);
}
}
break;
case PolicyType.SingleOrg:
var userOrgs = await _organizationUserRepository.GetManyByManyUsersAsync(
removableOrgUsers.Select(ou => ou.UserId.Value));
foreach (var orgUser in removableOrgUsers)
{
if (userOrgs.Any(ou => ou.UserId == orgUser.UserId
&& ou.OrganizationId != org.Id
&& ou.Status != OrganizationUserStatusType.Invited))
{
await organizationService.RemoveUserAsync(policy.OrganizationId, orgUser.Id,
savingUserId);
await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(
org.DisplayName(), orgUser.Email);
}
}
break;
default:
break;
}
}
await SetPolicyConfiguration(policy);
}
private async Task EnablePolicy_vNext(Policy policy, Organization org, IOrganizationService organizationService, Guid? savingUserId)
private async Task EnablePolicyAsync(Policy policy, Organization org, IOrganizationService organizationService, Guid? savingUserId)
{
var currentPolicy = await _policyRepository.GetByIdAsync(policy.Id);
if (!currentPolicy?.Enabled ?? true)
@ -350,7 +288,7 @@ public class PolicyService : IPolicyService
"Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.");
}
await organizationService.RemoveUserAsync(policy.OrganizationId, orgUser.Id,
await _removeOrganizationUserCommand.RemoveUserAsync(policy.OrganizationId, orgUser.Id,
savingUserId);
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
org.DisplayName(), orgUser.Email);
@ -366,7 +304,7 @@ public class PolicyService : IPolicyService
&& ou.OrganizationId != org.Id
&& ou.Status != OrganizationUserStatusType.Invited))
{
await organizationService.RemoveUserAsync(policy.OrganizationId, orgUser.Id,
await _removeOrganizationUserCommand.RemoveUserAsync(policy.OrganizationId, orgUser.Id,
savingUserId);
await _mailService.SendOrganizationUserRemovedForPolicySingleOrgEmailAsync(
org.DisplayName(), orgUser.Email);

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
@ -33,6 +34,7 @@ public class EmergencyAccessService : IEmergencyAccessService
private readonly IPasswordHasher<User> _passwordHasher;
private readonly IOrganizationService _organizationService;
private readonly IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> _dataProtectorTokenizer;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
public EmergencyAccessService(
IEmergencyAccessRepository emergencyAccessRepository,
@ -46,7 +48,8 @@ public class EmergencyAccessService : IEmergencyAccessService
IPasswordHasher<User> passwordHasher,
GlobalSettings globalSettings,
IOrganizationService organizationService,
IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> dataProtectorTokenizer)
IDataProtectorTokenFactory<EmergencyAccessInviteTokenable> dataProtectorTokenizer,
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
{
_emergencyAccessRepository = emergencyAccessRepository;
_organizationUserRepository = organizationUserRepository;
@ -60,6 +63,7 @@ public class EmergencyAccessService : IEmergencyAccessService
_globalSettings = globalSettings;
_organizationService = organizationService;
_dataProtectorTokenizer = dataProtectorTokenizer;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
}
public async Task<EmergencyAccess> InviteAsync(User invitingUser, string email, EmergencyAccessType type, int waitTime)
@ -341,7 +345,7 @@ public class EmergencyAccessService : IEmergencyAccessService
{
if (o.Type != OrganizationUserType.Owner)
{
await _organizationService.RemoveUserAsync(o.OrganizationId, grantor.Id);
await _removeOrganizationUserCommand.RemoveUserAsync(o.OrganizationId, grantor.Id);
}
}
}

View File

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

View File

@ -1,4 +1,5 @@
using Bit.Core.Billing.Models;
#nullable enable
using Bit.Core.Billing.Models;
using Bit.Core.Entities;
namespace Bit.Core.Billing.Services;
@ -8,7 +9,8 @@ public interface IPaymentHistoryService
Task<IEnumerable<BillingHistoryInfo.BillingInvoice>> GetInvoiceHistoryAsync(
ISubscriber subscriber,
int pageSize = 5,
string startAfter = null);
string? status = null,
string? startAfter = null);
Task<IEnumerable<BillingHistoryInfo.BillingTransaction>> GetTransactionHistoryAsync(
ISubscriber subscriber,

View File

@ -1,4 +1,5 @@
using Bit.Core.AdminConsole.Entities;
#nullable enable
using Bit.Core.AdminConsole.Entities;
using Bit.Core.Billing.Models;
using Bit.Core.Entities;
using Bit.Core.Models.BitStripe;
@ -16,11 +17,12 @@ public class PaymentHistoryService(
public async Task<IEnumerable<BillingHistoryInfo.BillingInvoice>> GetInvoiceHistoryAsync(
ISubscriber subscriber,
int pageSize = 5,
string startAfter = null)
string? status = null,
string? startAfter = null)
{
if (subscriber is not { GatewayCustomerId: not null, GatewaySubscriptionId: not null })
{
return null;
return Array.Empty<BillingHistoryInfo.BillingInvoice>();
}
var invoices = await stripeAdapter.InvoiceListAsync(new StripeInvoiceListOptions
@ -28,6 +30,7 @@ public class PaymentHistoryService(
Customer = subscriber.GatewayCustomerId,
Subscription = subscriber.GatewaySubscriptionId,
Limit = pageSize,
Status = status,
StartingAfter = startAfter
});
@ -48,6 +51,7 @@ public class PaymentHistoryService(
};
return transactions?.OrderByDescending(i => i.CreationDate)
.Select(t => new BillingHistoryInfo.BillingTransaction(t));
.Select(t => new BillingHistoryInfo.BillingTransaction(t))
?? Array.Empty<BillingHistoryInfo.BillingTransaction>();
}
}

View File

@ -144,6 +144,8 @@ public static class FeatureFlagKeys
public const string TrialPayment = "PM-8163-trial-payment";
public const string Pm3478RefactorOrganizationUserApi = "pm-3478-refactor-organizationuser-api";
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 static List<string> GetAllKeys()
{

View File

@ -21,8 +21,8 @@
<ItemGroup>
<PackageReference Include="AspNetCoreRateLimit.Redis" Version="2.0.0" />
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.401.16" />
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.26" />
<PackageReference Include="AWSSDK.SimpleEmail" Version="3.7.401.24" />
<PackageReference Include="AWSSDK.SQS" Version="3.7.400.34" />
<PackageReference Include="Azure.Data.Tables" Version="12.9.0" />
<PackageReference Include="Azure.Extensions.AspNetCore.DataProtection.Blobs" Version="1.3.4" />
<PackageReference Include="Microsoft.AspNetCore.DataProtection" Version="8.0.8" />
@ -36,7 +36,7 @@
<PackageReference Include="Handlebars.Net" Version="2.1.6" />
<PackageReference Include="MailKit" Version="4.8.0" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="8.0.8" />
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.43.1" />
<PackageReference Include="Microsoft.Azure.Cosmos" Version="3.44.0" />
<PackageReference Include="Microsoft.Azure.NotificationHubs" Version="4.2.0" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
<PackageReference Include="Microsoft.Extensions.Caching.Cosmos" Version="1.6.1" />
@ -46,13 +46,13 @@
<PackageReference Include="Microsoft.Extensions.Identity.Stores" Version="8.0.8" />
<PackageReference Include="Quartz" Version="3.9.0" />
<PackageReference Include="SendGrid" Version="9.29.3" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.2" />
<PackageReference Include="Serilog.AspNetCore" Version="8.0.3" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Serilog.Extensions.Logging.File" Version="3.0.0" />
<PackageReference Include="Sentry.Serilog" Version="3.41.4" />
<PackageReference Include="Duende.IdentityServer" Version="7.0.6" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="3.0.2" />
<PackageReference Include="Serilog.Sinks.SyslogMessages" Version="4.0.0" />
<PackageReference Include="AspNetCoreRateLimit" Version="5.0.0" />
<PackageReference Include="Braintree" Version="5.27.0" />
<PackageReference Include="Stripe.net" Version="45.14.0" />

View File

@ -0,0 +1,22 @@
namespace Bit.Core.Models.Data.Organizations;
public class VerifiedOrganizationDomainSsoDetail
{
public VerifiedOrganizationDomainSsoDetail()
{
}
public VerifiedOrganizationDomainSsoDetail(Guid organizationId, string organizationName, string domainName,
string organizationIdentifier)
{
OrganizationId = organizationId;
OrganizationName = organizationName;
DomainName = domainName;
OrganizationIdentifier = organizationIdentifier;
}
public Guid OrganizationId { get; init; }
public string OrganizationName { get; init; }
public string DomainName { get; init; }
public string OrganizationIdentifier { get; init; }
}

View File

@ -146,6 +146,7 @@ public static class OrganizationServiceCollectionExtensions
services.AddScoped<IAuthorizationHandler, OrganizationUserUserMiniDetailsAuthorizationHandler>();
services.AddScoped<IAuthorizationHandler, OrganizationUserUserDetailsAuthorizationHandler>();
services.AddScoped<IHasConfirmedOwnersExceptQuery, HasConfirmedOwnersExceptQuery>();
}
// TODO: move to OrganizationSubscriptionServiceCollectionExtensions when OrganizationUser methods are moved out of

View File

@ -11,6 +11,7 @@ public interface IOrganizationDomainRepository : IRepository<OrganizationDomain,
Task<ICollection<OrganizationDomain>> GetDomainsByOrganizationIdAsync(Guid orgId);
Task<ICollection<OrganizationDomain>> GetManyByNextRunDateAsync(DateTime date);
Task<OrganizationDomainSsoDetailsData?> GetOrganizationDomainSsoDetailsAsync(string email);
Task<IEnumerable<VerifiedOrganizationDomainSsoDetail>> GetVerifiedOrganizationDomainSsoDetailsAsync(string email);
Task<OrganizationDomain?> GetDomainByIdOrganizationIdAsync(Guid id, Guid organizationId);
Task<OrganizationDomain?> GetDomainByOrgIdAndDomainNameAsync(Guid orgId, string domainName);
Task<ICollection<OrganizationDomain>> GetExpiredOrganizationDomainsAsync();

View File

@ -40,10 +40,8 @@ public interface IUserService
KdfType kdf, int kdfIterations, int? kdfMemory, int? kdfParallelism);
Task<IdentityResult> RefreshSecurityStampAsync(User user, string masterPasswordHash);
Task UpdateTwoFactorProviderAsync(User user, TwoFactorProviderType type, bool setEnabled = true, bool logEvent = true);
Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type,
IOrganizationService organizationService);
Task<bool> RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode,
IOrganizationService organizationService);
Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type);
Task<bool> RecoverTwoFactorAsync(string email, string masterPassword, string recoveryCode);
Task<string> GenerateUserTokenAsync(User user, string tokenProvider, string purpose);
Task<IdentityResult> DeleteAsync(User user);
Task<IdentityResult> DeleteAsync(User user, string token);

View File

@ -1,6 +1,7 @@
using System.Security.Claims;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums;
@ -65,6 +66,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
private readonly IDataProtectorTokenFactory<OrgUserInviteTokenable> _orgUserInviteTokenDataFactory;
private readonly IFeatureService _featureService;
private readonly IPremiumUserBillingService _premiumUserBillingService;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
public UserService(
IUserRepository userRepository,
@ -98,7 +100,8 @@ public class UserService : UserManager<User>, IUserService, IDisposable
IStripeSyncService stripeSyncService,
IDataProtectorTokenFactory<OrgUserInviteTokenable> orgUserInviteTokenDataFactory,
IFeatureService featureService,
IPremiumUserBillingService premiumUserBillingService)
IPremiumUserBillingService premiumUserBillingService,
IRemoveOrganizationUserCommand removeOrganizationUserCommand)
: base(
store,
optionsAccessor,
@ -138,6 +141,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
_orgUserInviteTokenDataFactory = orgUserInviteTokenDataFactory;
_featureService = featureService;
_premiumUserBillingService = premiumUserBillingService;
_removeOrganizationUserCommand = removeOrganizationUserCommand;
}
public Guid? GetProperUserId(ClaimsPrincipal principal)
@ -827,8 +831,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
}
}
public async Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type,
IOrganizationService organizationService)
public async Task DisableTwoFactorProviderAsync(User user, TwoFactorProviderType type)
{
var providers = user.GetTwoFactorProviders();
if (!providers?.ContainsKey(type) ?? true)
@ -843,12 +846,11 @@ public class UserService : UserManager<User>, IUserService, IDisposable
if (!await TwoFactorIsEnabledAsync(user))
{
await CheckPoliciesOnTwoFactorRemovalAsync(user, organizationService);
await CheckPoliciesOnTwoFactorRemovalAsync(user);
}
}
public async Task<bool> RecoverTwoFactorAsync(string email, string secret, string recoveryCode,
IOrganizationService organizationService)
public async Task<bool> RecoverTwoFactorAsync(string email, string secret, string recoveryCode)
{
var user = await _userRepository.GetByEmailAsync(email);
if (user == null)
@ -872,7 +874,7 @@ public class UserService : UserManager<User>, IUserService, IDisposable
await SaveUserAsync(user);
await _mailService.SendRecoverTwoFactorEmail(user.Email, DateTime.UtcNow, _currentContext.IpAddress);
await _eventService.LogUserEventAsync(user.Id, EventType.User_Recovered2fa);
await CheckPoliciesOnTwoFactorRemovalAsync(user, organizationService);
await CheckPoliciesOnTwoFactorRemovalAsync(user);
return true;
}
@ -1327,13 +1329,13 @@ public class UserService : UserManager<User>, IUserService, IDisposable
}
}
private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user, IOrganizationService organizationService)
private async Task CheckPoliciesOnTwoFactorRemovalAsync(User user)
{
var twoFactorPolicies = await _policyService.GetPoliciesApplicableToUserAsync(user.Id, PolicyType.TwoFactorAuthentication);
var removeOrgUserTasks = twoFactorPolicies.Select(async p =>
{
await organizationService.RemoveUserAsync(p.OrganizationId, user.Id);
await _removeOrganizationUserCommand.RemoveUserAsync(p.OrganizationId, user.Id);
var organization = await _organizationRepository.GetByIdAsync(p.OrganizationId);
await _mailService.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(
organization.DisplayName(), user.Email);

View File

@ -1,6 +1,4 @@
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using System.Security.Claims;
using System.Security.Claims;
using System.Text.Json;
using Bit.Core;
using Bit.Core.AdminConsole.Entities;
@ -33,9 +31,8 @@ namespace Bit.Identity.IdentityServer;
public abstract class BaseRequestValidator<T> where T : class
{
private UserManager<User> _userManager;
private readonly IDeviceRepository _deviceRepository;
private readonly IDeviceService _deviceService;
private readonly IEventService _eventService;
private readonly IDeviceValidator _deviceValidator;
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider;
private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService;
private readonly IOrganizationRepository _organizationRepository;
@ -56,10 +53,9 @@ public abstract class BaseRequestValidator<T> where T : class
public BaseRequestValidator(
UserManager<User> userManager,
IDeviceRepository deviceRepository,
IDeviceService deviceService,
IUserService userService,
IEventService eventService,
IDeviceValidator deviceValidator,
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
IOrganizationRepository organizationRepository,
@ -77,10 +73,9 @@ public abstract class BaseRequestValidator<T> where T : class
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
{
_userManager = userManager;
_deviceRepository = deviceRepository;
_deviceService = deviceService;
_userService = userService;
_eventService = eventService;
_deviceValidator = deviceValidator;
_organizationDuoWebTokenProvider = organizationDuoWebTokenProvider;
_duoWebV4SDKService = duoWebV4SDKService;
_organizationRepository = organizationRepository;
@ -131,9 +126,7 @@ public abstract class BaseRequestValidator<T> where T : class
var (isTwoFactorRequired, twoFactorOrganization) = await RequiresTwoFactorAsync(user, request);
if (isTwoFactorRequired)
{
// Just defaulting it
var twoFactorProviderType = TwoFactorProviderType.Authenticator;
if (!twoFactorRequest || !Enum.TryParse(twoFactorProvider, out twoFactorProviderType))
if (!twoFactorRequest || !Enum.TryParse(twoFactorProvider, out TwoFactorProviderType twoFactorProviderType))
{
await BuildTwoFactorResultAsync(user, twoFactorOrganization, context);
return;
@ -162,7 +155,6 @@ public abstract class BaseRequestValidator<T> where T : class
twoFactorToken = null;
}
// Force legacy users to the web for migration
if (FeatureService.IsEnabled(FeatureFlagKeys.BlockLegacyUsers))
{
@ -176,7 +168,7 @@ public abstract class BaseRequestValidator<T> where T : class
// Returns true if can finish validation process
if (await IsValidAuthTypeAsync(user, request.GrantType))
{
var device = await SaveDeviceAsync(user, request);
var device = await _deviceValidator.SaveDeviceAsync(user, request);
if (device == null)
{
await BuildErrorResultAsync("No device information provided.", false, context, user);
@ -393,28 +385,6 @@ public abstract class BaseRequestValidator<T> where T : class
orgAbilities[orgId].Enabled && orgAbilities[orgId].Using2fa;
}
private Device GetDeviceFromRequest(ValidatedRequest request)
{
var deviceIdentifier = request.Raw["DeviceIdentifier"]?.ToString();
var deviceType = request.Raw["DeviceType"]?.ToString();
var deviceName = request.Raw["DeviceName"]?.ToString();
var devicePushToken = request.Raw["DevicePushToken"]?.ToString();
if (string.IsNullOrWhiteSpace(deviceIdentifier) || string.IsNullOrWhiteSpace(deviceType) ||
string.IsNullOrWhiteSpace(deviceName) || !Enum.TryParse(deviceType, out DeviceType type))
{
return null;
}
return new Device
{
Identifier = deviceIdentifier,
Name = deviceName,
Type = type,
PushToken = string.IsNullOrWhiteSpace(devicePushToken) ? null : devicePushToken
};
}
private async Task<bool> VerifyTwoFactor(User user, Organization organization, TwoFactorProviderType type,
string token)
{
@ -537,51 +507,6 @@ public abstract class BaseRequestValidator<T> where T : class
}
}
protected async Task<bool> KnownDeviceAsync(User user, ValidatedTokenRequest request) =>
(await GetKnownDeviceAsync(user, request)) != default;
protected async Task<Device> GetKnownDeviceAsync(User user, ValidatedTokenRequest request)
{
if (user == null)
{
return default;
}
return await _deviceRepository.GetByIdentifierAsync(GetDeviceFromRequest(request).Identifier, user.Id);
}
private async Task<Device> SaveDeviceAsync(User user, ValidatedTokenRequest request)
{
var device = GetDeviceFromRequest(request);
if (device != null)
{
var existingDevice = await GetKnownDeviceAsync(user, request);
if (existingDevice == null)
{
device.UserId = user.Id;
await _deviceService.SaveAsync(device);
var now = DateTime.UtcNow;
if (now - user.CreationDate > TimeSpan.FromMinutes(10))
{
var deviceType = device.Type.GetType().GetMember(device.Type.ToString())
.FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName();
if (!_globalSettings.DisableEmailNewDevice)
{
await _mailService.SendNewDeviceLoggedInEmail(user.Email, deviceType, now,
CurrentContext.IpAddress);
}
}
return device;
}
return existingDevice;
}
return null;
}
private async Task ResetFailedAuthDetailsAsync(User user)
{
// Early escape if db hit not necessary

View File

@ -29,8 +29,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
public CustomTokenRequestValidator(
UserManager<User> userManager,
IDeviceRepository deviceRepository,
IDeviceService deviceService,
IDeviceValidator deviceValidator,
IUserService userService,
IEventService eventService,
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
@ -48,7 +47,7 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
IDataProtectorTokenFactory<SsoEmail2faSessionTokenable> tokenDataFactory,
IFeatureService featureService,
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
: base(userManager, deviceRepository, deviceService, userService, eventService,
: base(userManager, userService, eventService, deviceValidator,
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository,
applicationCacheService, mailService, logger, currentContext, globalSettings,
userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository,
@ -83,11 +82,8 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
{
context.Result.CustomResponse = new Dictionary<string, object> { { "encrypted_payload", payload } };
}
return;
}
await ValidateAsync(context, context.Result.ValidatedRequest,
new CustomValidatorRequestContext { KnownDevice = true });
}
@ -103,7 +99,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
{
validatorContext.User = await _userManager.FindByEmailAsync(email);
}
return validatorContext.User != null;
}
@ -121,7 +116,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
context.Result.ValidatedRequest.ClientClaims.Add(claim);
}
}
if (context.Result.CustomResponse == null || user.MasterPassword != null)
{
return Task.CompletedTask;
@ -138,7 +132,6 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
context.Result.CustomResponse["ApiUseKeyConnector"] = true;
context.Result.CustomResponse["ResetMasterPassword"] = false;
}
return Task.CompletedTask;
}
@ -150,13 +143,11 @@ public class CustomTokenRequestValidator : BaseRequestValidator<CustomTokenReque
{
return Task.CompletedTask;
}
if (userDecryptionOptions is { KeyConnectorOption: { } })
{
context.Result.CustomResponse["KeyConnectorUrl"] = userDecryptionOptions.KeyConnectorOption.KeyConnectorUrl;
context.Result.CustomResponse["ResetMasterPassword"] = false;
}
return Task.CompletedTask;
}

View File

@ -0,0 +1,109 @@
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Duende.IdentityServer.Validation;
namespace Bit.Identity.IdentityServer;
public interface IDeviceValidator
{
/// <summary>
/// Save a device to the database. If the device is already known, it will be returned.
/// </summary>
/// <param name="user">The user is assumed NOT null, still going to check though</param>
/// <param name="request">Duende Validated Request that contains the data to create the device object</param>
/// <returns>Returns null if user or device is malformed; The existing device if already in DB; a new device login</returns>
Task<Device> SaveDeviceAsync(User user, ValidatedTokenRequest request);
/// <summary>
/// Check if a device is known to the user.
/// </summary>
/// <param name="user">current user trying to authenticate</param>
/// <param name="request">contains raw information that is parsed about the device</param>
/// <returns>true if the device is known, false if it is not</returns>
Task<bool> KnownDeviceAsync(User user, ValidatedTokenRequest request);
}
public class DeviceValidator(
IDeviceService deviceService,
IDeviceRepository deviceRepository,
GlobalSettings globalSettings,
IMailService mailService,
ICurrentContext currentContext) : IDeviceValidator
{
private readonly IDeviceService _deviceService = deviceService;
private readonly IDeviceRepository _deviceRepository = deviceRepository;
private readonly GlobalSettings _globalSettings = globalSettings;
private readonly IMailService _mailService = mailService;
private readonly ICurrentContext _currentContext = currentContext;
public async Task<Device> SaveDeviceAsync(User user, ValidatedTokenRequest request)
{
var device = GetDeviceFromRequest(request);
if (device != null && user != null)
{
var existingDevice = await GetKnownDeviceAsync(user, device);
if (existingDevice == null)
{
device.UserId = user.Id;
await _deviceService.SaveAsync(device);
// This makes sure the user isn't sent a "new device" email on their first login
var now = DateTime.UtcNow;
if (now - user.CreationDate > TimeSpan.FromMinutes(10))
{
var deviceType = device.Type.GetType().GetMember(device.Type.ToString())
.FirstOrDefault()?.GetCustomAttribute<DisplayAttribute>()?.GetName();
if (!_globalSettings.DisableEmailNewDevice)
{
await _mailService.SendNewDeviceLoggedInEmail(user.Email, deviceType, now,
_currentContext.IpAddress);
}
}
return device;
}
return existingDevice;
}
return null;
}
public async Task<bool> KnownDeviceAsync(User user, ValidatedTokenRequest request) =>
(await GetKnownDeviceAsync(user, GetDeviceFromRequest(request))) != default;
private async Task<Device> GetKnownDeviceAsync(User user, Device device)
{
if (user == null || device == null)
{
return default;
}
return await _deviceRepository.GetByIdentifierAsync(device.Identifier, user.Id);
}
private static Device GetDeviceFromRequest(ValidatedRequest request)
{
var deviceIdentifier = request.Raw["DeviceIdentifier"]?.ToString();
var requestDeviceType = request.Raw["DeviceType"]?.ToString();
var deviceName = request.Raw["DeviceName"]?.ToString();
var devicePushToken = request.Raw["DevicePushToken"]?.ToString();
if (string.IsNullOrWhiteSpace(deviceIdentifier) ||
string.IsNullOrWhiteSpace(requestDeviceType) ||
string.IsNullOrWhiteSpace(deviceName) ||
!Enum.TryParse(requestDeviceType, out DeviceType parsedDeviceType))
{
return null;
}
return new Device
{
Identifier = deviceIdentifier,
Name = deviceName,
Type = parsedDeviceType,
PushToken = string.IsNullOrWhiteSpace(devicePushToken) ? null : devicePushToken
};
}
}

View File

@ -25,12 +25,12 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
private readonly ICurrentContext _currentContext;
private readonly ICaptchaValidationService _captchaValidationService;
private readonly IAuthRequestRepository _authRequestRepository;
private readonly IDeviceValidator _deviceValidator;
public ResourceOwnerPasswordValidator(
UserManager<User> userManager,
IDeviceRepository deviceRepository,
IDeviceService deviceService,
IUserService userService,
IEventService eventService,
IDeviceValidator deviceValidator,
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
IOrganizationRepository organizationRepository,
@ -48,7 +48,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
IFeatureService featureService,
ISsoConfigRepository ssoConfigRepository,
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder)
: base(userManager, deviceRepository, deviceService, userService, eventService,
: base(userManager, userService, eventService, deviceValidator,
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository,
applicationCacheService, mailService, logger, currentContext, globalSettings, userRepository, policyService,
tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder)
@ -57,6 +57,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
_currentContext = currentContext;
_captchaValidationService = captchaValidationService;
_authRequestRepository = authRequestRepository;
_deviceValidator = deviceValidator;
}
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
@ -72,7 +73,7 @@ public class ResourceOwnerPasswordValidator : BaseRequestValidator<ResourceOwner
var validatorContext = new CustomValidatorRequestContext
{
User = user,
KnownDevice = await KnownDeviceAsync(user, context.Request)
KnownDevice = await _deviceValidator.KnownDeviceAsync(user, context.Request),
};
string bypassToken = null;
if (!validatorContext.KnownDevice &&

View File

@ -27,13 +27,13 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
private readonly IDataProtectorTokenFactory<WebAuthnLoginAssertionOptionsTokenable> _assertionOptionsDataProtector;
private readonly IAssertWebAuthnLoginCredentialCommand _assertWebAuthnLoginCredentialCommand;
private readonly IDeviceValidator _deviceValidator;
public WebAuthnGrantValidator(
UserManager<User> userManager,
IDeviceRepository deviceRepository,
IDeviceService deviceService,
IUserService userService,
IEventService eventService,
IDeviceValidator deviceValidator,
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
IOrganizationRepository organizationRepository,
@ -52,13 +52,14 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder,
IAssertWebAuthnLoginCredentialCommand assertWebAuthnLoginCredentialCommand
)
: base(userManager, deviceRepository, deviceService, userService, eventService,
: base(userManager, userService, eventService, deviceValidator,
organizationDuoWebTokenProvider, duoWebV4SDKService, organizationRepository, organizationUserRepository,
applicationCacheService, mailService, logger, currentContext, globalSettings,
userRepository, policyService, tokenDataFactory, featureService, ssoConfigRepository, userDecryptionOptionsBuilder)
{
_assertionOptionsDataProtector = assertionOptionsDataProtector;
_assertWebAuthnLoginCredentialCommand = assertWebAuthnLoginCredentialCommand;
_deviceValidator = deviceValidator;
}
string IExtensionGrantValidator.GrantType => "webauthn";
@ -87,7 +88,7 @@ public class WebAuthnGrantValidator : BaseRequestValidator<ExtensionGrantValidat
var validatorContext = new CustomValidatorRequestContext
{
User = user,
KnownDevice = await KnownDeviceAsync(user, context.Request)
KnownDevice = await _deviceValidator.KnownDeviceAsync(user, context.Request)
};
UserDecryptionOptionsBuilder.WithWebAuthnLoginCredential(credential);

View File

@ -20,6 +20,7 @@ public static class ServiceCollectionExtensions
services.AddSingleton<StaticClientStore>();
services.AddTransient<IAuthorizationCodeStore, AuthorizationCodeStore>();
services.AddTransient<IUserDecryptionOptionsBuilder, UserDecryptionOptionsBuilder>();
services.AddTransient<IDeviceValidator, DeviceValidator>();
var issuerUri = new Uri(globalSettings.BaseServiceUri.InternalIdentity);
var identityServerBuilder = services

View File

@ -71,6 +71,17 @@ public class OrganizationDomainRepository : Repository<OrganizationDomain, Guid>
}
}
public async Task<IEnumerable<VerifiedOrganizationDomainSsoDetail>> GetVerifiedOrganizationDomainSsoDetailsAsync(string email)
{
await using var connection = new SqlConnection(ConnectionString);
return await connection
.QueryAsync<VerifiedOrganizationDomainSsoDetail>(
$"[{Schema}].[VerifiedOrganizationDomainSsoDetails_ReadByEmail]",
new { Email = email },
commandType: CommandType.StoredProcedure);
}
public async Task<OrganizationDomain?> GetDomainByIdOrganizationIdAsync(Guid id, Guid orgId)
{
using (var connection = new SqlConnection(ConnectionString))

View File

@ -95,6 +95,29 @@ public class OrganizationDomainRepository : Repository<Core.Entities.Organizatio
return ssoDetails;
}
public async Task<IEnumerable<VerifiedOrganizationDomainSsoDetail>> GetVerifiedOrganizationDomainSsoDetailsAsync(string email)
{
var domainName = new MailAddress(email).Host;
using var scope = ServiceScopeFactory.CreateScope();
var dbContext = GetDatabaseContext(scope);
return await (from o in dbContext.Organizations
from od in o.Domains
join s in dbContext.SsoConfigs on o.Id equals s.OrganizationId into sJoin
from s in sJoin.DefaultIfEmpty()
where od.DomainName == domainName
&& o.Enabled
&& s.Enabled
&& od.VerifiedDate != null
select new VerifiedOrganizationDomainSsoDetail(
o.Id,
o.Name,
od.DomainName,
o.Identifier))
.AsNoTracking()
.ToListAsync();
}
public async Task<Core.Entities.OrganizationDomain?> GetDomainByIdOrganizationIdAsync(Guid id, Guid orgId)
{
using var scope = ServiceScopeFactory.CreateScope();

View File

@ -7,7 +7,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.8.0" />
<PackageReference Include="Swashbuckle.AspNetCore.SwaggerGen" Version="6.8.1" />
</ItemGroup>
</Project>

View File

@ -38,7 +38,8 @@ public sealed class RequestLoggingMiddleware
new RequestLogScope(context.GetIpAddress(_globalSettings),
GetHeaderValue(context, "user-agent"),
GetHeaderValue(context, "device-type"),
GetHeaderValue(context, "device-type"))))
GetHeaderValue(context, "device-type"),
GetHeaderValue(context, "bitwarden-client-version"))))
{
return _next(context);
}
@ -59,12 +60,13 @@ public sealed class RequestLoggingMiddleware
{
private string? _cachedToString;
public RequestLogScope(string? ipAddress, string? userAgent, string? deviceType, string? origin)
public RequestLogScope(string? ipAddress, string? userAgent, string? deviceType, string? origin, string? clientVersion)
{
IpAddress = ipAddress;
UserAgent = userAgent;
DeviceType = deviceType;
Origin = origin;
ClientVersion = clientVersion;
}
public KeyValuePair<string, object?> this[int index]
@ -87,17 +89,22 @@ public sealed class RequestLoggingMiddleware
{
return new KeyValuePair<string, object?>(nameof(Origin), Origin);
}
else if (index == 4)
{
return new KeyValuePair<string, object?>(nameof(ClientVersion), ClientVersion);
}
throw new ArgumentOutOfRangeException(nameof(index));
}
}
public int Count => 4;
public int Count => 5;
public string? IpAddress { get; }
public string? UserAgent { get; }
public string? DeviceType { get; }
public string? Origin { get; }
public string? ClientVersion { get; }
public IEnumerator<KeyValuePair<string, object?>> GetEnumerator()
{
@ -110,7 +117,7 @@ public sealed class RequestLoggingMiddleware
public override string ToString()
{
_cachedToString ??= $"IpAddress:{IpAddress} UserAgent:{UserAgent} DeviceType:{DeviceType} Origin:{Origin}";
_cachedToString ??= $"IpAddress:{IpAddress} UserAgent:{UserAgent} DeviceType:{DeviceType} Origin:{Origin} ClientVersion:{ClientVersion}";
return _cachedToString;
}
}

View File

@ -0,0 +1,23 @@
CREATE PROCEDURE [dbo].[VerifiedOrganizationDomainSsoDetails_ReadByEmail]
@Email NVARCHAR(256)
AS
BEGIN
SET NOCOUNT ON
DECLARE @Domain NVARCHAR(256)
SELECT @Domain = SUBSTRING(@Email, CHARINDEX( '@', @Email) + 1, LEN(@Email))
SELECT
O.Id AS OrganizationId,
O.Name AS OrganizationName,
O.Identifier AS OrganizationIdentifier,
OD.DomainName
FROM [dbo].[OrganizationView] O
INNER JOIN [dbo].[OrganizationDomainView] OD ON O.Id = OD.OrganizationId
LEFT JOIN [dbo].[Ssoconfig] S ON O.Id = S.OrganizationId
WHERE OD.DomainName = @Domain
AND O.Enabled = 1
AND OD.VerifiedDate IS NOT NULL
AND S.Enabled = 1
END

View File

@ -316,4 +316,26 @@ public class OrganizationDomainControllerTests
Assert.IsType<OrganizationDomainSsoDetailsResponseModel>(result);
}
[Theory, BitAutoData]
public async Task GetVerifiedOrgDomainSsoDetails_ShouldThrowNotFound_WhenEmailHasNotClaimedDomain(
OrganizationDomainSsoDetailsRequestModel model, SutProvider<OrganizationDomainController> sutProvider)
{
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetVerifiedOrganizationDomainSsoDetailsAsync(model.Email).Returns(Array.Empty<VerifiedOrganizationDomainSsoDetail>());
await Assert.ThrowsAsync<NotFoundException>(() => sutProvider.Sut.GetOrgDomainSsoDetails(model));
}
[Theory, BitAutoData]
public async Task GetVerifiedOrgDomainSsoDetails_ShouldReturnOrganizationDomainSsoDetails_WhenEmailHasClaimedDomain(
OrganizationDomainSsoDetailsRequestModel model, IEnumerable<VerifiedOrganizationDomainSsoDetail> ssoDetailsData, SutProvider<OrganizationDomainController> sutProvider)
{
sutProvider.GetDependency<IOrganizationDomainRepository>()
.GetVerifiedOrganizationDomainSsoDetailsAsync(model.Email).Returns(ssoDetailsData);
var result = await sutProvider.Sut.GetVerifiedOrgDomainSsoDetailsAsync(model);
Assert.IsType<VerifiedOrganizationDomainSsoDetailsResponseModel>(result);
}
}

View File

@ -7,6 +7,7 @@ using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.Models.Business.Tokenables;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationApiKeys.Interfaces;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
@ -49,6 +50,7 @@ public class OrganizationsControllerTests : IDisposable
private readonly IProviderRepository _providerRepository;
private readonly IProviderBillingService _providerBillingService;
private readonly IDataProtectorTokenFactory<OrgDeleteTokenable> _orgDeleteTokenDataFactory;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly OrganizationsController _sut;
@ -72,6 +74,7 @@ public class OrganizationsControllerTests : IDisposable
_providerRepository = Substitute.For<IProviderRepository>();
_providerBillingService = Substitute.For<IProviderBillingService>();
_orgDeleteTokenDataFactory = Substitute.For<IDataProtectorTokenFactory<OrgDeleteTokenable>>();
_removeOrganizationUserCommand = Substitute.For<IRemoveOrganizationUserCommand>();
_sut = new OrganizationsController(
_organizationRepository,
@ -91,7 +94,8 @@ public class OrganizationsControllerTests : IDisposable
_pushNotificationService,
_providerRepository,
_providerBillingService,
_orgDeleteTokenDataFactory);
_orgDeleteTokenDataFactory,
_removeOrganizationUserCommand);
}
public void Dispose()
@ -120,13 +124,12 @@ public class OrganizationsControllerTests : IDisposable
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => _sut.Leave(orgId.ToString()));
var exception = await Assert.ThrowsAsync<BadRequestException>(() => _sut.Leave(orgId));
Assert.Contains("Your organization's Single Sign-On settings prevent you from leaving.",
exception.Message);
await _organizationService.DidNotReceiveWithAnyArgs().RemoveUserAsync(default, default);
await _removeOrganizationUserCommand.DidNotReceiveWithAnyArgs().RemoveUserAsync(default, default);
}
[Theory]
@ -155,8 +158,9 @@ public class OrganizationsControllerTests : IDisposable
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
await _organizationService.RemoveUserAsync(orgId, user.Id);
await _organizationService.Received(1).RemoveUserAsync(orgId, user.Id);
await _sut.Leave(orgId);
await _removeOrganizationUserCommand.Received(1).RemoveUserAsync(orgId, user.Id);
}
[Theory, AutoData]

View File

@ -4,8 +4,8 @@ using Bit.Api.AdminConsole.Models.Request.Organizations;
using Bit.Api.Billing.Controllers;
using Bit.Api.Models.Request.Organizations;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
@ -46,6 +46,7 @@ public class OrganizationsControllerTests : IDisposable
private readonly IAddSecretsManagerSubscriptionCommand _addSecretsManagerSubscriptionCommand;
private readonly IReferenceEventService _referenceEventService;
private readonly ISubscriberService _subscriberService;
private readonly IRemoveOrganizationUserCommand _removeOrganizationUserCommand;
private readonly OrganizationsController _sut;
@ -68,6 +69,7 @@ public class OrganizationsControllerTests : IDisposable
_addSecretsManagerSubscriptionCommand = Substitute.For<IAddSecretsManagerSubscriptionCommand>();
_referenceEventService = Substitute.For<IReferenceEventService>();
_subscriberService = Substitute.For<ISubscriberService>();
_removeOrganizationUserCommand = Substitute.For<IRemoveOrganizationUserCommand>();
_sut = new OrganizationsController(
_organizationRepository,
@ -91,36 +93,6 @@ public class OrganizationsControllerTests : IDisposable
_sut?.Dispose();
}
[Theory]
[InlineAutoData(true, false)]
[InlineAutoData(false, true)]
[InlineAutoData(false, false)]
public async Task OrganizationsController_UserCanLeaveOrganizationThatDoesntProvideKeyConnector(
bool keyConnectorEnabled, bool userUsesKeyConnector, Guid orgId, User user)
{
var ssoConfig = new SsoConfig
{
Id = default,
Data = new SsoConfigurationData
{
MemberDecryptionType = keyConnectorEnabled
? MemberDecryptionType.KeyConnector
: MemberDecryptionType.MasterPassword
}.Serialize(),
Enabled = true,
OrganizationId = orgId,
};
user.UsesKeyConnector = userUsesKeyConnector;
_currentContext.OrganizationUser(orgId).Returns(true);
_ssoConfigRepository.GetByOrganizationIdAsync(orgId).Returns(ssoConfig);
_userService.GetUserByPrincipalAsync(Arg.Any<ClaimsPrincipal>()).Returns(user);
await _organizationService.RemoveUserAsync(orgId, user.Id);
await _organizationService.Received(1).RemoveUserAsync(orgId, user.Id);
}
[Theory, AutoData]
public async Task OrganizationsController_PostUpgrade_UserCannotEditSubscription_ThrowsNotFoundException(
Guid organizationId,

View File

@ -14,7 +14,7 @@
<PackageReference Include="AutoFixture.Xunit2" Version="$(AutoFixtureXUnit2Version)" />
<PackageReference Include="AutoFixture.AutoNSubstitute" Version="$(AutoFixtureAutoNSubstituteVersion)" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
<PackageReference Include="Kralizek.AutoFixture.Extensions.MockHttp" Version="2.0.0" />
<PackageReference Include="Kralizek.AutoFixture.Extensions.MockHttp" Version="2.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Core\Core.csproj" />

View File

@ -40,7 +40,7 @@ public class DeleteManagedOrganizationUserAccountCommandTests
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)))
.Returns(new Dictionary<Guid, bool> { { organizationUser.Id, true } });
sutProvider.GetDependency<IOrganizationService>()
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(
organizationUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)),
@ -184,7 +184,7 @@ public class DeleteManagedOrganizationUserAccountCommandTests
.OrganizationOwner(organizationUser.OrganizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationService>()
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(
organizationUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(organizationUser.Id)),
@ -399,8 +399,8 @@ public class DeleteManagedOrganizationUserAccountCommandTests
.OrganizationOwner(orgUser.OrganizationId)
.Returns(true);
sutProvider.GetDependency<IOrganizationService>()
.HasConfirmedOwnersExceptAsync(orgUser.OrganizationId, Arg.Any<IEnumerable<Guid>>(), true)
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(orgUser.OrganizationId, Arg.Any<IEnumerable<Guid>>(), Arg.Any<bool>())
.Returns(false);
// Act

View File

@ -0,0 +1,139 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Entities.Provider;
using Bit.Core.AdminConsole.Enums.Provider;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
using Xunit;
namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers;
[SutProviderCustomize]
public class HasConfirmedOwnersExceptQueryTests
{
[Theory, BitAutoData]
public async Task HasConfirmedOwnersExcept_WithConfirmedOwner_WithNoException_ReturnsTrue(
Organization organization,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
SutProvider<HasConfirmedOwnersExceptQuery> sutProvider)
{
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner)
.Returns(new List<OrganizationUser> { owner });
var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List<Guid>(), true);
Assert.True(result);
}
[Theory, BitAutoData]
public async Task HasConfirmedOwnersExcept_ExcludingConfirmedOwner_ReturnsFalse(
Organization organization,
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser owner,
SutProvider<HasConfirmedOwnersExceptQuery> sutProvider)
{
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner)
.Returns(new List<OrganizationUser> { owner });
var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List<Guid> { owner.Id }, true);
Assert.False(result);
}
[Theory, BitAutoData]
public async Task HasConfirmedOwnersExcept_WithInvitedOwner_ReturnsFalse(
Organization organization,
[OrganizationUser(OrganizationUserStatusType.Invited, OrganizationUserType.Owner)] OrganizationUser owner,
SutProvider<HasConfirmedOwnersExceptQuery> sutProvider)
{
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByOrganizationAsync(organization.Id, OrganizationUserType.Owner)
.Returns(new List<OrganizationUser> { owner });
var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List<Guid>(), true);
Assert.False(result);
}
[Theory]
[BitAutoData(true)]
[BitAutoData(false)]
public async Task HasConfirmedOwnersExcept_WithConfirmedProviderUser_IncludeProviderTrue_ReturnsTrue(
bool includeProvider,
Organization organization,
ProviderUser providerUser,
SutProvider<HasConfirmedOwnersExceptQuery> sutProvider)
{
providerUser.Status = ProviderUserStatusType.Confirmed;
sutProvider.GetDependency<IProviderUserRepository>()
.GetManyByOrganizationAsync(organization.Id, ProviderUserStatusType.Confirmed)
.Returns(new List<ProviderUser> { providerUser });
var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organization.Id, new List<Guid>(), includeProvider);
Assert.Equal(includeProvider, result);
}
[Theory, BitAutoData]
public async Task HasConfirmedOwnersExceptAsync_WithConfirmedOwners_ReturnsTrue(
Guid organizationId,
IEnumerable<Guid> organizationUsersId,
ICollection<OrganizationUser> owners,
SutProvider<HasConfirmedOwnersExceptQuery> sutProvider)
{
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByOrganizationAsync(organizationId, OrganizationUserType.Owner)
.Returns(owners);
var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId);
Assert.True(result);
}
[Theory, BitAutoData]
public async Task HasConfirmedOwnersExceptAsync_WithConfirmedProviders_ReturnsTrue(
Guid organizationId,
IEnumerable<Guid> organizationUsersId,
ICollection<ProviderUser> providerUsers,
SutProvider<HasConfirmedOwnersExceptQuery> sutProvider)
{
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByOrganizationAsync(organizationId, OrganizationUserType.Owner)
.Returns(new List<OrganizationUser>());
sutProvider.GetDependency<IProviderUserRepository>()
.GetManyByOrganizationAsync(organizationId, ProviderUserStatusType.Confirmed)
.Returns(providerUsers);
var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId);
Assert.True(result);
}
[Theory, BitAutoData]
public async Task HasConfirmedOwnersExceptAsync_WithNoConfirmedOwnersOrProviders_ReturnsFalse(
Guid organizationId,
IEnumerable<Guid> organizationUsersId,
SutProvider<HasConfirmedOwnersExceptQuery> sutProvider)
{
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetManyByOrganizationAsync(organizationId, OrganizationUserType.Owner)
.Returns(new List<OrganizationUser>());
sutProvider.GetDependency<IProviderUserRepository>()
.GetManyByOrganizationAsync(organizationId, ProviderUserStatusType.Confirmed)
.Returns(new List<ProviderUser>());
var result = await sutProvider.Sut.HasConfirmedOwnersExceptAsync(organizationId, organizationUsersId);
Assert.False(result);
}
}

View File

@ -1,9 +1,12 @@
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Test.AutoFixture.OrganizationUserFixtures;
using Bit.Test.Common.AutoFixture;
using Bit.Test.Common.AutoFixture.Attributes;
using NSubstitute;
@ -14,33 +17,38 @@ namespace Bit.Core.Test.AdminConsole.OrganizationFeatures.OrganizationUsers;
[SutProviderCustomize]
public class RemoveOrganizationUserCommandTests
{
[Theory]
[BitAutoData]
public async Task RemoveUser_Success(SutProvider<RemoveOrganizationUserCommand> sutProvider, Guid organizationId, Guid organizationUserId)
[Theory, BitAutoData]
public async Task RemoveUser_Success(
[OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser,
[OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser deletingUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUserId)
.Returns(new OrganizationUser
{
Id = organizationUserId,
OrganizationId = organizationId
});
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var currentContext = sutProvider.GetDependency<ICurrentContext>();
await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null);
organizationUser.OrganizationId = deletingUser.OrganizationId;
organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser);
organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser);
currentContext.OrganizationOwner(deletingUser.OrganizationId).Returns(true);
await sutProvider.GetDependency<IOrganizationService>().Received(1).RemoveUserAsync(organizationId, organizationUserId, null);
await sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId);
await organizationUserRepository.Received(1).DeleteAsync(organizationUser);
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed);
}
[Theory]
[BitAutoData]
public async Task RemoveUser_NotFound_Throws(SutProvider<RemoveOrganizationUserCommand> sutProvider, Guid organizationId, Guid organizationUserId)
public async Task RemoveUser_NotFound_ThrowsException(SutProvider<RemoveOrganizationUserCommand> sutProvider,
Guid organizationId, Guid organizationUserId)
{
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null));
}
[Theory]
[BitAutoData]
public async Task RemoveUser_MismatchingOrganizationId_Throws(SutProvider<RemoveOrganizationUserCommand> sutProvider, Guid organizationId, Guid organizationUserId)
public async Task RemoveUser_MismatchingOrganizationId_ThrowsException(
SutProvider<RemoveOrganizationUserCommand> sutProvider, Guid organizationId, Guid organizationUserId)
{
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUserId)
@ -53,20 +61,249 @@ public class RemoveOrganizationUserCommandTests
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, null));
}
[Theory]
[BitAutoData]
public async Task RemoveUser_WithEventSystemUser_Success(SutProvider<RemoveOrganizationUserCommand> sutProvider, Guid organizationId, Guid organizationUserId, EventSystemUser eventSystemUser)
[Theory, BitAutoData]
public async Task RemoveUser_InvalidUser_ThrowsException(
OrganizationUser organizationUser, OrganizationUser deletingUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
sutProvider.GetDependency<IOrganizationUserRepository>()
.GetByIdAsync(organizationUserId)
.Returns(new OrganizationUser
{
Id = organizationUserId,
OrganizationId = organizationId
});
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
await sutProvider.Sut.RemoveUserAsync(organizationId, organizationUserId, eventSystemUser);
organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser);
await sutProvider.GetDependency<IOrganizationService>().Received(1).RemoveUserAsync(organizationId, organizationUserId, eventSystemUser);
var exception = await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.RemoveUserAsync(Guid.NewGuid(), organizationUser.Id, deletingUser.UserId));
Assert.Contains("User not found.", exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveUser_RemoveYourself_ThrowsException(OrganizationUser deletingUser, SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, deletingUser.Id, deletingUser.UserId));
Assert.Contains("You cannot remove yourself.", exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveUser_NonOwnerRemoveOwner_ThrowsException(
[OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser,
[OrganizationUser(type: OrganizationUserType.Admin)] OrganizationUser deletingUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var currentContext = sutProvider.GetDependency<ICurrentContext>();
organizationUser.OrganizationId = deletingUser.OrganizationId;
organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser);
currentContext.OrganizationAdmin(deletingUser.OrganizationId).Returns(true);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, deletingUser.UserId));
Assert.Contains("Only owners can delete other owners.", exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveUser_RemovingLastOwner_ThrowsException(
[OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser,
OrganizationUser deletingUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var hasConfirmedOwnersExceptQuery = sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>();
organizationUser.OrganizationId = deletingUser.OrganizationId;
organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser);
hasConfirmedOwnersExceptQuery.HasConfirmedOwnersExceptAsync(
deletingUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)), Arg.Any<bool>())
.Returns(false);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RemoveUserAsync(deletingUser.OrganizationId, organizationUser.Id, null));
Assert.Contains("Organization must have at least one confirmed owner.", exception.Message);
hasConfirmedOwnersExceptQuery
.Received(1)
.HasConfirmedOwnersExceptAsync(
organizationUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)), true);
}
[Theory, BitAutoData]
public async Task RemoveUser_WithEventSystemUser_Success(
[OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser,
EventSystemUser eventSystemUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
organizationUserRepository.GetByIdAsync(organizationUser.Id).Returns(organizationUser);
await sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.Id, eventSystemUser);
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed, eventSystemUser);
}
[Theory, BitAutoData]
public async Task RemoveUser_ByUserId_Success(
[OrganizationUser(type: OrganizationUserType.User)] OrganizationUser organizationUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
organizationUserRepository
.GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value)
.Returns(organizationUser);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(
organizationUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)),
Arg.Any<bool>())
.Returns(true);
await sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.UserId.Value);
await sutProvider.GetDependency<IEventService>().Received(1).LogOrganizationUserEventAsync(organizationUser, EventType.OrganizationUser_Removed);
}
[Theory, BitAutoData]
public async Task RemoveUser_ByUserId_NotFound_ThrowsException(SutProvider<RemoveOrganizationUserCommand> sutProvider,
Guid organizationId, Guid userId)
{
await Assert.ThrowsAsync<NotFoundException>(async () => await sutProvider.Sut.RemoveUserAsync(organizationId, userId));
}
[Theory, BitAutoData]
public async Task RemoveUser_ByUserId_InvalidUser_ThrowsException(OrganizationUser organizationUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
organizationUserRepository.GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value).Returns(organizationUser);
var exception = await Assert.ThrowsAsync<NotFoundException>(
() => sutProvider.Sut.RemoveUserAsync(Guid.NewGuid(), organizationUser.UserId.Value));
Assert.Contains("User not found.", exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveUser_ByUserId_RemovingLastOwner_ThrowsException(
[OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser organizationUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var hasConfirmedOwnersExceptQuery = sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>();
organizationUserRepository.GetByOrganizationAsync(organizationUser.OrganizationId, organizationUser.UserId!.Value).Returns(organizationUser);
hasConfirmedOwnersExceptQuery
.HasConfirmedOwnersExceptAsync(
organizationUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)),
Arg.Any<bool>())
.Returns(false);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RemoveUserAsync(organizationUser.OrganizationId, organizationUser.UserId.Value));
Assert.Contains("Organization must have at least one confirmed owner.", exception.Message);
hasConfirmedOwnersExceptQuery
.Received(1)
.HasConfirmedOwnersExceptAsync(
organizationUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(i => i.Contains(organizationUser.Id)),
Arg.Any<bool>());
}
[Theory, BitAutoData]
public async Task RemoveUsers_FilterInvalid_ThrowsException(OrganizationUser organizationUser, OrganizationUser deletingUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var organizationUsers = new[] { organizationUser };
var organizationUserIds = organizationUsers.Select(u => u.Id);
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId));
Assert.Contains("Users invalid.", exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveUsers_RemoveYourself_ThrowsException(
OrganizationUser deletingUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var organizationUsers = new[] { deletingUser };
var organizationUserIds = organizationUsers.Select(u => u.Id);
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(true);
var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId);
Assert.Contains("You cannot remove yourself.", result[0].Item2);
}
[Theory, BitAutoData]
public async Task RemoveUsers_NonOwnerRemoveOwner_ThrowsException(
[OrganizationUser(type: OrganizationUserType.Admin)] OrganizationUser deletingUser,
[OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1,
[OrganizationUser(OrganizationUserStatusType.Confirmed)] OrganizationUser orgUser2,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId;
var organizationUsers = new[] { orgUser1 };
var organizationUserIds = organizationUsers.Select(u => u.Id);
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(true);
var result = await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId);
Assert.Contains("Only owners can delete other owners.", result[0].Item2);
}
[Theory, BitAutoData]
public async Task RemoveUsers_LastOwner_ThrowsException(
[OrganizationUser(status: OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser orgUser,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var organizationUsers = new[] { orgUser };
var organizationUserIds = organizationUsers.Select(u => u.Id);
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers);
organizationUserRepository.GetManyByOrganizationAsync(orgUser.OrganizationId, OrganizationUserType.Owner).Returns(organizationUsers);
var exception = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.RemoveUsersAsync(orgUser.OrganizationId, organizationUserIds, null));
Assert.Contains("Organization must have at least one confirmed owner.", exception.Message);
}
[Theory, BitAutoData]
public async Task RemoveUsers_Success(
[OrganizationUser(OrganizationUserStatusType.Confirmed, OrganizationUserType.Owner)] OrganizationUser deletingUser,
[OrganizationUser(type: OrganizationUserType.Owner)] OrganizationUser orgUser1, OrganizationUser orgUser2,
SutProvider<RemoveOrganizationUserCommand> sutProvider)
{
var organizationUserRepository = sutProvider.GetDependency<IOrganizationUserRepository>();
var currentContext = sutProvider.GetDependency<ICurrentContext>();
orgUser1.OrganizationId = orgUser2.OrganizationId = deletingUser.OrganizationId;
var organizationUsers = new[] { orgUser1, orgUser2 };
var organizationUserIds = organizationUsers.Select(u => u.Id);
organizationUserRepository.GetManyAsync(default).ReturnsForAnyArgs(organizationUsers);
organizationUserRepository.GetByIdAsync(deletingUser.Id).Returns(deletingUser);
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(deletingUser.OrganizationId, Arg.Any<IEnumerable<Guid>>())
.Returns(true);
currentContext.OrganizationOwner(deletingUser.OrganizationId).Returns(true);
await sutProvider.Sut.RemoveUsersAsync(deletingUser.OrganizationId, organizationUserIds, deletingUser.UserId);
}
}

View File

@ -1,6 +1,7 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.Entities;
using Bit.Core.Enums;
@ -169,7 +170,7 @@ public class UpdateOrganizationUserCommandTests
await organizationService.Received(1).ValidateOrganizationCustomPermissionsEnabledAsync(
newUserData.OrganizationId,
newUserData.Type);
await organizationService.Received(1).HasConfirmedOwnersExceptAsync(
await sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>().Received(1).HasConfirmedOwnersExceptAsync(
newUserData.OrganizationId,
Arg.Is<IEnumerable<Guid>>(i => i.Contains(newUserData.Id)));
}
@ -187,7 +188,7 @@ public class UpdateOrganizationUserCommandTests
newUser.UserId = oldUser.UserId;
newUser.OrganizationId = oldUser.OrganizationId = organization.Id;
organizationUserRepository.GetByIdAsync(oldUser.Id).Returns(oldUser);
organizationService
sutProvider.GetDependency<IHasConfirmedOwnersExceptQuery>()
.HasConfirmedOwnersExceptAsync(
oldUser.OrganizationId,
Arg.Is<IEnumerable<Guid>>(i => i.Contains(oldUser.Id)))

View File

@ -1,12 +1,14 @@
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.Enums;
using Bit.Core.AdminConsole.Models.Data.Organizations.Policies;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services.Implementations;
using Bit.Core.Auth.Entities;
using Bit.Core.Auth.Enums;
using Bit.Core.Auth.Models.Data;
using Bit.Core.Auth.Repositories;
using Bit.Core.Auth.UserFeatures.TwoFactorAuth.Interfaces;
using Bit.Core.Enums;
using Bit.Core.Exceptions;
using Bit.Core.Models.Data.Organizations.OrganizationUsers;
@ -32,7 +34,6 @@ public class PolicyServiceTests
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Substitute.For<IUserService>(),
Substitute.For<IOrganizationService>(),
Guid.NewGuid()));
@ -60,7 +61,6 @@ public class PolicyServiceTests
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Substitute.For<IUserService>(),
Substitute.For<IOrganizationService>(),
Guid.NewGuid()));
@ -93,7 +93,6 @@ public class PolicyServiceTests
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Substitute.For<IUserService>(),
Substitute.For<IOrganizationService>(),
Guid.NewGuid()));
@ -125,7 +124,6 @@ public class PolicyServiceTests
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Substitute.For<IUserService>(),
Substitute.For<IOrganizationService>(),
Guid.NewGuid()));
@ -163,7 +161,6 @@ public class PolicyServiceTests
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Substitute.For<IUserService>(),
Substitute.For<IOrganizationService>(),
Guid.NewGuid()));
@ -192,7 +189,6 @@ public class PolicyServiceTests
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Substitute.For<IUserService>(),
Substitute.For<IOrganizationService>(),
Guid.NewGuid()));
@ -226,7 +222,7 @@ public class PolicyServiceTests
var utcNow = DateTime.UtcNow;
await sutProvider.Sut.SaveAsync(policy, Substitute.For<IUserService>(), Substitute.For<IOrganizationService>(), Guid.NewGuid());
await sutProvider.Sut.SaveAsync(policy, Substitute.For<IOrganizationService>(), Guid.NewGuid());
await sutProvider.GetDependency<IEventService>().Received()
.LogPolicyEventAsync(policy, EventType.Policy_Updated);
@ -256,7 +252,6 @@ public class PolicyServiceTests
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Substitute.For<IUserService>(),
Substitute.For<IOrganizationService>(),
Guid.NewGuid()));
@ -348,34 +343,39 @@ public class PolicyServiceTests
orgUserDetailAdmin
});
var userService = Substitute.For<IUserService>();
var organizationService = Substitute.For<IOrganizationService>();
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Any<IEnumerable<OrganizationUserUserDetails>>())
.Returns(new List<(OrganizationUserUserDetails user, bool hasTwoFactor)>()
{
(orgUserDetailUserInvited, false),
(orgUserDetailUserAcceptedWith2FA, true),
(orgUserDetailUserAcceptedWithout2FA, false),
(orgUserDetailAdmin, false),
});
userService.TwoFactorIsEnabledAsync(orgUserDetailUserInvited).Returns(false);
userService.TwoFactorIsEnabledAsync(orgUserDetailUserAcceptedWith2FA).Returns(true);
userService.TwoFactorIsEnabledAsync(orgUserDetailUserAcceptedWithout2FA).Returns(false);
userService.TwoFactorIsEnabledAsync(orgUserDetailAdmin).Returns(false);
var organizationService = Substitute.For<IOrganizationService>();
var removeOrganizationUserCommand = sutProvider.GetDependency<IRemoveOrganizationUserCommand>();
var utcNow = DateTime.UtcNow;
var savingUserId = Guid.NewGuid();
await sutProvider.Sut.SaveAsync(policy, userService, organizationService, savingUserId);
await sutProvider.Sut.SaveAsync(policy, organizationService, savingUserId);
await organizationService.Received()
await removeOrganizationUserCommand.Received()
.RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWithout2FA.Id, savingUserId);
await sutProvider.GetDependency<IMailService>().Received()
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWithout2FA.Email);
await organizationService.DidNotReceive()
await removeOrganizationUserCommand.DidNotReceive()
.RemoveUserAsync(policy.OrganizationId, orgUserDetailUserInvited.Id, savingUserId);
await sutProvider.GetDependency<IMailService>().DidNotReceive()
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserInvited.Email);
await organizationService.DidNotReceive()
await removeOrganizationUserCommand.DidNotReceive()
.RemoveUserAsync(policy.OrganizationId, orgUserDetailUserAcceptedWith2FA.Id, savingUserId);
await sutProvider.GetDependency<IMailService>().DidNotReceive()
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailUserAcceptedWith2FA.Email);
await organizationService.DidNotReceive()
await removeOrganizationUserCommand.DidNotReceive()
.RemoveUserAsync(policy.OrganizationId, orgUserDetailAdmin.Id, savingUserId);
await sutProvider.GetDependency<IMailService>().DidNotReceive()
.SendOrganizationUserRemovedForPolicyTwoStepEmailAsync(organization.DisplayName(), orgUserDetailAdmin.Email);
@ -456,21 +456,29 @@ public class PolicyServiceTests
orgUserDetailAdmin
});
var userService = Substitute.For<IUserService>();
var organizationService = Substitute.For<IOrganizationService>();
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids =>
ids.Contains(orgUserDetailUserWith2FANoMP.UserId.Value)
&& ids.Contains(orgUserDetailUserWithout2FA.UserId.Value)
&& ids.Contains(orgUserDetailAdmin.UserId.Value)))
.Returns(new List<(Guid userId, bool hasTwoFactor)>()
{
(orgUserDetailUserWith2FANoMP.UserId.Value, true),
(orgUserDetailUserWithout2FA.UserId.Value, false),
(orgUserDetailAdmin.UserId.Value, false),
});
userService.TwoFactorIsEnabledAsync(orgUserDetailUserWith2FANoMP).Returns(true);
userService.TwoFactorIsEnabledAsync(orgUserDetailUserWithout2FA).Returns(false);
userService.TwoFactorIsEnabledAsync(orgUserDetailAdmin).Returns(false);
var organizationService = Substitute.For<IOrganizationService>();
var removeOrganizationUserCommand = sutProvider.GetDependency<IRemoveOrganizationUserCommand>();
var savingUserId = Guid.NewGuid();
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy, userService, organizationService, savingUserId));
() => sutProvider.Sut.SaveAsync(policy, organizationService, savingUserId));
Assert.Contains("Policy could not be enabled. Non-compliant members will lose access to their accounts. Identify members without two-step login from the policies column in the members page.", badRequestException.Message, StringComparison.OrdinalIgnoreCase);
await organizationService.DidNotReceiveWithAnyArgs()
await removeOrganizationUserCommand.DidNotReceiveWithAnyArgs()
.RemoveUserAsync(organizationId: default, organizationUserId: default, deletingUserId: default);
await sutProvider.GetDependency<IMailService>().DidNotReceiveWithAnyArgs()
@ -526,17 +534,20 @@ public class PolicyServiceTests
orgUserDetail,
});
var userService = Substitute.For<IUserService>();
var organizationService = Substitute.For<IOrganizationService>();
sutProvider.GetDependency<ITwoFactorIsEnabledQuery>()
.TwoFactorIsEnabledAsync(Arg.Is<IEnumerable<Guid>>(ids => ids.Contains(orgUserDetail.UserId.Value)))
.Returns(new List<(Guid userId, bool hasTwoFactor)>()
{
(orgUserDetail.UserId.Value, false),
});
userService.TwoFactorIsEnabledAsync(orgUserDetail)
.Returns(false);
var organizationService = Substitute.For<IOrganizationService>();
var utcNow = DateTime.UtcNow;
var savingUserId = Guid.NewGuid();
await sutProvider.Sut.SaveAsync(policy, userService, organizationService, savingUserId);
await sutProvider.Sut.SaveAsync(policy, organizationService, savingUserId);
await sutProvider.GetDependency<IEventService>().Received()
.LogPolicyEventAsync(policy, EventType.Policy_Updated);
@ -579,7 +590,6 @@ public class PolicyServiceTests
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Substitute.For<IUserService>(),
Substitute.For<IOrganizationService>(),
Guid.NewGuid()));
@ -616,7 +626,6 @@ public class PolicyServiceTests
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Substitute.For<IUserService>(),
Substitute.For<IOrganizationService>(),
Guid.NewGuid()));
@ -650,7 +659,6 @@ public class PolicyServiceTests
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Substitute.For<IUserService>(),
Substitute.For<IOrganizationService>(),
Guid.NewGuid()));
@ -684,7 +692,6 @@ public class PolicyServiceTests
var badRequestException = await Assert.ThrowsAsync<BadRequestException>(
() => sutProvider.Sut.SaveAsync(policy,
Substitute.For<IUserService>(),
Substitute.For<IOrganizationService>(),
Guid.NewGuid()));

View File

@ -342,7 +342,6 @@ public class SsoConfigServiceTests
await sutProvider.GetDependency<IPolicyService>().Received(1)
.SaveAsync(
Arg.Is<Policy>(t => t.Type == PolicyType.SingleOrg),
Arg.Any<IUserService>(),
Arg.Any<IOrganizationService>(),
null
);
@ -350,7 +349,6 @@ public class SsoConfigServiceTests
await sutProvider.GetDependency<IPolicyService>().Received(1)
.SaveAsync(
Arg.Is<Policy>(t => t.Type == PolicyType.ResetPassword && t.GetDataModel<ResetPasswordDataModel>().AutoEnrollEnabled),
Arg.Any<IUserService>(),
Arg.Any<IOrganizationService>(),
null
);

View File

@ -29,7 +29,7 @@ public class PaymentHistoryServiceTests
var result = await paymentHistoryService.GetInvoiceHistoryAsync(subscriber);
// Assert
Assert.NotNull(result);
Assert.NotEmpty(result);
Assert.Single(result);
await stripeAdapter.Received(1).InvoiceListAsync(Arg.Any<StripeInvoiceListOptions>());
}
@ -47,7 +47,7 @@ public class PaymentHistoryServiceTests
var result = await paymentHistoryService.GetInvoiceHistoryAsync(null);
// Assert
Assert.Null(result);
Assert.Empty(result);
}
[Fact]
@ -66,7 +66,7 @@ public class PaymentHistoryServiceTests
var result = await paymentHistoryService.GetTransactionHistoryAsync(subscriber);
// Assert
Assert.NotNull(result);
Assert.NotEmpty(result);
Assert.Single(result);
await transactionRepository.Received(1).GetManyByOrganizationIdAsync(subscriber.Id, Arg.Any<int>(), Arg.Any<DateTime?>());
}
@ -84,6 +84,6 @@ public class PaymentHistoryServiceTests
var result = await paymentHistoryService.GetTransactionHistoryAsync(null);
// Assert
Assert.Null(result);
Assert.Empty(result);
}
}

View File

@ -17,7 +17,7 @@
</PackageReference>
<PackageReference Include="AutoFixture.Xunit2" Version="$(AutoFixtureXUnit2Version)" />
<PackageReference Include="AutoFixture.AutoNSubstitute" Version="$(AutoFixtureAutoNSubstituteVersion)" />
<PackageReference Include="Kralizek.AutoFixture.Extensions.MockHttp" Version="2.0.0" />
<PackageReference Include="Kralizek.AutoFixture.Extensions.MockHttp" Version="2.1.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Core\Core.csproj" />

View File

@ -1,5 +1,6 @@
using System.Text.Json;
using Bit.Core.AdminConsole.Entities;
using Bit.Core.AdminConsole.OrganizationFeatures.OrganizationUsers.Interfaces;
using Bit.Core.AdminConsole.Repositories;
using Bit.Core.AdminConsole.Services;
using Bit.Core.Auth.Enums;
@ -263,7 +264,8 @@ public class UserServiceTests
sutProvider.GetDependency<IStripeSyncService>(),
new FakeDataProtectorTokenFactory<OrgUserInviteTokenable>(),
sutProvider.GetDependency<IFeatureService>(),
sutProvider.GetDependency<IPremiumUserBillingService>()
sutProvider.GetDependency<IPremiumUserBillingService>(),
sutProvider.GetDependency<IRemoveOrganizationUserCommand>()
);
var actualIsVerified = await sut.VerifySecretAsync(user, secret);

View File

@ -4,11 +4,15 @@ using Bit.Core.Auth.Enums;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Identity.IdentityServer;
using Bit.Identity.Models.Request.Accounts;
using Bit.IntegrationTestCommon.Factories;
using Bit.Test.Common.AutoFixture.Attributes;
using Bit.Test.Common.Helpers;
using Duende.IdentityServer.Validation;
using Microsoft.AspNetCore.Identity;
using NSubstitute;
using Xunit;
namespace Bit.Identity.IntegrationTest.RequestValidation;
@ -21,6 +25,7 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
private readonly IdentityApplicationFactory _factory;
private readonly UserManager<User> _userManager;
private readonly IAuthRequestRepository _authRequestRepository;
private readonly IDeviceService _deviceService;
public ResourceOwnerPasswordValidatorTests(IdentityApplicationFactory factory)
{
@ -28,13 +33,13 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
_userManager = _factory.GetService<UserManager<User>>();
_authRequestRepository = _factory.GetService<IAuthRequestRepository>();
_deviceService = _factory.GetService<IDeviceService>();
}
[Fact]
public async Task ValidateAsync_Success()
{
// Arrange
// Arrange
await EnsureUserCreatedAsync();
// Act
@ -53,7 +58,7 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
[Fact]
public async Task ValidateAsync_AuthEmailHeaderInvalid_InvalidGrantResponse()
{
// Arrange
// Arrange
await EnsureUserCreatedAsync();
// Act
@ -88,12 +93,12 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
}
/// <summary>
/// I would have liked to spy into the IUserService but by spying into the IUserService it
/// creates a Singleton that is not available to the UserManager<User> thus causing the
/// I would have liked to spy into the IUserService but by spying into the IUserService it
/// creates a Singleton that is not available to the UserManager<User> thus causing the
/// RegisterAsync() to create a the user in a different UserStore than the one the
/// UserManager<User> has access to. This is an assumption made from observing the behavior while
/// writing theses tests. I could be wrong.
///
/// UserManager<User> has access to (This is an assumption made from observing the behavior while
/// writing theses tests, I could be wrong).
///
/// For the time being, verifying that the user is not null confirms that the failure is due to
/// a bad password.
/// </summary>
@ -102,10 +107,11 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
[Theory, BitAutoData]
public async Task ValidateAsync_BadPassword_Failure(string badPassword)
{
// Arrange
// Arrange
await EnsureUserCreatedAsync();
// Verify the User is not null to ensure the failure is due to bad password
Assert.NotNull(await _userManager.FindByEmailAsync(DefaultUsername));
// Act
var context = await _factory.Server.PostAsync("/connect/token",
@ -113,8 +119,6 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
context => context.SetAuthEmail(DefaultUsername));
// Assert
Assert.NotNull(await _userManager.FindByEmailAsync(DefaultUsername));
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
var root = body.RootElement;
@ -200,8 +204,8 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
// Assert
/*
An improvement on the current failure flow would be to document which part of
the flow failed since all of the failures are basically the same.
An improvement on the current failure flow would be to document which part of
the flow failed since all of the failures are basically the same.
This doesn't build confidence in the tests.
*/
@ -213,6 +217,43 @@ public class ResourceOwnerPasswordValidatorTests : IClassFixture<IdentityApplica
Assert.Equal("Username or password is incorrect. Try again.", errorMessage);
}
[Fact]
public async Task ValidateAsync_DeviceSaveAsync_ReturnsNullDevice_ErrorResult()
{
// Arrange
var factory = new IdentityApplicationFactory();
// Stub DeviceValidator
factory.SubstituteService<IDeviceValidator>(sub =>
{
sub.SaveDeviceAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
.Returns(null as Device);
});
// Add User
await factory.RegisterAsync(new RegisterRequestModel
{
Email = DefaultUsername,
MasterPasswordHash = DefaultPassword
});
var userManager = factory.GetService<UserManager<User>>();
var user = await userManager.FindByEmailAsync(DefaultUsername);
Assert.NotNull(user);
// Act
var context = await factory.Server.PostAsync("/connect/token",
GetFormUrlEncodedContent(),
context => context.SetAuthEmail(DefaultUsername));
// Assert
var body = await AssertHelper.AssertResponseTypeIs<JsonDocument>(context);
var root = body.RootElement;
var errorModel = AssertHelper.AssertJsonProperty(root, "ErrorModel", JsonValueKind.Object);
var errorMessage = AssertHelper.AssertJsonProperty(errorModel, "Message", JsonValueKind.String).GetString();
Assert.Equal("No device information provided.", errorMessage);
}
private async Task EnsureUserCreatedAsync(IdentityApplicationFactory factory = null)
{
factory ??= _factory;

View File

@ -29,10 +29,9 @@ namespace Bit.Identity.Test.IdentityServer;
public class BaseRequestValidatorTests
{
private UserManager<User> _userManager;
private readonly IDeviceRepository _deviceRepository;
private readonly IDeviceService _deviceService;
private readonly IUserService _userService;
private readonly IEventService _eventService;
private readonly IDeviceValidator _deviceValidator;
private readonly IOrganizationDuoWebTokenProvider _organizationDuoWebTokenProvider;
private readonly ITemporaryDuoWebV4SDKService _duoWebV4SDKService;
private readonly IOrganizationRepository _organizationRepository;
@ -53,10 +52,9 @@ public class BaseRequestValidatorTests
public BaseRequestValidatorTests()
{
_deviceRepository = Substitute.For<IDeviceRepository>();
_deviceService = Substitute.For<IDeviceService>();
_userService = Substitute.For<IUserService>();
_eventService = Substitute.For<IEventService>();
_deviceValidator = Substitute.For<IDeviceValidator>();
_organizationDuoWebTokenProvider = Substitute.For<IOrganizationDuoWebTokenProvider>();
_duoWebV4SDKService = Substitute.For<ITemporaryDuoWebV4SDKService>();
_organizationRepository = Substitute.For<IOrganizationRepository>();
@ -76,10 +74,9 @@ public class BaseRequestValidatorTests
_sut = new BaseRequestValidatorTestWrapper(
_userManager,
_deviceRepository,
_deviceService,
_userService,
_eventService,
_deviceValidator,
_organizationDuoWebTokenProvider,
_duoWebV4SDKService,
_organizationRepository,
@ -228,7 +225,8 @@ public class BaseRequestValidatorTests
public async Task ValidateAsync_ClientCredentialsGrantType_ShouldSucceed(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
GrantValidationResult grantResult,
Device device)
{
// Arrange
var context = CreateContext(tokenRequest, requestContext, grantResult);
@ -240,18 +238,13 @@ public class BaseRequestValidatorTests
_globalSettings.DisableEmailNewDevice = false;
context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device
context.ValidatedTokenRequest.Raw["DeviceIdentifier"] = "DeviceIdentifier";
context.ValidatedTokenRequest.Raw["DeviceType"] = "Android"; // This needs to be an actual Type
context.ValidatedTokenRequest.Raw["DeviceName"] = "DeviceName";
context.ValidatedTokenRequest.Raw["DevicePushToken"] = "DevicePushToken";
_deviceValidator.SaveDeviceAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
.Returns(device);
// Act
await _sut.ValidateAsync(context);
// Assert
await _mailService.Received(1).SendNewDeviceLoggedInEmail(
context.CustomValidatorRequestContext.User.Email, "Android", Arg.Any<DateTime>(), Arg.Any<string>()
);
Assert.False(context.GrantResult.IsError);
}
@ -262,7 +255,8 @@ public class BaseRequestValidatorTests
public async Task ValidateAsync_ClientCredentialsGrantType_ExistingDevice_ShouldSucceed(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest tokenRequest,
CustomValidatorRequestContext requestContext,
GrantValidationResult grantResult)
GrantValidationResult grantResult,
Device device)
{
// Arrange
var context = CreateContext(tokenRequest, requestContext, grantResult);
@ -274,13 +268,9 @@ public class BaseRequestValidatorTests
_globalSettings.DisableEmailNewDevice = false;
context.ValidatedTokenRequest.GrantType = "client_credentials"; // This || AuthCode will allow process to continue to get device
context.ValidatedTokenRequest.Raw["DeviceIdentifier"] = "DeviceIdentifier";
context.ValidatedTokenRequest.Raw["DeviceType"] = "Android"; // This needs to be an actual Type
context.ValidatedTokenRequest.Raw["DeviceName"] = "DeviceName";
context.ValidatedTokenRequest.Raw["DevicePushToken"] = "DevicePushToken";
_deviceRepository.GetByIdentifierAsync("DeviceIdentifier", Arg.Any<Guid>())
.Returns(new Device() { Identifier = "DeviceIdentifier" });
_deviceValidator.SaveDeviceAsync(Arg.Any<User>(), Arg.Any<ValidatedTokenRequest>())
.Returns(device);
// Act
await _sut.ValidateAsync(context);

View File

@ -0,0 +1,247 @@
using Bit.Core.Context;
using Bit.Core.Entities;
using Bit.Core.Enums;
using Bit.Core.Repositories;
using Bit.Core.Services;
using Bit.Core.Settings;
using Bit.Identity.IdentityServer;
using Bit.Test.Common.AutoFixture.Attributes;
using Duende.IdentityServer.Validation;
using NSubstitute;
using Xunit;
using AuthFixtures = Bit.Identity.Test.AutoFixture;
namespace Bit.Identity.Test.IdentityServer;
public class DeviceValidatorTests
{
private readonly IDeviceService _deviceService;
private readonly IDeviceRepository _deviceRepository;
private readonly GlobalSettings _globalSettings;
private readonly IMailService _mailService;
private readonly ICurrentContext _currentContext;
private readonly DeviceValidator _sut;
public DeviceValidatorTests()
{
_deviceService = Substitute.For<IDeviceService>();
_deviceRepository = Substitute.For<IDeviceRepository>();
_globalSettings = new GlobalSettings();
_mailService = Substitute.For<IMailService>();
_currentContext = Substitute.For<ICurrentContext>();
_sut = new DeviceValidator(
_deviceService,
_deviceRepository,
_globalSettings,
_mailService,
_currentContext);
}
[Theory]
[BitAutoData]
public async void SaveDeviceAsync_DeviceNull_ShouldReturnNull(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
User user)
{
// Arrange
request.Raw["DeviceIdentifier"] = null;
// Act
var device = await _sut.SaveDeviceAsync(user, request);
// Assert
Assert.Null(device);
await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail(
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<string>());
}
[Theory]
[BitAutoData]
public async void SaveDeviceAsync_UserIsNull_ShouldReturnNull(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
request = AddValidDeviceToRequest(request);
// Act
var device = await _sut.SaveDeviceAsync(null, request);
// Assert
Assert.Null(device);
await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail(
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<string>());
}
[Theory]
[BitAutoData]
public async void SaveDeviceAsync_ExistingUser_NewDevice_ReturnsDevice_SendsEmail(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
User user)
{
// Arrange
request = AddValidDeviceToRequest(request);
user.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(11);
_globalSettings.DisableEmailNewDevice = false;
// Act
var device = await _sut.SaveDeviceAsync(user, request);
// Assert
Assert.NotNull(device);
Assert.Equal(user.Id, device.UserId);
Assert.Equal("DeviceIdentifier", device.Identifier);
Assert.Equal(DeviceType.Android, device.Type);
await _mailService.Received(1).SendNewDeviceLoggedInEmail(
user.Email, "Android", Arg.Any<DateTime>(), Arg.Any<string>());
}
[Theory]
[BitAutoData]
public async void SaveDeviceAsync_ExistingUser_NewDevice_ReturnsDevice_SendEmailFalse(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
User user)
{
// Arrange
request = AddValidDeviceToRequest(request);
user.CreationDate = DateTime.UtcNow - TimeSpan.FromMinutes(11);
_globalSettings.DisableEmailNewDevice = true;
// Act
var device = await _sut.SaveDeviceAsync(user, request);
// Assert
Assert.NotNull(device);
Assert.Equal(user.Id, device.UserId);
Assert.Equal("DeviceIdentifier", device.Identifier);
Assert.Equal(DeviceType.Android, device.Type);
await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail(
user.Email, "Android", Arg.Any<DateTime>(), Arg.Any<string>());
}
[Theory]
[BitAutoData]
public async void SaveDeviceAsync_DeviceIsKnown_ShouldReturnDevice(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
User user,
Device device)
{
// Arrange
request = AddValidDeviceToRequest(request);
device.UserId = user.Id;
device.Identifier = "DeviceIdentifier";
device.Type = DeviceType.Android;
device.Name = "DeviceName";
device.PushToken = "DevicePushToken";
_deviceRepository.GetByIdentifierAsync(device.Identifier, user.Id).Returns(device);
// Act
var resultDevice = await _sut.SaveDeviceAsync(user, request);
// Assert
Assert.Equal(device, resultDevice);
await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail(
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<string>());
}
[Theory]
[BitAutoData]
public async void SaveDeviceAsync_NewUser_DeviceUnknown_ShouldSaveDevice_NoEmail(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
User user)
{
// Arrange
request = AddValidDeviceToRequest(request);
user.CreationDate = DateTime.UtcNow;
_deviceRepository.GetByIdentifierAsync(Arg.Any<string>(), Arg.Any<Guid>()).Returns(null as Device);
// Act
var device = await _sut.SaveDeviceAsync(user, request);
// Assert
Assert.NotNull(device);
Assert.Equal(user.Id, device.UserId);
Assert.Equal("DeviceIdentifier", device.Identifier);
Assert.Equal(DeviceType.Android, device.Type);
await _deviceService.Received(1).SaveAsync(device);
await _mailService.DidNotReceive().SendNewDeviceLoggedInEmail(
Arg.Any<string>(), Arg.Any<string>(), Arg.Any<DateTime>(), Arg.Any<string>());
}
[Theory]
[BitAutoData]
public async void KnownDeviceAsync_UserNull_ReturnsFalse(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request)
{
// Arrange
request = AddValidDeviceToRequest(request);
// Act
var result = await _sut.KnownDeviceAsync(null, request);
// Assert
Assert.False(result);
}
[Theory]
[BitAutoData]
public async void KnownDeviceAsync_DeviceNull_ReturnsFalse(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
User user)
{
// Arrange
// Device raw data is null which will cause the device to be null
// Act
var result = await _sut.KnownDeviceAsync(user, request);
// Assert
Assert.False(result);
}
[Theory]
[BitAutoData]
public async void KnownDeviceAsync_DeviceNotInDatabase_ReturnsFalse(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
User user)
{
// Arrange
request = AddValidDeviceToRequest(request);
_deviceRepository.GetByIdentifierAsync(Arg.Any<string>(), Arg.Any<Guid>())
.Returns(null as Device);
// Act
var result = await _sut.KnownDeviceAsync(user, request);
// Assert
Assert.False(result);
}
[Theory]
[BitAutoData]
public async void KnownDeviceAsync_UserAndDeviceValid_ReturnsTrue(
[AuthFixtures.ValidatedTokenRequest] ValidatedTokenRequest request,
User user,
Device device)
{
// Arrange
request = AddValidDeviceToRequest(request);
_deviceRepository.GetByIdentifierAsync(Arg.Any<string>(), Arg.Any<Guid>())
.Returns(device);
// Act
var result = await _sut.KnownDeviceAsync(user, request);
// Assert
Assert.True(result);
}
private ValidatedTokenRequest AddValidDeviceToRequest(ValidatedTokenRequest request)
{
request.Raw["DeviceIdentifier"] = "DeviceIdentifier";
request.Raw["DeviceType"] = "Android";
request.Raw["DeviceName"] = "DeviceName";
request.Raw["DevicePushToken"] = "DevicePushToken";
return request;
}
}

View File

@ -51,10 +51,9 @@ IBaseRequestValidatorTestWrapper
public bool isValid { get; set; }
public BaseRequestValidatorTestWrapper(
UserManager<User> userManager,
IDeviceRepository deviceRepository,
IDeviceService deviceService,
IUserService userService,
IEventService eventService,
IDeviceValidator deviceValidator,
IOrganizationDuoWebTokenProvider organizationDuoWebTokenProvider,
ITemporaryDuoWebV4SDKService duoWebV4SDKService,
IOrganizationRepository organizationRepository,
@ -72,10 +71,9 @@ IBaseRequestValidatorTestWrapper
IUserDecryptionOptionsBuilder userDecryptionOptionsBuilder) :
base(
userManager,
deviceRepository,
deviceService,
userService,
eventService,
deviceValidator,
organizationDuoWebTokenProvider,
duoWebV4SDKService,
organizationRepository,

View File

@ -145,7 +145,10 @@ public abstract class WebApplicationFactoryBase<T> : WebApplicationFactory<T>
// Email Verification
{ "globalSettings:enableEmailVerification", "true" },
{ "globalSettings:disableUserRegistration", "false" },
{ "globalSettings:launchDarkly:flagValues:email-verification", "true" }
{ "globalSettings:launchDarkly:flagValues:email-verification", "true" },
// New Device Verification
{ "globalSettings:disableEmailNewDevice", "false" },
});
});

View File

@ -14,13 +14,15 @@ public class DbMigrator
private readonly string _connectionString;
private readonly ILogger<DbMigrator> _logger;
private readonly bool _skipDatabasePreparation;
private readonly bool _noTransactionMigration;
public DbMigrator(string connectionString, ILogger<DbMigrator> logger = null,
bool skipDatabasePreparation = false)
bool skipDatabasePreparation = false, bool noTransactionMigration = false)
{
_connectionString = connectionString;
_logger = logger ?? CreateLogger();
_skipDatabasePreparation = skipDatabasePreparation;
_noTransactionMigration = noTransactionMigration;
}
public bool MigrateMsSqlDatabaseWithRetries(bool enableLogging = true,
@ -30,6 +32,7 @@ public class DbMigrator
CancellationToken cancellationToken = default)
{
var attempt = 1;
while (attempt < 10)
{
try
@ -69,6 +72,7 @@ public class DbMigrator
using (var connection = new SqlConnection(masterConnectionString))
{
var databaseName = new SqlConnectionStringBuilder(_connectionString).InitialCatalog;
if (string.IsNullOrWhiteSpace(databaseName))
{
databaseName = "vault";
@ -105,10 +109,10 @@ public class DbMigrator
}
private bool MigrateDatabase(bool enableLogging = true,
bool repeatable = false,
string folderName = MigratorConstants.DefaultMigrationsFolderName,
bool dryRun = false,
CancellationToken cancellationToken = default)
bool repeatable = false,
string folderName = MigratorConstants.DefaultMigrationsFolderName,
bool dryRun = false,
CancellationToken cancellationToken = default)
{
if (enableLogging)
{
@ -121,8 +125,17 @@ public class DbMigrator
.SqlDatabase(_connectionString)
.WithScriptsAndCodeEmbeddedInAssembly(Assembly.GetExecutingAssembly(),
s => s.Contains($".{folderName}.") && !s.Contains(".Archive."))
.WithTransaction()
.WithExecutionTimeout(new TimeSpan(0, 5, 0));
.WithExecutionTimeout(TimeSpan.FromMinutes(5));
if (_noTransactionMigration)
{
builder = builder.WithoutTransaction()
.WithExecutionTimeout(TimeSpan.FromMinutes(60));
}
else
{
builder = builder.WithTransaction();
}
if (repeatable)
{
@ -144,6 +157,7 @@ public class DbMigrator
{
var scriptsToExec = upgrader.GetScriptsToExecute();
var stringBuilder = new StringBuilder("Scripts that will be applied:");
foreach (var script in scriptsToExec)
{
stringBuilder.AppendLine(script.Name);

View File

@ -0,0 +1,24 @@
CREATE OR ALTER PROCEDURE [dbo].[VerifiedOrganizationDomainSsoDetails_ReadByEmail]
@Email NVARCHAR(256)
AS
BEGIN
SET NOCOUNT ON
DECLARE @Domain NVARCHAR(256)
SELECT @Domain = SUBSTRING(@Email, CHARINDEX( '@', @Email) + 1, LEN(@Email))
SELECT
O.Id AS OrganizationId,
O.Name AS OrganizationName,
O.Identifier AS OrganizationIdentifier,
OD.DomainName
FROM [dbo].[OrganizationView] O
INNER JOIN [dbo].[OrganizationDomainView] OD ON O.Id = OD.OrganizationId
LEFT JOIN [dbo].[Ssoconfig] S ON O.Id = S.OrganizationId
WHERE OD.DomainName = @Domain
AND O.Enabled = 1
AND OD.VerifiedDate IS NOT NULL
AND S.Enabled = 1
END
GO

View File

@ -17,13 +17,15 @@ internal class Program
[Option('f', "folder", Description = "Folder name of database scripts")]
string folderName = MigratorConstants.DefaultMigrationsFolderName,
[Option('d', "dry-run", Description = "Print the scripts that will be applied without actually executing them")]
bool dryRun = false
) => MigrateDatabase(databaseConnectionString, repeatable, folderName, dryRun);
bool dryRun = false,
[Option("no-transaction", Description = "Run without adding transaction per script or all scripts")]
bool noTransactionMigration = false
) => MigrateDatabase(databaseConnectionString, repeatable, folderName, dryRun, noTransactionMigration);
private static bool MigrateDatabase(string databaseConnectionString,
bool repeatable = false, string folderName = "", bool dryRun = false)
bool repeatable = false, string folderName = "", bool dryRun = false, bool noTransactionMigration = false)
{
var migrator = new DbMigrator(databaseConnectionString);
var migrator = new DbMigrator(databaseConnectionString, noTransactionMigration: noTransactionMigration);
bool success;
if (!string.IsNullOrWhiteSpace(folderName))
{